eb7bb07a79
Use JXR for HDR video on Windows. Other operating systems will tonemap HDR to SDR, and save to PNG. Continue to take PNG screenshots for SDR. We will probably support EXR for Mac/Linux someday.
364 lines
8.9 KiB
C++
364 lines
8.9 KiB
C++
/******************************************************************************
|
|
Copyright (C) 2020 by Hugh Bailey <obs.jim@gmail.com>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include "window-basic-main.hpp"
|
|
#include "screenshot-obj.hpp"
|
|
#include "qt-wrappers.hpp"
|
|
|
|
#ifdef _WIN32
|
|
#include <wincodec.h>
|
|
#include <wincodecsdk.h>
|
|
#include <wrl/client.h>
|
|
#pragma comment(lib, "windowscodecs.lib")
|
|
#endif
|
|
|
|
static void ScreenshotTick(void *param, float);
|
|
|
|
/* ========================================================================= */
|
|
|
|
ScreenshotObj::ScreenshotObj(obs_source_t *source)
|
|
: weakSource(OBSGetWeakRef(source))
|
|
{
|
|
obs_add_tick_callback(ScreenshotTick, this);
|
|
}
|
|
|
|
ScreenshotObj::~ScreenshotObj()
|
|
{
|
|
obs_enter_graphics();
|
|
gs_stagesurface_destroy(stagesurf);
|
|
gs_texrender_destroy(texrender);
|
|
obs_leave_graphics();
|
|
|
|
obs_remove_tick_callback(ScreenshotTick, this);
|
|
|
|
if (th.joinable()) {
|
|
th.join();
|
|
|
|
if (cx && cy) {
|
|
OBSBasic *main = OBSBasic::Get();
|
|
main->ShowStatusBarMessage(
|
|
QTStr("Basic.StatusBar.ScreenshotSavedTo")
|
|
.arg(QT_UTF8(path.c_str())));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenshotObj::Screenshot()
|
|
{
|
|
OBSSource source = OBSGetStrongRef(weakSource);
|
|
|
|
if (source) {
|
|
cx = obs_source_get_base_width(source);
|
|
cy = obs_source_get_base_height(source);
|
|
} else {
|
|
obs_video_info ovi;
|
|
obs_get_video_info(&ovi);
|
|
cx = ovi.base_width;
|
|
cy = ovi.base_height;
|
|
}
|
|
|
|
if (!cx || !cy) {
|
|
blog(LOG_WARNING, "Cannot screenshot, invalid target size");
|
|
obs_remove_tick_callback(ScreenshotTick, this);
|
|
deleteLater();
|
|
return;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
enum gs_color_space space =
|
|
obs_source_get_color_space(source, 0, nullptr);
|
|
if (space == GS_CS_709_EXTENDED) {
|
|
/* Convert for JXR */
|
|
space = GS_CS_709_SCRGB;
|
|
}
|
|
#else
|
|
/* Tonemap to SDR if HDR */
|
|
const enum gs_color_space space = GS_CS_SRGB;
|
|
#endif
|
|
const enum gs_color_format format = gs_get_format_from_space(space);
|
|
|
|
texrender = gs_texrender_create(format, GS_ZS_NONE);
|
|
stagesurf = gs_stagesurface_create(cx, cy, format);
|
|
|
|
if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) {
|
|
vec4 zero;
|
|
vec4_zero(&zero);
|
|
|
|
gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0);
|
|
gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f);
|
|
|
|
gs_blend_state_push();
|
|
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
|
|
|
|
if (source) {
|
|
obs_source_inc_showing(source);
|
|
obs_source_video_render(source);
|
|
obs_source_dec_showing(source);
|
|
} else {
|
|
obs_render_main_texture();
|
|
}
|
|
|
|
gs_blend_state_pop();
|
|
gs_texrender_end(texrender);
|
|
}
|
|
}
|
|
|
|
void ScreenshotObj::Download()
|
|
{
|
|
gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender));
|
|
}
|
|
|
|
void ScreenshotObj::Copy()
|
|
{
|
|
uint8_t *videoData = nullptr;
|
|
uint32_t videoLinesize = 0;
|
|
|
|
if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) {
|
|
if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) {
|
|
const uint32_t linesize = cx * 8;
|
|
half_bytes.reserve(cx * cy * 8);
|
|
|
|
for (uint32_t y = 0; y < cy; y++) {
|
|
const uint8_t *const line =
|
|
videoData + (y * videoLinesize);
|
|
half_bytes.insert(half_bytes.end(), line,
|
|
line + linesize);
|
|
}
|
|
} else {
|
|
image = QImage(cx, cy, QImage::Format::Format_RGBX8888);
|
|
|
|
int linesize = image.bytesPerLine();
|
|
for (int y = 0; y < (int)cy; y++)
|
|
memcpy(image.scanLine(y),
|
|
videoData + (y * videoLinesize),
|
|
linesize);
|
|
}
|
|
|
|
gs_stagesurface_unmap(stagesurf);
|
|
}
|
|
}
|
|
|
|
void ScreenshotObj::Save()
|
|
{
|
|
OBSBasic *main = OBSBasic::Get();
|
|
config_t *config = main->Config();
|
|
|
|
const char *mode = config_get_string(config, "Output", "Mode");
|
|
const char *type = config_get_string(config, "AdvOut", "RecType");
|
|
const char *adv_path =
|
|
strcmp(type, "Standard")
|
|
? config_get_string(config, "AdvOut", "FFFilePath")
|
|
: config_get_string(config, "AdvOut", "RecFilePath");
|
|
const char *rec_path =
|
|
strcmp(mode, "Advanced")
|
|
? config_get_string(config, "SimpleOutput", "FilePath")
|
|
: adv_path;
|
|
|
|
bool noSpace =
|
|
config_get_bool(config, "SimpleOutput", "FileNameWithoutSpace");
|
|
const char *filenameFormat =
|
|
config_get_string(config, "Output", "FilenameFormatting");
|
|
bool overwriteIfExists =
|
|
config_get_bool(config, "Output", "OverwriteIfExists");
|
|
|
|
const char *ext = half_bytes.empty() ? "png" : "jxr";
|
|
path = GetOutputFilename(
|
|
rec_path, ext, noSpace, overwriteIfExists,
|
|
GetFormatString(filenameFormat, "Screenshot", nullptr).c_str());
|
|
|
|
th = std::thread([this] { MuxAndFinish(); });
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx,
|
|
uint32_t cy, IWICBitmapFrameEncode *frameEncode,
|
|
IPropertyBag2 *options)
|
|
{
|
|
wchar_t lossless[] = L"Lossless";
|
|
PROPBAG2 bag = {};
|
|
bag.pstrName = lossless;
|
|
VARIANT value = {};
|
|
value.vt = VT_BOOL;
|
|
value.bVal = TRUE;
|
|
HRESULT hr = options->Write(1, &bag, &value);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = frameEncode->Initialize(options);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = frameEncode->SetSize(cx, cy);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = frameEncode->SetResolution(72, 72);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf;
|
|
hr = frameEncode->SetPixelFormat(&pixelFormat);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf,
|
|
sizeof(WICPixelFormatGUID)) != 0)
|
|
return E_FAIL;
|
|
|
|
hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = frameEncode->Commit();
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy)
|
|
{
|
|
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
|
|
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL,
|
|
CLSCTX_INPROC_SERVER,
|
|
IID_PPV_ARGS(factory.GetAddressOf()));
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
Microsoft::WRL::ComPtr<IWICStream> stream;
|
|
hr = factory->CreateStream(stream.GetAddressOf());
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = stream->InitializeFromFilename(path, GENERIC_WRITE);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmapEncoder> encoder = NULL;
|
|
hr = factory->CreateEncoder(GUID_ContainerFormatWmp, NULL,
|
|
encoder.GetAddressOf());
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmapFrameEncode> frameEncode;
|
|
Microsoft::WRL::ComPtr<IPropertyBag2> options;
|
|
hr = encoder->CreateNewFrame(frameEncode.GetAddressOf(),
|
|
options.GetAddressOf());
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
hr = SaveJxrImage(path, pixels, cx, cy, frameEncode.Get(),
|
|
options.Get());
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
encoder->Commit();
|
|
return S_OK;
|
|
}
|
|
#endif // #ifdef _WIN32
|
|
|
|
void ScreenshotObj::MuxAndFinish()
|
|
{
|
|
if (half_bytes.empty()) {
|
|
image.save(QT_UTF8(path.c_str()));
|
|
blog(LOG_INFO, "Saved screenshot to '%s'", path.c_str());
|
|
} else {
|
|
#ifdef _WIN32
|
|
wchar_t *path_w = nullptr;
|
|
os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w);
|
|
if (path_w) {
|
|
SaveJxr(path_w, half_bytes.data(), cx, cy);
|
|
bfree(path_w);
|
|
}
|
|
#endif // #ifdef _WIN32
|
|
}
|
|
|
|
deleteLater();
|
|
}
|
|
|
|
/* ========================================================================= */
|
|
|
|
#define STAGE_SCREENSHOT 0
|
|
#define STAGE_DOWNLOAD 1
|
|
#define STAGE_COPY_AND_SAVE 2
|
|
#define STAGE_FINISH 3
|
|
|
|
static void ScreenshotTick(void *param, float)
|
|
{
|
|
ScreenshotObj *data = reinterpret_cast<ScreenshotObj *>(param);
|
|
|
|
if (data->stage == STAGE_FINISH) {
|
|
return;
|
|
}
|
|
|
|
obs_enter_graphics();
|
|
|
|
switch (data->stage) {
|
|
case STAGE_SCREENSHOT:
|
|
data->Screenshot();
|
|
break;
|
|
case STAGE_DOWNLOAD:
|
|
data->Download();
|
|
break;
|
|
case STAGE_COPY_AND_SAVE:
|
|
data->Copy();
|
|
QMetaObject::invokeMethod(data, "Save");
|
|
obs_remove_tick_callback(ScreenshotTick, data);
|
|
break;
|
|
}
|
|
|
|
obs_leave_graphics();
|
|
|
|
data->stage++;
|
|
}
|
|
|
|
void OBSBasic::Screenshot(OBSSource source)
|
|
{
|
|
if (!!screenshotData) {
|
|
blog(LOG_WARNING, "Cannot take new screenshot, "
|
|
"screenshot currently in progress");
|
|
return;
|
|
}
|
|
|
|
screenshotData = new ScreenshotObj(source);
|
|
}
|
|
|
|
void OBSBasic::ScreenshotSelectedSource()
|
|
{
|
|
OBSSceneItem item = GetCurrentSceneItem();
|
|
if (item) {
|
|
Screenshot(obs_sceneitem_get_source(item));
|
|
} else {
|
|
blog(LOG_INFO, "Could not take a source screenshot: "
|
|
"no source selected");
|
|
}
|
|
}
|
|
|
|
void OBSBasic::ScreenshotProgram()
|
|
{
|
|
Screenshot(GetProgramSource());
|
|
}
|
|
|
|
void OBSBasic::ScreenshotScene()
|
|
{
|
|
Screenshot(GetCurrentSceneSource());
|
|
}
|