Skip to content

Commit 3faf2de

Browse files
committed
Requests: GetStreamScreenshot
Adds a new request called `GetStreamScreenshot` which returns a Base64-encoded screenshot of the stream (program).
1 parent 0548c77 commit 3faf2de

File tree

4 files changed

+170
-1
lines changed

4 files changed

+170
-1
lines changed

src/requesthandler/RequestHandler.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ const std::unordered_map<std::string, RequestMethodHandler> RequestHandler::_han
168168
{"StartStream", &RequestHandler::StartStream},
169169
{"StopStream", &RequestHandler::StopStream},
170170
{"SendStreamCaption", &RequestHandler::SendStreamCaption},
171+
{"GetStreamScreenshot", &RequestHandler::GetStreamScreenshot},
171172

172173
// Record
173174
{"GetRecordStatus", &RequestHandler::GetRecordStatus},

src/requesthandler/RequestHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ class RequestHandler {
187187
RequestResult StartStream(const Request &);
188188
RequestResult StopStream(const Request &);
189189
RequestResult SendStreamCaption(const Request &);
190+
RequestResult GetStreamScreenshot(const Request &request);
190191

191192
// Record
192193
RequestResult GetRecordStatus(const Request &);

src/requesthandler/RequestHandler_General.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
3333
* @responseField obsWebSocketVersion | String | Current obs-websocket version
3434
* @responseField rpcVersion | Number | Current latest obs-websocket RPC version
3535
* @responseField availableRequests | Array<String> | Array of available RPC requests for the currently negotiated RPC version
36-
* @responseField supportedImageFormats | Array<String> | Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests.
36+
* @responseField supportedImageFormats | Array<String> | Image formats available in `GetSourceScreenshot`, `SaveSourceScreenshot` and `GetStreamScreenshot` requests.
3737
* @responseField platform | String | Name of the platform. Usually `windows`, `macos`, or `ubuntu` (linux flavor). Not guaranteed to be any of those
3838
* @responseField platformDescription | String | Description of the platform, like `Windows 10 (10.0)`
3939
*

src/requesthandler/RequestHandler_Stream.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,98 @@ You should have received a copy of the GNU General Public License along
1717
with this program. If not, see <https://www.gnu.org/licenses/>
1818
*/
1919

20+
#include <QBuffer>
21+
#include <QImageWriter>
22+
#include <QFileInfo>
23+
#include <QImage>
24+
#include <QDir>
25+
2026
#include "RequestHandler.h"
2127

28+
QImage TakeStreamScreenshot(bool &success, uint32_t requestedWidth = 0, uint32_t requestedHeight = 0)
29+
{
30+
// Get info about the program
31+
obs_video_info ovi;
32+
obs_get_video_info(&ovi);
33+
const uint32_t streamWidth = ovi.base_width;
34+
const uint32_t streamHeight = ovi.base_height;
35+
const double streamAspectRatio = ((double)streamWidth / (double)streamHeight);
36+
37+
uint32_t imgWidth = streamWidth;
38+
uint32_t imgHeight = streamHeight;
39+
40+
// Determine suitable image width
41+
if (requestedWidth) {
42+
imgWidth = requestedWidth;
43+
44+
if (!requestedHeight)
45+
imgHeight = ((double)imgWidth / streamAspectRatio);
46+
}
47+
48+
// Determine suitable image height
49+
if (requestedHeight) {
50+
imgHeight = requestedHeight;
51+
52+
if (!requestedWidth)
53+
imgWidth = ((double)imgHeight * streamAspectRatio);
54+
}
55+
56+
// Create final image texture
57+
QImage ret(imgWidth, imgHeight, QImage::Format::Format_RGBA8888);
58+
ret.fill(0);
59+
60+
// Video image buffer
61+
uint8_t *videoData = nullptr;
62+
uint32_t videoLinesize = 0;
63+
64+
// Enter graphics context
65+
obs_enter_graphics();
66+
67+
gs_texrender_t *texRender = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
68+
gs_stagesurf_t *stageSurface = gs_stagesurface_create(imgWidth, imgHeight, GS_RGBA);
69+
70+
success = false;
71+
gs_texrender_reset(texRender);
72+
if (gs_texrender_begin(texRender, imgWidth, imgHeight)) {
73+
vec4 background;
74+
vec4_zero(&background);
75+
76+
gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0);
77+
gs_ortho(0.0f, (float)streamWidth, 0.0f, (float)streamHeight, -100.0f, 100.0f);
78+
79+
gs_blend_state_push();
80+
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
81+
82+
obs_render_main_texture();
83+
84+
gs_blend_state_pop();
85+
gs_texrender_end(texRender);
86+
87+
gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender));
88+
if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) {
89+
int lineSize = ret.bytesPerLine();
90+
for (uint y = 0; y < imgHeight; y++) {
91+
memcpy(ret.scanLine(y), videoData + (y * videoLinesize), lineSize);
92+
}
93+
gs_stagesurface_unmap(stageSurface);
94+
success = true;
95+
}
96+
}
97+
98+
gs_stagesurface_destroy(stageSurface);
99+
gs_texrender_destroy(texRender);
100+
101+
obs_leave_graphics();
102+
103+
return ret;
104+
}
105+
106+
bool IsStreamImageFormatValid(std::string format)
107+
{
108+
QByteArrayList supportedFormats = QImageWriter::supportedImageFormats();
109+
return supportedFormats.contains(format.c_str());
110+
}
111+
22112
/**
23113
* Gets the status of the stream output.
24114
*
@@ -160,3 +250,80 @@ RequestResult RequestHandler::SendStreamCaption(const Request &request)
160250

161251
return RequestResult::Success();
162252
}
253+
254+
/**
255+
* Gets a Base64-encoded screenshot of the stream.
256+
*
257+
* 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.
258+
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the stream.
259+
*
260+
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
261+
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Stream value is used
262+
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Stream value is used
263+
* @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
264+
*
265+
* @responseField imageData | String | Base64-encoded screenshot
266+
*
267+
* @requestType GetOutputScreenshot
268+
* @complexity 4
269+
* @rpcVersion -1
270+
* @initialVersion 5.4.0
271+
* @category stream
272+
* @api requests
273+
*/
274+
RequestResult RequestHandler::GetStreamScreenshot(const Request &request)
275+
{
276+
RequestStatus::RequestStatus statusCode;
277+
std::string comment;
278+
std::string imageFormat = request.RequestData["imageFormat"];
279+
280+
if (!IsStreamImageFormatValid(imageFormat))
281+
return RequestResult::Error(RequestStatus::InvalidRequestField,
282+
"Your specified image format is invalid or not supported by this system.");
283+
284+
uint32_t requestedWidth{0};
285+
uint32_t requestedHeight{0};
286+
int compressionQuality{-1};
287+
288+
if (request.Contains("imageWidth")) {
289+
if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096))
290+
return RequestResult::Error(statusCode, comment);
291+
292+
requestedWidth = request.RequestData["imageWidth"];
293+
}
294+
295+
if (request.Contains("imageHeight")) {
296+
if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096))
297+
return RequestResult::Error(statusCode, comment);
298+
299+
requestedHeight = request.RequestData["imageHeight"];
300+
}
301+
302+
if (request.Contains("imageCompressionQuality")) {
303+
if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100))
304+
return RequestResult::Error(statusCode, comment);
305+
306+
compressionQuality = request.RequestData["imageCompressionQuality"];
307+
}
308+
309+
bool success;
310+
QImage renderedImage = TakeStreamScreenshot(success, requestedWidth, requestedHeight);
311+
312+
if (!success)
313+
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot.");
314+
315+
QByteArray encodedImgBytes;
316+
QBuffer buffer(&encodedImgBytes);
317+
buffer.open(QBuffer::WriteOnly);
318+
319+
if (!renderedImage.save(&buffer, imageFormat.c_str(), compressionQuality))
320+
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to encode screenshot.");
321+
322+
buffer.close();
323+
324+
QString encodedPicture = QString("data:image/%1;base64,").arg(imageFormat.c_str()).append(encodedImgBytes.toBase64());
325+
326+
json responseData;
327+
responseData["imageData"] = encodedPicture.toStdString();
328+
return RequestResult::Success(responseData);
329+
}

0 commit comments

Comments
 (0)