diff --git a/src/requesthandler/RequestHandler.cpp b/src/requesthandler/RequestHandler.cpp index d8ef94ef..fb9b00cf 100644 --- a/src/requesthandler/RequestHandler.cpp +++ b/src/requesthandler/RequestHandler.cpp @@ -168,6 +168,7 @@ const std::unordered_map RequestHandler::_han {"StartStream", &RequestHandler::StartStream}, {"StopStream", &RequestHandler::StopStream}, {"SendStreamCaption", &RequestHandler::SendStreamCaption}, + {"GetStreamScreenshot", &RequestHandler::GetStreamScreenshot}, // Record {"GetRecordStatus", &RequestHandler::GetRecordStatus}, diff --git a/src/requesthandler/RequestHandler.h b/src/requesthandler/RequestHandler.h index ea023afd..58efe3fb 100644 --- a/src/requesthandler/RequestHandler.h +++ b/src/requesthandler/RequestHandler.h @@ -187,6 +187,7 @@ class RequestHandler { RequestResult StartStream(const Request &); RequestResult StopStream(const Request &); RequestResult SendStreamCaption(const Request &); + RequestResult GetStreamScreenshot(const Request &request); // Record RequestResult GetRecordStatus(const Request &); diff --git a/src/requesthandler/RequestHandler_General.cpp b/src/requesthandler/RequestHandler_General.cpp index 0bd5436d..a3e705a3 100644 --- a/src/requesthandler/RequestHandler_General.cpp +++ b/src/requesthandler/RequestHandler_General.cpp @@ -33,7 +33,7 @@ with this program. If not, see * @responseField obsWebSocketVersion | String | Current obs-websocket version * @responseField rpcVersion | Number | Current latest obs-websocket RPC version * @responseField availableRequests | Array | Array of available RPC requests for the currently negotiated RPC version - * @responseField supportedImageFormats | Array | Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests. + * @responseField supportedImageFormats | Array | Image formats available in `GetSourceScreenshot`, `SaveSourceScreenshot` and `GetStreamScreenshot` requests. * @responseField platform | String | Name of the platform. Usually `windows`, `macos`, or `ubuntu` (linux flavor). Not guaranteed to be any of those * @responseField platformDescription | String | Description of the platform, like `Windows 10 (10.0)` * diff --git a/src/requesthandler/RequestHandler_Stream.cpp b/src/requesthandler/RequestHandler_Stream.cpp index 35936d0d..2cca1181 100644 --- a/src/requesthandler/RequestHandler_Stream.cpp +++ b/src/requesthandler/RequestHandler_Stream.cpp @@ -17,8 +17,98 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ +#include +#include +#include +#include +#include + #include "RequestHandler.h" +QImage TakeStreamScreenshot(bool &success, uint32_t requestedWidth = 0, uint32_t requestedHeight = 0) +{ + // Get info about the program + obs_video_info ovi; + obs_get_video_info(&ovi); + const uint32_t streamWidth = ovi.base_width; + const uint32_t streamHeight = ovi.base_height; + const double streamAspectRatio = ((double)streamWidth / (double)streamHeight); + + uint32_t imgWidth = streamWidth; + uint32_t imgHeight = streamHeight; + + // Determine suitable image width + if (requestedWidth) { + imgWidth = requestedWidth; + + if (!requestedHeight) + imgHeight = ((double)imgWidth / streamAspectRatio); + } + + // Determine suitable image height + if (requestedHeight) { + imgHeight = requestedHeight; + + if (!requestedWidth) + imgWidth = ((double)imgHeight * streamAspectRatio); + } + + // Create final image texture + QImage ret(imgWidth, imgHeight, QImage::Format::Format_RGBA8888); + ret.fill(0); + + // Video image buffer + uint8_t *videoData = nullptr; + uint32_t videoLinesize = 0; + + // Enter graphics context + obs_enter_graphics(); + + gs_texrender_t *texRender = gs_texrender_create(GS_RGBA, GS_ZS_NONE); + gs_stagesurf_t *stageSurface = gs_stagesurface_create(imgWidth, imgHeight, GS_RGBA); + + success = false; + gs_texrender_reset(texRender); + if (gs_texrender_begin(texRender, imgWidth, imgHeight)) { + vec4 background; + vec4_zero(&background); + + gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); + gs_ortho(0.0f, (float)streamWidth, 0.0f, (float)streamHeight, -100.0f, 100.0f); + + gs_blend_state_push(); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + obs_render_main_texture(); + + gs_blend_state_pop(); + gs_texrender_end(texRender); + + gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender)); + if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) { + int lineSize = ret.bytesPerLine(); + for (uint y = 0; y < imgHeight; y++) { + memcpy(ret.scanLine(y), videoData + (y * videoLinesize), lineSize); + } + gs_stagesurface_unmap(stageSurface); + success = true; + } + } + + gs_stagesurface_destroy(stageSurface); + gs_texrender_destroy(texRender); + + obs_leave_graphics(); + + return ret; +} + +bool IsStreamImageFormatValid(std::string format) +{ + QByteArrayList supportedFormats = QImageWriter::supportedImageFormats(); + return supportedFormats.contains(format.c_str()); +} + /** * Gets the status of the stream output. * @@ -160,3 +250,80 @@ RequestResult RequestHandler::SendStreamCaption(const Request &request) return RequestResult::Success(); } + +/** + * Gets a Base64-encoded screenshot of the stream. + * + * The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept. + * If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the stream. + * + * @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats + * @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Stream value is used + * @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Stream value is used + * @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1 + * + * @responseField imageData | String | Base64-encoded screenshot + * + * @requestType GetOutputScreenshot + * @complexity 4 + * @rpcVersion -1 + * @initialVersion 5.4.0 + * @category stream + * @api requests + */ +RequestResult RequestHandler::GetStreamScreenshot(const Request &request) +{ + RequestStatus::RequestStatus statusCode; + std::string comment; + std::string imageFormat = request.RequestData["imageFormat"]; + + if (!IsStreamImageFormatValid(imageFormat)) + return RequestResult::Error(RequestStatus::InvalidRequestField, + "Your specified image format is invalid or not supported by this system."); + + uint32_t requestedWidth{0}; + uint32_t requestedHeight{0}; + int compressionQuality{-1}; + + if (request.Contains("imageWidth")) { + if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096)) + return RequestResult::Error(statusCode, comment); + + requestedWidth = request.RequestData["imageWidth"]; + } + + if (request.Contains("imageHeight")) { + if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096)) + return RequestResult::Error(statusCode, comment); + + requestedHeight = request.RequestData["imageHeight"]; + } + + if (request.Contains("imageCompressionQuality")) { + if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100)) + return RequestResult::Error(statusCode, comment); + + compressionQuality = request.RequestData["imageCompressionQuality"]; + } + + bool success; + QImage renderedImage = TakeStreamScreenshot(success, requestedWidth, requestedHeight); + + if (!success) + return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot."); + + QByteArray encodedImgBytes; + QBuffer buffer(&encodedImgBytes); + buffer.open(QBuffer::WriteOnly); + + if (!renderedImage.save(&buffer, imageFormat.c_str(), compressionQuality)) + return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to encode screenshot."); + + buffer.close(); + + QString encodedPicture = QString("data:image/%1;base64,").arg(imageFormat.c_str()).append(encodedImgBytes.toBase64()); + + json responseData; + responseData["imageData"] = encodedPicture.toStdString(); + return RequestResult::Success(responseData); +}