Skip to content

Commit

Permalink
Requests: GetStreamScreenshot
Browse files Browse the repository at this point in the history
Adds a new request called `GetStreamScreenshot` which returns a
Base64-encoded screenshot of the stream (program).
  • Loading branch information
ghostzero committed Jul 25, 2024
1 parent 0548c77 commit 3faf2de
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/requesthandler/RequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const std::unordered_map<std::string, RequestMethodHandler> RequestHandler::_han
{"StartStream", &RequestHandler::StartStream},
{"StopStream", &RequestHandler::StopStream},
{"SendStreamCaption", &RequestHandler::SendStreamCaption},
{"GetStreamScreenshot", &RequestHandler::GetStreamScreenshot},

// Record
{"GetRecordStatus", &RequestHandler::GetRecordStatus},
Expand Down
1 change: 1 addition & 0 deletions src/requesthandler/RequestHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 &);
Expand Down
2 changes: 1 addition & 1 deletion src/requesthandler/RequestHandler_General.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
* @responseField obsWebSocketVersion | String | Current obs-websocket version
* @responseField rpcVersion | Number | Current latest obs-websocket RPC version
* @responseField availableRequests | Array<String> | Array of available RPC requests for the currently negotiated RPC version
* @responseField supportedImageFormats | Array<String> | Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests.
* @responseField supportedImageFormats | Array<String> | 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)`
*
Expand Down
167 changes: 167 additions & 0 deletions src/requesthandler/RequestHandler_Stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,98 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/

#include <QBuffer>
#include <QImageWriter>
#include <QFileInfo>
#include <QImage>
#include <QDir>

#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.
*
Expand Down Expand Up @@ -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);
}

0 comments on commit 3faf2de

Please sign in to comment.