Skip to content

Commit 3fd1334

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

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
@@ -166,6 +166,7 @@ const std::unordered_map<std::string, RequestMethodHandler> RequestHandler::_han
166166
{"StartStream", &RequestHandler::StartStream},
167167
{"StopStream", &RequestHandler::StopStream},
168168
{"SendStreamCaption", &RequestHandler::SendStreamCaption},
169+
{"GetStreamScreenshot", &RequestHandler::GetStreamScreenshot},
169170

170171
// Record
171172
{"GetRecordStatus", &RequestHandler::GetRecordStatus},

src/requesthandler/RequestHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ class RequestHandler {
185185
RequestResult StartStream(const Request &);
186186
RequestResult StopStream(const Request &);
187187
RequestResult SendStreamCaption(const Request &);
188+
RequestResult GetStreamScreenshot(const Request &request);
188189

189190
// Record
190191
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
*
@@ -156,3 +246,80 @@ RequestResult RequestHandler::SendStreamCaption(const Request &request)
156246

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

0 commit comments

Comments
 (0)