diff --git a/Utils.cpp b/Utils.cpp index 078fb6d2..a0ae68ad 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -142,6 +142,23 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, const char* n return search.result; } +bool Utils::IsValidAlignment(const uint32_t alignment) { + switch (alignment) { + case OBS_ALIGN_CENTER: + case OBS_ALIGN_LEFT: + case OBS_ALIGN_RIGHT: + case OBS_ALIGN_TOP: + case OBS_ALIGN_BOTTOM: + case OBS_ALIGN_TOP | OBS_ALIGN_LEFT: + case OBS_ALIGN_TOP | OBS_ALIGN_RIGHT: + case OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT: + case OBS_ALIGN_BOTTOM | OBS_ALIGN_RIGHT: { + return true; + } + } + return false; +} + obs_source_t* Utils::GetTransitionFromName(const char* search_name) { obs_source_t* found_transition = NULL; diff --git a/Utils.h b/Utils.h index 83dfaaee..f313e5c8 100644 --- a/Utils.h +++ b/Utils.h @@ -39,6 +39,8 @@ class Utils { static obs_source_t* GetTransitionFromName(const char* search_name); static obs_source_t* GetSceneFromNameOrCurrent(const char* scene_name); + static bool IsValidAlignment(const uint32_t alignment); + static obs_data_array_t* GetScenes(); static obs_data_t* GetSceneData(obs_source* source); diff --git a/WSRequestHandler.cpp b/WSRequestHandler.cpp index 005e2716..fc3fdc1f 100644 --- a/WSRequestHandler.cpp +++ b/WSRequestHandler.cpp @@ -55,6 +55,8 @@ WSRequestHandler::WSRequestHandler(QWebSocket* client) : messageMap["SetSceneItemPosition"] = WSRequestHandler::HandleSetSceneItemPosition; messageMap["SetSceneItemTransform"] = WSRequestHandler::HandleSetSceneItemTransform; messageMap["SetSceneItemCrop"] = WSRequestHandler::HandleSetSceneItemCrop; + messageMap["GetSceneItemProperties"] = WSRequestHandler::HandleGetSceneItemProperties; + messageMap["SetSceneItemProperties"] = WSRequestHandler::HandleSetSceneItemProperties; messageMap["ResetSceneItem"] = WSRequestHandler::HandleResetSceneItem; messageMap["GetStreamingStatus"] = WSRequestHandler::HandleGetStreamingStatus; @@ -186,6 +188,17 @@ void WSRequestHandler::SendErrorResponse(const char* errorMessage) { SendResponse(response); } +void WSRequestHandler::SendErrorResponse(obs_data_t* additionalFields) { + obs_data_t* response = obs_data_create(); + obs_data_set_string(response, "status", "error"); + obs_data_set_string(response, "message-id", _messageId); + + if (additionalFields) + obs_data_set_obj(response, "error", additionalFields); + + SendResponse(response); +} + void WSRequestHandler::SendResponse(obs_data_t* response) { const char *json = obs_data_get_json(response); _client->sendTextMessage(json); @@ -1315,8 +1328,8 @@ void WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req) { if (scene_item) { struct obs_sceneitem_crop crop = { 0 }; crop.top = obs_data_get_int(req->data, "top"); - crop.bottom = obs_data_get_int(req->data, "bottom");; - crop.left = obs_data_get_int(req->data, "left");; + crop.bottom = obs_data_get_int(req->data, "bottom"); + crop.left = obs_data_get_int(req->data, "left"); crop.right = obs_data_get_int(req->data, "right"); obs_sceneitem_set_crop(scene_item, &crop); @@ -1330,6 +1343,324 @@ void WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req) { obs_source_release(scene); } +/** + * Gets the scene specific properties of the specified source item. + * + * @param {String (optional)} `scene-name` the name of the scene that the source item belongs to. Defaults to the current scene. + * @param {String} `item` The name of the source. + * + * @return {String} `name` The name of the source. + * @return {int} `position.x` The x position of the source from the left. + * @return {int} `position.y` The y position of the source from the top. + * @return {int} `position.alignment` The point on the source that the item is manipulated from. + * @return {double} `rotation` The clockwise rotation of the item in degrees around the point of alignment. + * @return {double} `scale.x` The x-scale factor of the source. + * @return {double} `scale.y` The y-scale factor of the source. + * @return {int} `crop.top` The number of pixels cropped off the top of the source before scaling. + * @return {int} `crop.right` The number of pixels cropped off the right of the source before scaling. + * @return {int} `crop.bottom` The number of pixels cropped off the bottom of the source before scaling. + * @return {int} `crop.left` The number of pixels cropped off the left of the source before scaling. + * @return {bool} `visible` If the source is visible. + * @return {String} `bounds.type` Type of bounding box. + * @return {int} `bounds.alignment` Alignment of the bounding box. + * @return {double} `bounds.x` Width of the bounding box. + * @return {double} `bounds.y` Height of the bounding box. + * + * @api requests + * @name GetSceneItemSceneProperties + * @category sources + * @since unreleased + */ +void WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler* req) { + if (!req->hasField("item")) { + req->SendErrorResponse("missing request parameters"); + return; + } + + const char* item_name = obs_data_get_string(req->data, "item"); + if (!str_valid(item_name)) { + req->SendErrorResponse("invalid request parameters"); + return; + } + + const char* scene_name = obs_data_get_string(req->data, "scene-name"); + obs_source_t* scene = Utils::GetSceneFromNameOrCurrent(scene_name); + if (!scene) { + req->SendErrorResponse("requested scene doesn't exist"); + return; + } + + obs_sceneitem_t* scene_item = Utils::GetSceneItemFromName(scene, item_name); + if (!scene_item) { + req->SendErrorResponse("specified scene item doesn't exist"); + obs_source_release(scene); + return; + } + + obs_data_t* data = obs_data_create(); + + obs_data_set_string(data, "name", item_name); + + obs_data_t* pos_data = obs_data_create(); + vec2 pos; + obs_sceneitem_get_pos(scene_item, &pos); + obs_data_set_double(pos_data, "x", pos.x); + obs_data_set_double(pos_data, "y", pos.y); + obs_data_set_int(pos_data, "alignment", obs_sceneitem_get_alignment(scene_item)); + obs_data_set_obj(data, "position", pos_data); + + obs_data_set_double(data, "rotation", obs_sceneitem_get_rot(scene_item)); + + obs_data_t* scale_data = obs_data_create(); + vec2 scale; + obs_sceneitem_get_scale(scene_item, &scale); + obs_data_set_double(scale_data, "x", scale.x); + obs_data_set_double(scale_data, "y", scale.y); + obs_data_set_obj(data, "scale", scale_data); + + obs_data_t* crop_data = obs_data_create(); + obs_sceneitem_crop crop; + obs_sceneitem_get_crop(scene_item, &crop); + obs_data_set_int(crop_data, "left", crop.left); + obs_data_set_int(crop_data, "top", crop.top); + obs_data_set_int(crop_data, "right", crop.right); + obs_data_set_int(crop_data, "bottom", crop.bottom); + obs_data_set_obj(data, "crop", crop_data); + + obs_data_set_bool(data, "visible", obs_sceneitem_visible(scene_item)); + + obs_data_t* bounds_data = obs_data_create(); + obs_bounds_type bounds_type = obs_sceneitem_get_bounds_type(scene_item); + if (bounds_type == OBS_BOUNDS_NONE) { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_NONE"); + } + else { + switch(bounds_type) { + case OBS_BOUNDS_STRETCH: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_STRETCH"); + break; + } + case OBS_BOUNDS_SCALE_INNER: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_SCALE_INNER"); + break; + } + case OBS_BOUNDS_SCALE_OUTER: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_SCALE_OUTER"); + break; + } + case OBS_BOUNDS_SCALE_TO_WIDTH: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_SCALE_TO_WIDTH"); + break; + } + case OBS_BOUNDS_SCALE_TO_HEIGHT: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_SCALE_TO_HEIGHT"); + break; + } + case OBS_BOUNDS_MAX_ONLY: { + obs_data_set_string(bounds_data, "type", "OBS_BOUNDS_MAX_ONLY"); + break; + } + } + obs_data_set_int(bounds_data, "alignment", obs_sceneitem_get_bounds_alignment(scene_item)); + vec2 bounds; + obs_sceneitem_get_bounds(scene_item, &bounds); + obs_data_set_double(bounds_data, "x", bounds.x); + obs_data_set_double(bounds_data, "y", bounds.y); + } + obs_data_set_obj(data, "bounds", bounds_data); + + obs_sceneitem_release(scene_item); + req->SendOKResponse(data); + obs_source_release(scene); +} + +/** + * Sets the scene specific properties of a source. Unspecified properties will remain unchanged. + * + * @param {String (optional)} `scene-name` the name of the scene that the source item belongs to. Defaults to the current scene. + * @param {String} `item` The name of the source. + * @param {int} `position.x` The new x position of the source. + * @param {int} `position.y` The new y position of the source. + * @param {int} `position.alignment` The new alignment of the source. + * @param {double} `rotation` The new clockwise rotation of the item in degrees. + * @param {double} `scale.x` The new x scale of the item. + * @param {double} `scale.y` The new y scale of the item. + * @param {int} `crop.top` The new amount of pixels cropped off the top of the source before scaling. + * @param {int} `crop.bottom` The new amount of pixels cropped off the bottom of the source before scaling. + * @param {int} `crop.left` The new amount of pixels cropped off the left of the source before scaling. + * @param {int} `crop.right` The new amount of pixels cropped off the right of the source before scaling. + * @param {bool} `visible` The new visibility of the source. 'true' shows source, 'false' hides source. + * @param {String} `bounds.type` The new bounds type of the source. + * @param {int} `bounds.alignment` The new alignment of the bounding box. (0-2, 4-6, 8-10) + * @param {double} `bounds.x` The new width of the bounding box. + * @param {double} `bounds.y' The new height of the bounding box. + * + * @api requests + * @name SetSceneItemProperties + * @category sources + * @since unreleased + */ +void WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* req) { + if (!req->hasField("item")) { + req->SendErrorResponse("missing request parameters"); + return; + } + + const char* item_name = obs_data_get_string(req->data, "item"); + if (!str_valid(item_name)) { + req->SendErrorResponse("invalid request parameters"); + return; + } + + const char* scene_name = obs_data_get_string(req->data, "scene-name"); + obs_source_t* scene = Utils::GetSceneFromNameOrCurrent(scene_name); + if (!scene) { + req->SendErrorResponse("requested scene doesn't exist"); + return; + } + + obs_sceneitem_t* scene_item = Utils::GetSceneItemFromName(scene, item_name); + if (!scene_item) { + req->SendErrorResponse("specified scene item doesn't exist"); + obs_source_release(scene); + return; + } + + bool bad_request = false; + obs_data_t* error_message = obs_data_create(); + + if (req->hasField("position")) { + vec2 old_position; + obs_data_t* position_error = obs_data_create(); + obs_sceneitem_get_pos(scene_item, &old_position); + obs_data_t* req_position = obs_data_get_obj(req->data, "position"); + vec2 new_position = old_position; + if (obs_data_has_user_value(req_position, "x")) { + new_position.x = obs_data_get_int(req_position, "x"); + } + if (obs_data_has_user_value(req_position, "y")) { + new_position.y = obs_data_get_int(req_position, "y"); + } + if (obs_data_has_user_value(req_position, "alignment")) { + const uint32_t alignment = obs_data_get_int(req_position, "alignment"); + if (Utils::IsValidAlignment(alignment)) { + obs_sceneitem_set_alignment(scene_item, alignment); + } else { + bad_request = true; + obs_data_set_string(position_error, "alignment", "invalid"); + obs_data_set_obj(error_message, "position", position_error); + } + } + obs_sceneitem_set_pos(scene_item, &new_position); + } + + if (req->hasField("rotation")) { + obs_sceneitem_set_rot(scene_item, (float)obs_data_get_double(req->data, "rotation")); + } + + if (req->hasField("scale")) { + vec2 old_scale; + obs_sceneitem_get_scale(scene_item, &old_scale); + obs_data_t* req_scale = obs_data_get_obj(req->data, "scale"); + vec2 new_scale = old_scale; + if (obs_data_has_user_value(req_scale, "x")) { + new_scale.x = obs_data_get_double(req_scale, "x"); + } + if (obs_data_has_user_value(req_scale, "y")) { + new_scale.y = obs_data_get_double(req_scale, "y"); + } + obs_sceneitem_set_scale(scene_item, &new_scale); + } + + if (req->hasField("crop")) { + obs_sceneitem_crop old_crop; + obs_sceneitem_get_crop(scene_item, &old_crop); + obs_data_t* req_crop = obs_data_get_obj(req->data, "crop"); + obs_sceneitem_crop new_crop = old_crop; + if (obs_data_has_user_value(req_crop, "top")) { + new_crop.top = obs_data_get_int(req_crop, "top"); + } + if (obs_data_has_user_value(req_crop, "right")) { + new_crop.right = obs_data_get_int(req_crop, "right"); + } + if (obs_data_has_user_value(req_crop, "bottom")) { + new_crop.bottom = obs_data_get_int(req_crop, "bottom"); + } + if (obs_data_has_user_value(req_crop, "left")) { + new_crop.left = obs_data_get_int(req_crop, "left"); + } + obs_sceneitem_set_crop(scene_item, &new_crop); + } + + if (req->hasField("visible")) { + obs_sceneitem_set_visible(scene_item, obs_data_get_bool(req->data, "visible")); + } + + if (req->hasField("bounds")) { + bool bad_bounds = false; + obs_data_t* bounds_error = obs_data_create(); + obs_data_t* req_bounds = obs_data_get_obj(req->data, "bounds"); + if (obs_data_has_user_value(req_bounds, "type")) { + const char* new_bounds_type = obs_data_get_string(req_bounds, "type"); + if (new_bounds_type == "OBS_BOUNDS_NONE") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_NONE); + } + else if (new_bounds_type == "OBS_BOUNDS_STRETCH") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_STRETCH); + } + else if (new_bounds_type == "OBS_BOUNDS_SCALE_INNER") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_SCALE_INNER); + } + else if (new_bounds_type == "OBS_BOUNDS_SCALE_OUTER") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_SCALE_OUTER); + } + else if (new_bounds_type == "OBS_BOUNDS_SCALE_TO_WIDTH") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_SCALE_TO_WIDTH); + } + else if (new_bounds_type == "OBS_BOUNDS_SCALE_TO_HEIGHT") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_SCALE_TO_HEIGHT); + } + else if (new_bounds_type == "OBS_BOUNDS_MAX_ONLY") { + obs_sceneitem_set_bounds_type(scene_item, OBS_BOUNDS_MAX_ONLY); + } + else { + bad_request = bad_bounds = true; + obs_data_set_string(bounds_error, "type", "invalid"); + } + } + vec2 old_bounds; + obs_sceneitem_get_bounds(scene_item, &old_bounds); + vec2 new_bounds = old_bounds; + if (obs_data_has_user_value(req_bounds, "x")) { + new_bounds.x = obs_data_get_double(req_bounds, "x"); + } + if (obs_data_has_user_value(req_bounds, "y")) { + new_bounds.y = obs_data_get_double(req_bounds, "y"); + } + obs_sceneitem_set_bounds(scene_item, &new_bounds); + if (obs_data_has_user_value(req_bounds, "alignment")) { + const uint32_t bounds_alignment = obs_data_get_int(req_bounds, "alignment"); + if (Utils::IsValidAlignment(bounds_alignment)) { + obs_sceneitem_set_bounds_alignment(scene_item, bounds_alignment); + } else { + bad_request = bad_bounds = true; + obs_data_set_string(bounds_error, "alignment", "invalid"); + } + } + if (bad_bounds) { + obs_data_set_obj(error_message, "bounds", bounds_error); + } + } + + obs_sceneitem_release(scene_item); + if (bad_request) { + req->SendErrorResponse(error_message); + } else { + req->SendOKResponse(); + } + obs_source_release(scene); +} + /** * Change the active scene collection. * diff --git a/WSRequestHandler.h b/WSRequestHandler.h index cde4f736..53eb56a2 100644 --- a/WSRequestHandler.h +++ b/WSRequestHandler.h @@ -46,6 +46,7 @@ class WSRequestHandler : public QObject { void SendOKResponse(obs_data_t* additionalFields = NULL); void SendErrorResponse(const char* errorMessage); + void SendErrorResponse(obs_data_t* additionalFields = NULL); void SendResponse(obs_data_t* response); static void HandleGetVersion(WSRequestHandler* req); @@ -62,6 +63,8 @@ class WSRequestHandler : public QObject { static void HandleSetSceneItemPosition(WSRequestHandler* req); static void HandleSetSceneItemTransform(WSRequestHandler* req); static void HandleSetSceneItemCrop(WSRequestHandler* req); + static void HandleGetSceneItemProperties(WSRequestHandler* req); + static void HandleSetSceneItemProperties(WSRequestHandler* req); static void HandleResetSceneItem(WSRequestHandler* req); static void HandleGetStreamingStatus(WSRequestHandler* req);