-
-
Notifications
You must be signed in to change notification settings - Fork 707
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
|
||
gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender)); | ||
if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) { | ||
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for your information and for a future improvement, |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for your information, |
||
gs_stagesurface_unmap(stageSurface); | ||
success = true; | ||
} | ||
} | ||
|
||
gs_stagesurface_destroy(stageSurface); | ||
gs_texrender_destroy(texRender); | ||
|
||
obs_leave_graphics(); | ||
|
||
return ret; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The majority of this function |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
There was a problem hiding this comment.
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 becauseStream
is used for the output layer APIs. And, the implementation should be inRequestHandler_Outputs.cpp
instead ofRequestHandler_Stream.cpp
. I want tt2468 to comment about the name as he has overhauled a lot of API names in version 5.0.There was a problem hiding this comment.
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 toGetStreamScreenshot
.