Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Requests: GetStreamScreenshot #1189

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel GetProgramScreenshot would be a better name because Stream is used for the output layer APIs. And, the implementation should be in RequestHandler_Outputs.cpp instead of RequestHandler_Stream.cpp. I want tt2468 to comment about the name as he has overhauled a lot of API names in version 5.0.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the GetProgramScreenshot seems like a better name since "Program" is also used in the user-facing part. But I wasn't sure about the naming conventions on obs-websocket, so yes, we may need @tt2468 feedback on this :)

My first idea was GetOutputScreenshot, but that was too generic. Then I renamed it to 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);
Comment on lines +71 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function obs_get_main_texture() returns the main texture. This texture should be sufficient for this purpose.
If the requested geometry is same as the base size, just pass the texture to gs_stage_texture. If not, still need to use gs_texture_render to rescale.


gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender));
if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) {
Comment on lines +87 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for your information and for a future improvement,
gs_stagesurface_map followed by gs_stage_texture is sometimes slow.
It might be better to release the graphics context, wait, and take the context again.

int lineSize = ret.bytesPerLine();
for (uint y = 0; y < imgHeight; y++) {
memcpy(ret.scanLine(y), videoData + (y * videoLinesize), lineSize);
}
Comment on lines +89 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for your information,
between gs_stagesurface_map and gs_stagesurface_unmap, we may release the graphics context so that the graphics thread won't be blocked while copying the pixel data.

gs_stagesurface_unmap(stageSurface);
success = true;
}
}

gs_stagesurface_destroy(stageSurface);
gs_texrender_destroy(texRender);

obs_leave_graphics();

return ret;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The majority of this function TakeStreamScreenshot looks same as TakeSourceScreenshot.
Can we share the code? For example, creating QImage and copying the pixel data to QImage can be a common function.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is GetOutputScreenshot a typo? (This name sounds also good though.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a typo

* @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);
}