feat(vaapi): add option to enable strict enforcement of frame size (#3332)
* feat(vaapi): add option to enable strict enforcement of frame size * Eliminate the QP fallback code that was only required for VAAPI
This commit is contained in:
@@ -2303,6 +2303,33 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## VA-API Encoder
|
||||||
|
|
||||||
|
### vaapi_strict_rc_buffer
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td colspan="2">
|
||||||
|
Enabling this option can avoid dropped frames over the network during scene changes, but video quality may
|
||||||
|
be reduced during motion.
|
||||||
|
@note{This option only applies for H.264 and HEVC when using VA-API [encoder](#encoder) on AMD GPUs.}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Default</td>
|
||||||
|
<td colspan="2">@code{}
|
||||||
|
disabled
|
||||||
|
@endcode</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Example</td>
|
||||||
|
<td colspan="2">@code{}
|
||||||
|
vaapi_strict_rc_buffer = enabled
|
||||||
|
@endcode</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Software Encoder
|
## Software Encoder
|
||||||
|
|
||||||
### sw_preset
|
### sw_preset
|
||||||
|
|||||||
@@ -377,6 +377,10 @@ namespace config {
|
|||||||
-1,
|
-1,
|
||||||
}, // vt
|
}, // vt
|
||||||
|
|
||||||
|
{
|
||||||
|
false, // strict_rc_buffer
|
||||||
|
}, // vaapi
|
||||||
|
|
||||||
{}, // capture
|
{}, // capture
|
||||||
{}, // encoder
|
{}, // encoder
|
||||||
{}, // adapter_name
|
{}, // adapter_name
|
||||||
@@ -1014,6 +1018,8 @@ namespace config {
|
|||||||
int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view);
|
int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view);
|
||||||
int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view);
|
int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view);
|
||||||
|
|
||||||
|
bool_f(vars, "vaapi_strict_rc_buffer", video.vaapi.strict_rc_buffer);
|
||||||
|
|
||||||
string_f(vars, "capture", video.capture);
|
string_f(vars, "capture", video.capture);
|
||||||
string_f(vars, "encoder", video.encoder);
|
string_f(vars, "encoder", video.encoder);
|
||||||
string_f(vars, "adapter_name", video.adapter_name);
|
string_f(vars, "adapter_name", video.adapter_name);
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ namespace config {
|
|||||||
int vt_coder;
|
int vt_coder;
|
||||||
} vt;
|
} vt;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool strict_rc_buffer;
|
||||||
|
} vaapi;
|
||||||
|
|
||||||
std::string capture;
|
std::string capture;
|
||||||
std::string encoder;
|
std::string encoder;
|
||||||
std::string adapter_name;
|
std::string adapter_name;
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ namespace platf {
|
|||||||
* @note Implementations may set or modify codec options prior to codec initialization.
|
* @note Implementations may set or modify codec options prior to codec initialization.
|
||||||
*/
|
*/
|
||||||
virtual void
|
virtual void
|
||||||
init_codec_options(AVCodecContext *ctx, AVDictionary *options) {};
|
init_codec_options(AVCodecContext *ctx, AVDictionary **options) {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Prepare to derive a context.
|
* @brief Prepare to derive a context.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/pixdesc.h>
|
||||||
#include <va/va.h>
|
#include <va/va.h>
|
||||||
#include <va/va_drm.h>
|
#include <va/va_drm.h>
|
||||||
#if !VA_CHECK_VERSION(1, 9, 0)
|
#if !VA_CHECK_VERSION(1, 9, 0)
|
||||||
@@ -129,13 +130,173 @@ namespace va {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Finds a supported VA entrypoint for the given VA profile.
|
||||||
|
* @param profile The profile to match.
|
||||||
|
* @return A valid encoding entrypoint or 0 on failure.
|
||||||
|
*/
|
||||||
|
VAEntrypoint
|
||||||
|
select_va_entrypoint(VAProfile profile) {
|
||||||
|
std::vector<VAEntrypoint> entrypoints(vaMaxNumEntrypoints(va_display));
|
||||||
|
int num_eps;
|
||||||
|
auto status = vaQueryConfigEntrypoints(va_display, profile, entrypoints.data(), &num_eps);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
BOOST_LOG(error) << "Failed to query VA entrypoints: "sv << vaErrorStr(status);
|
||||||
|
return (VAEntrypoint) 0;
|
||||||
|
}
|
||||||
|
entrypoints.resize(num_eps);
|
||||||
|
|
||||||
|
// Sorted in order of descending preference
|
||||||
|
VAEntrypoint ep_preferences[] = {
|
||||||
|
VAEntrypointEncSliceLP,
|
||||||
|
VAEntrypointEncSlice,
|
||||||
|
VAEntrypointEncPicture
|
||||||
|
};
|
||||||
|
for (auto ep_pref : ep_preferences) {
|
||||||
|
if (std::find(entrypoints.begin(), entrypoints.end(), ep_pref) != entrypoints.end()) {
|
||||||
|
return ep_pref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (VAEntrypoint) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Determines if a given VA profile is supported.
|
||||||
|
* @param profile The profile to match.
|
||||||
|
* @return Boolean value indicating if the profile is supported.
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
is_va_profile_supported(VAProfile profile) {
|
||||||
|
std::vector<VAProfile> profiles(vaMaxNumProfiles(va_display));
|
||||||
|
int num_profs;
|
||||||
|
auto status = vaQueryConfigProfiles(va_display, profiles.data(), &num_profs);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
BOOST_LOG(error) << "Failed to query VA profiles: "sv << vaErrorStr(status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
profiles.resize(num_profs);
|
||||||
|
|
||||||
|
return std::find(profiles.begin(), profiles.end(), profile) != profiles.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Determines the matching VA profile for the codec configuration.
|
||||||
|
* @param ctx The FFmpeg codec context.
|
||||||
|
* @return The matching VA profile or `VAProfileNone` on failure.
|
||||||
|
*/
|
||||||
|
VAProfile
|
||||||
|
get_va_profile(AVCodecContext *ctx) {
|
||||||
|
if (ctx->codec_id == AV_CODEC_ID_H264) {
|
||||||
|
// There's no VAAPI profile for H.264 4:4:4
|
||||||
|
return VAProfileH264High;
|
||||||
|
}
|
||||||
|
else if (ctx->codec_id == AV_CODEC_ID_HEVC) {
|
||||||
|
switch (ctx->profile) {
|
||||||
|
case FF_PROFILE_HEVC_REXT:
|
||||||
|
switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) {
|
||||||
|
case 10:
|
||||||
|
return VAProfileHEVCMain444_10;
|
||||||
|
case 8:
|
||||||
|
return VAProfileHEVCMain444;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FF_PROFILE_HEVC_MAIN_10:
|
||||||
|
return VAProfileHEVCMain10;
|
||||||
|
case FF_PROFILE_HEVC_MAIN:
|
||||||
|
return VAProfileHEVCMain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ctx->codec_id == AV_CODEC_ID_AV1) {
|
||||||
|
switch (ctx->profile) {
|
||||||
|
case FF_PROFILE_AV1_HIGH:
|
||||||
|
return VAProfileAV1Profile1;
|
||||||
|
case FF_PROFILE_AV1_MAIN:
|
||||||
|
return VAProfileAV1Profile0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_LOG(error) << "Unknown encoder profile: "sv << ctx->profile;
|
||||||
|
return VAProfileNone;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
init_codec_options(AVCodecContext *ctx, AVDictionary *options) override {
|
init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
|
||||||
// Don't set the RC buffer size when using H.264 on Intel GPUs. It causes
|
auto va_profile = get_va_profile(ctx);
|
||||||
// major encoding quality degradation.
|
if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) {
|
||||||
|
// Don't bother doing anything if the profile isn't supported
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto va_entrypoint = select_va_entrypoint(va_profile);
|
||||||
|
if (va_entrypoint == 0) {
|
||||||
|
// It's possible that only decoding is supported for this profile
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto vendor = vaQueryVendorString(va_display);
|
auto vendor = vaQueryVendorString(va_display);
|
||||||
if (ctx->codec_id != AV_CODEC_ID_H264 || (vendor && !strstr(vendor, "Intel"))) {
|
|
||||||
|
if (va_entrypoint == VAEntrypointEncSliceLP) {
|
||||||
|
BOOST_LOG(info) << "Using LP encoding mode"sv;
|
||||||
|
av_dict_set_int(options, "low_power", 1, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(info) << "Using normal encoding mode"sv;
|
||||||
|
}
|
||||||
|
|
||||||
|
VAConfigAttrib rc_attr = { VAConfigAttribRateControl };
|
||||||
|
auto status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &rc_attr, 1);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
// Stick to the default rate control (CQP)
|
||||||
|
rc_attr.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
VAConfigAttrib slice_attr = { VAConfigAttribEncMaxSlices };
|
||||||
|
status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &slice_attr, 1);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
// Assume only a single slice is supported
|
||||||
|
slice_attr.value = 1;
|
||||||
|
}
|
||||||
|
if (ctx->slices > slice_attr.value) {
|
||||||
|
BOOST_LOG(info) << "Limiting slice count to encoder maximum: "sv << slice_attr.value;
|
||||||
|
ctx->slices = slice_attr.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use VBR with a single frame VBV when the user forces it and for known good cases:
|
||||||
|
// - Intel GPUs
|
||||||
|
// - AV1
|
||||||
|
//
|
||||||
|
// VBR ensures the bitstream isn't full of filler data for bitrate undershoots and
|
||||||
|
// single frame VBV ensures that we don't have large bitrate overshoots (at least
|
||||||
|
// as much as they can be avoided without pre-analysis).
|
||||||
|
//
|
||||||
|
// When we have to resort to the default 1 second VBV for encoding quality reasons,
|
||||||
|
// we stick to CBR in order to avoid encoding huge frames after bitrate undershoots
|
||||||
|
// leave headroom available in the RC window.
|
||||||
|
if (config::video.vaapi.strict_rc_buffer ||
|
||||||
|
(vendor && strstr(vendor, "Intel")) ||
|
||||||
|
ctx->codec_id == AV_CODEC_ID_AV1) {
|
||||||
ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num;
|
ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num;
|
||||||
|
|
||||||
|
if (rc_attr.value & VA_RC_VBR) {
|
||||||
|
BOOST_LOG(info) << "Using VBR with single frame VBV size"sv;
|
||||||
|
av_dict_set(options, "rc_mode", "VBR", 0);
|
||||||
|
}
|
||||||
|
else if (rc_attr.value & VA_RC_CBR) {
|
||||||
|
BOOST_LOG(info) << "Using CBR with single frame VBV size"sv;
|
||||||
|
av_dict_set(options, "rc_mode", "CBR", 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(warning) << "Using CQP with single frame VBV size"sv;
|
||||||
|
av_dict_set_int(options, "qp", config::video.qp, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!(rc_attr.value & (VA_RC_CBR | VA_RC_VBR))) {
|
||||||
|
BOOST_LOG(warning) << "Using CQP rate control"sv;
|
||||||
|
av_dict_set_int(options, "qp", config::video.qp, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(info) << "Using default rate control"sv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
143
src/video.cpp
143
src/video.cpp
@@ -462,7 +462,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"av1_nvenc"s,
|
"av1_nvenc"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -472,7 +471,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"hevc_nvenc"s,
|
"hevc_nvenc"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -482,7 +480,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"h264_nvenc"s,
|
"h264_nvenc"s,
|
||||||
},
|
},
|
||||||
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT // flags
|
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT // flags
|
||||||
@@ -525,7 +522,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"av1_nvenc"s,
|
"av1_nvenc"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -552,7 +548,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"hevc_nvenc"s,
|
"hevc_nvenc"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -576,7 +571,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"h264_nvenc"s,
|
"h264_nvenc"s,
|
||||||
},
|
},
|
||||||
PARALLEL_ENCODING
|
PARALLEL_ENCODING
|
||||||
@@ -618,7 +612,6 @@ namespace video {
|
|||||||
{ "profile"s, (int) qsv::profile_av1_e::high },
|
{ "profile"s, (int) qsv::profile_av1_e::high },
|
||||||
},
|
},
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"av1_qsv"s,
|
"av1_qsv"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -652,7 +645,6 @@ namespace video {
|
|||||||
// Fallback options
|
// Fallback options
|
||||||
{ "low_power"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; } },
|
{ "low_power"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; } },
|
||||||
},
|
},
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"hevc_qsv"s,
|
"hevc_qsv"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -683,7 +675,6 @@ namespace video {
|
|||||||
// Fallback options
|
// Fallback options
|
||||||
{ "low_power"s, 0 }, // Some old/low-end Intel GPUs don't support low power encoding
|
{ "low_power"s, 0 }, // Some old/low-end Intel GPUs don't support low power encoding
|
||||||
},
|
},
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"h264_qsv"s,
|
"h264_qsv"s,
|
||||||
},
|
},
|
||||||
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
|
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
|
||||||
@@ -716,7 +707,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"av1_amf"s,
|
"av1_amf"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -741,7 +731,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"hevc_amf"s,
|
"hevc_amf"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -767,7 +756,6 @@ namespace video {
|
|||||||
// Fallback options
|
// Fallback options
|
||||||
{ "usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ }, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410
|
{ "usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ }, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410
|
||||||
},
|
},
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"h264_amf"s,
|
"h264_amf"s,
|
||||||
},
|
},
|
||||||
PARALLEL_ENCODING
|
PARALLEL_ENCODING
|
||||||
@@ -797,13 +785,10 @@ namespace video {
|
|||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
|
|
||||||
// QP rate control fallback
|
|
||||||
std::nullopt,
|
|
||||||
|
|
||||||
#ifdef ENABLE_BROKEN_AV1_ENCODER
|
#ifdef ENABLE_BROKEN_AV1_ENCODER
|
||||||
// Due to bugs preventing on-demand IDR frames from working and very poor
|
// Due to bugs preventing on-demand IDR frames from working and very poor
|
||||||
// real-time encoding performance, we do not enable libsvtav1 by default.
|
// real-time encoding performance, we do not enable libsvtav1 by default.
|
||||||
// It is only suitable for testing AV1 until the IDR frame issue is fixed.
|
// It is only suitable for testing AV1 until the IDR frame issue is fixed.
|
||||||
"libsvtav1"s,
|
"libsvtav1"s,
|
||||||
#else
|
#else
|
||||||
{},
|
{},
|
||||||
@@ -825,7 +810,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"libx265"s,
|
"libx265"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -839,7 +823,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt, // QP rate control fallback
|
|
||||||
"libx264"s,
|
"libx264"s,
|
||||||
},
|
},
|
||||||
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
|
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
|
||||||
@@ -857,7 +840,6 @@ namespace video {
|
|||||||
{
|
{
|
||||||
// Common options
|
// Common options
|
||||||
{
|
{
|
||||||
{ "low_power"s, 1 },
|
|
||||||
{ "async_depth"s, 1 },
|
{ "async_depth"s, 1 },
|
||||||
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
||||||
},
|
},
|
||||||
@@ -865,17 +847,12 @@ namespace video {
|
|||||||
{}, // HDR-specific options
|
{}, // HDR-specific options
|
||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{
|
{}, // Fallback options
|
||||||
// Fallback options
|
|
||||||
{ "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints
|
|
||||||
},
|
|
||||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
|
||||||
"av1_vaapi"s,
|
"av1_vaapi"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Common options
|
// Common options
|
||||||
{
|
{
|
||||||
{ "low_power"s, 1 },
|
|
||||||
{ "async_depth"s, 1 },
|
{ "async_depth"s, 1 },
|
||||||
{ "sei"s, 0 },
|
{ "sei"s, 0 },
|
||||||
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
||||||
@@ -884,17 +861,12 @@ namespace video {
|
|||||||
{}, // HDR-specific options
|
{}, // HDR-specific options
|
||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{
|
{}, // Fallback options
|
||||||
// Fallback options
|
|
||||||
{ "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints
|
|
||||||
},
|
|
||||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
|
||||||
"hevc_vaapi"s,
|
"hevc_vaapi"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Common options
|
// Common options
|
||||||
{
|
{
|
||||||
{ "low_power"s, 1 },
|
|
||||||
{ "async_depth"s, 1 },
|
{ "async_depth"s, 1 },
|
||||||
{ "sei"s, 0 },
|
{ "sei"s, 0 },
|
||||||
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
||||||
@@ -903,15 +875,11 @@ namespace video {
|
|||||||
{}, // HDR-specific options
|
{}, // HDR-specific options
|
||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{
|
{}, // Fallback options
|
||||||
// Fallback options
|
|
||||||
{ "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints
|
|
||||||
},
|
|
||||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
|
||||||
"h264_vaapi"s,
|
"h264_vaapi"s,
|
||||||
},
|
},
|
||||||
// RC buffer size will be set in platform code if supported
|
// RC buffer size will be set in platform code if supported
|
||||||
LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY | NO_RC_BUF_LIMIT
|
LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -938,7 +906,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt,
|
|
||||||
"av1_videotoolbox"s,
|
"av1_videotoolbox"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -955,7 +922,6 @@ namespace video {
|
|||||||
{}, // YUV444 SDR-specific options
|
{}, // YUV444 SDR-specific options
|
||||||
{}, // YUV444 HDR-specific options
|
{}, // YUV444 HDR-specific options
|
||||||
{}, // Fallback options
|
{}, // Fallback options
|
||||||
std::nullopt,
|
|
||||||
"hevc_videotoolbox"s,
|
"hevc_videotoolbox"s,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -975,7 +941,6 @@ namespace video {
|
|||||||
// Fallback options
|
// Fallback options
|
||||||
{ "flags"s, "-low_delay" },
|
{ "flags"s, "-low_delay" },
|
||||||
},
|
},
|
||||||
std::nullopt,
|
|
||||||
"h264_videotoolbox"s,
|
"h264_videotoolbox"s,
|
||||||
},
|
},
|
||||||
DEFAULT
|
DEFAULT
|
||||||
@@ -1651,52 +1616,43 @@ namespace video {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_format[encoder_t::CBR]) {
|
auto bitrate = config.bitrate * 1000;
|
||||||
auto bitrate = config.bitrate * 1000;
|
ctx->rc_max_rate = bitrate;
|
||||||
ctx->rc_max_rate = bitrate;
|
ctx->bit_rate = bitrate;
|
||||||
ctx->bit_rate = bitrate;
|
|
||||||
|
|
||||||
if (encoder.flags & CBR_WITH_VBR) {
|
if (encoder.flags & CBR_WITH_VBR) {
|
||||||
// Ensure rc_max_bitrate != bit_rate to force VBR mode
|
// Ensure rc_max_bitrate != bit_rate to force VBR mode
|
||||||
ctx->bit_rate--;
|
ctx->bit_rate--;
|
||||||
}
|
|
||||||
else {
|
|
||||||
ctx->rc_min_rate = bitrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoder.flags & RELAXED_COMPLIANCE) {
|
|
||||||
ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(encoder.flags & NO_RC_BUF_LIMIT)) {
|
|
||||||
if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) {
|
|
||||||
// Use a larger rc_buffer_size for software encoding when slices are enabled,
|
|
||||||
// because libx264 can severely degrade quality if the buffer is too small.
|
|
||||||
// libx265 encounters this issue more frequently, so always scale the
|
|
||||||
// buffer by 1.5x for software HEVC encoding.
|
|
||||||
ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ctx->rc_buffer_size = bitrate / config.framerate;
|
|
||||||
|
|
||||||
#ifndef __APPLE__
|
|
||||||
if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) {
|
|
||||||
ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (video_format.qp) {
|
|
||||||
handle_option(*video_format.qp);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv;
|
ctx->rc_min_rate = bitrate;
|
||||||
return nullptr;
|
}
|
||||||
|
|
||||||
|
if (encoder.flags & RELAXED_COMPLIANCE) {
|
||||||
|
ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(encoder.flags & NO_RC_BUF_LIMIT)) {
|
||||||
|
if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) {
|
||||||
|
// Use a larger rc_buffer_size for software encoding when slices are enabled,
|
||||||
|
// because libx264 can severely degrade quality if the buffer is too small.
|
||||||
|
// libx265 encounters this issue more frequently, so always scale the
|
||||||
|
// buffer by 1.5x for software HEVC encoding.
|
||||||
|
ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx->rc_buffer_size = bitrate / config.framerate;
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) {
|
||||||
|
ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow the encoding device a final opportunity to set/unset or override any options
|
// Allow the encoding device a final opportunity to set/unset or override any options
|
||||||
encode_device->init_codec_options(ctx.get(), options);
|
encode_device->init_codec_options(ctx.get(), &options);
|
||||||
|
|
||||||
if (auto status = avcodec_open2(ctx.get(), codec, &options)) {
|
if (auto status = avcodec_open2(ctx.get(), codec, &options)) {
|
||||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||||
@@ -2419,18 +2375,11 @@ namespace video {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
retry:
|
|
||||||
// If we're expecting failure, use the autoselect ref config first since that will always succeed
|
// If we're expecting failure, use the autoselect ref config first since that will always succeed
|
||||||
// if the encoder is available.
|
// if the encoder is available.
|
||||||
auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames);
|
auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames);
|
||||||
auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);
|
auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);
|
||||||
if (autoselect_h264 < 0) {
|
if (autoselect_h264 < 0) {
|
||||||
if (encoder.h264.qp && encoder.h264[encoder_t::CBR]) {
|
|
||||||
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
|
|
||||||
encoder.h264.capabilities.set();
|
|
||||||
encoder.h264[encoder_t::CBR] = false;
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (expect_failure) {
|
else if (expect_failure) {
|
||||||
@@ -2454,7 +2403,6 @@ namespace video {
|
|||||||
config_autoselect.videoFormat = 1;
|
config_autoselect.videoFormat = 1;
|
||||||
|
|
||||||
if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) {
|
if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) {
|
||||||
retry_hevc:
|
|
||||||
auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);
|
auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);
|
||||||
|
|
||||||
// If H.264 succeeded with max ref frames specified, assume that we can count on
|
// If H.264 succeeded with max ref frames specified, assume that we can count on
|
||||||
@@ -2463,13 +2411,6 @@ namespace video {
|
|||||||
max_ref_frames_hevc :
|
max_ref_frames_hevc :
|
||||||
validate_config(disp, encoder, config_autoselect);
|
validate_config(disp, encoder, config_autoselect);
|
||||||
|
|
||||||
if (autoselect_hevc < 0 && encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) {
|
|
||||||
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
|
|
||||||
encoder.hevc.capabilities.set();
|
|
||||||
encoder.hevc[encoder_t::CBR] = false;
|
|
||||||
goto retry_hevc;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
|
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
|
||||||
encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag);
|
encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag);
|
||||||
}
|
}
|
||||||
@@ -2492,7 +2433,6 @@ namespace video {
|
|||||||
config_autoselect.videoFormat = 2;
|
config_autoselect.videoFormat = 2;
|
||||||
|
|
||||||
if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) {
|
if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) {
|
||||||
retry_av1:
|
|
||||||
auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);
|
auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);
|
||||||
|
|
||||||
// If H.264 succeeded with max ref frames specified, assume that we can count on
|
// If H.264 succeeded with max ref frames specified, assume that we can count on
|
||||||
@@ -2501,13 +2441,6 @@ namespace video {
|
|||||||
max_ref_frames_av1 :
|
max_ref_frames_av1 :
|
||||||
validate_config(disp, encoder, config_autoselect);
|
validate_config(disp, encoder, config_autoselect);
|
||||||
|
|
||||||
if (autoselect_av1 < 0 && encoder.av1.qp && encoder.av1[encoder_t::CBR]) {
|
|
||||||
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
|
|
||||||
encoder.av1.capabilities.set();
|
|
||||||
encoder.av1[encoder_t::CBR] = false;
|
|
||||||
goto retry_av1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
|
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
|
||||||
encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag);
|
encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ namespace video {
|
|||||||
enum flag_e {
|
enum flag_e {
|
||||||
PASSED, ///< Indicates the encoder is supported.
|
PASSED, ///< Indicates the encoder is supported.
|
||||||
REF_FRAMES_RESTRICT, ///< Set maximum reference frames.
|
REF_FRAMES_RESTRICT, ///< Set maximum reference frames.
|
||||||
CBR, ///< Some encoders don't support CBR, if not supported attempt constant quantization parameter instead.
|
|
||||||
DYNAMIC_RANGE, ///< HDR support.
|
DYNAMIC_RANGE, ///< HDR support.
|
||||||
YUV444, ///< YUV 4:4:4 support.
|
YUV444, ///< YUV 4:4:4 support.
|
||||||
VUI_PARAMETERS, ///< AMD encoder with VAAPI doesn't add VUI parameters to SPS.
|
VUI_PARAMETERS, ///< AMD encoder with VAAPI doesn't add VUI parameters to SPS.
|
||||||
@@ -135,7 +134,6 @@ namespace video {
|
|||||||
switch (flag) {
|
switch (flag) {
|
||||||
_CONVERT(PASSED);
|
_CONVERT(PASSED);
|
||||||
_CONVERT(REF_FRAMES_RESTRICT);
|
_CONVERT(REF_FRAMES_RESTRICT);
|
||||||
_CONVERT(CBR);
|
|
||||||
_CONVERT(DYNAMIC_RANGE);
|
_CONVERT(DYNAMIC_RANGE);
|
||||||
_CONVERT(YUV444);
|
_CONVERT(YUV444);
|
||||||
_CONVERT(VUI_PARAMETERS);
|
_CONVERT(VUI_PARAMETERS);
|
||||||
@@ -167,11 +165,6 @@ namespace video {
|
|||||||
std::vector<option_t> hdr444_options;
|
std::vector<option_t> hdr444_options;
|
||||||
std::vector<option_t> fallback_options;
|
std::vector<option_t> fallback_options;
|
||||||
|
|
||||||
// QP option to set in the case that CBR/VBR is not supported
|
|
||||||
// by the encoder. If CBR/VBR is guaranteed to be supported,
|
|
||||||
// don't specify this option to avoid wasteful encoder probing.
|
|
||||||
std::optional<option_t> qp;
|
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
std::bitset<MAX_FLAGS> capabilities;
|
std::bitset<MAX_FLAGS> capabilities;
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,13 @@
|
|||||||
"vt_realtime": "enabled",
|
"vt_realtime": "enabled",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "vaapi",
|
||||||
|
name: "VA-API Encoder",
|
||||||
|
options: {
|
||||||
|
"vaapi_strict_rc_buffer": "disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "sw",
|
id: "sw",
|
||||||
name: "Software Encoder",
|
name: "Software Encoder",
|
||||||
@@ -283,7 +290,7 @@
|
|||||||
var app = document.getElementById("app");
|
var app = document.getElementById("app");
|
||||||
if (this.platform === "windows") {
|
if (this.platform === "windows") {
|
||||||
this.tabs = this.tabs.filter((el) => {
|
this.tabs = this.tabs.filter((el) => {
|
||||||
return el.id !== "vt";
|
return el.id !== "vt" && el.id !== "vaapi";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.platform === "linux") {
|
if (this.platform === "linux") {
|
||||||
@@ -293,7 +300,7 @@
|
|||||||
}
|
}
|
||||||
if (this.platform === "macos") {
|
if (this.platform === "macos") {
|
||||||
this.tabs = this.tabs.filter((el) => {
|
this.tabs = this.tabs.filter((el) => {
|
||||||
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv";
|
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "vaapi";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import IntelQuickSyncEncoder from './encoders/IntelQuickSyncEncoder.vue'
|
|||||||
import AmdAmfEncoder from './encoders/AmdAmfEncoder.vue'
|
import AmdAmfEncoder from './encoders/AmdAmfEncoder.vue'
|
||||||
import VideotoolboxEncoder from './encoders/VideotoolboxEncoder.vue'
|
import VideotoolboxEncoder from './encoders/VideotoolboxEncoder.vue'
|
||||||
import SoftwareEncoder from './encoders/SoftwareEncoder.vue'
|
import SoftwareEncoder from './encoders/SoftwareEncoder.vue'
|
||||||
|
import VAAPIEncoder from './encoders/VAAPIEncoder.vue'
|
||||||
|
|
||||||
const props = defineProps([
|
const props = defineProps([
|
||||||
'platform',
|
'platform',
|
||||||
@@ -45,6 +46,13 @@ const config = ref(props.config)
|
|||||||
:config="config"
|
:config="config"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- VAAPI Encoder Tab -->
|
||||||
|
<VAAPIEncoder
|
||||||
|
v-if="currentTab === 'vaapi'"
|
||||||
|
:platform="platform"
|
||||||
|
:config="config"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Software Encoder Tab -->
|
<!-- Software Encoder Tab -->
|
||||||
<SoftwareEncoder
|
<SoftwareEncoder
|
||||||
v-if="currentTab === 'sw'"
|
v-if="currentTab === 'sw'"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps([
|
||||||
|
'platform',
|
||||||
|
'config',
|
||||||
|
])
|
||||||
|
|
||||||
|
const config = ref(props.config)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="vaapi-encoder" class="config-page">
|
||||||
|
<!-- Strict RC Buffer -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="vaapi_strict_rc_buffer" class="form-label">{{ $t('config.vaapi_strict_rc_buffer') }}</label>
|
||||||
|
<select id="vaapi_strict_rc_buffer" class="form-select" v-model="config.vaapi_strict_rc_buffer">
|
||||||
|
<option value="enabled">{{ $t('_common.enabled') }}</option>
|
||||||
|
<option value="disabled">{{ $t('_common.disabled_def') }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">{{ $t('config.vaapi_strict_rc_buffer_desc') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -310,6 +310,8 @@
|
|||||||
"touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.",
|
"touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.",
|
||||||
"upnp": "UPnP",
|
"upnp": "UPnP",
|
||||||
"upnp_desc": "Automatically configure port forwarding for streaming over the Internet",
|
"upnp_desc": "Automatically configure port forwarding for streaming over the Internet",
|
||||||
|
"vaapi_strict_rc_buffer": "Strictly enforce frame bitrate limits for H.264/HEVC on AMD GPUs",
|
||||||
|
"vaapi_strict_rc_buffer_desc": "Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion.",
|
||||||
"virtual_sink": "Virtual Sink",
|
"virtual_sink": "Virtual Sink",
|
||||||
"virtual_sink_desc": "Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!",
|
"virtual_sink_desc": "Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!",
|
||||||
"virtual_sink_placeholder": "Steam Streaming Speakers",
|
"virtual_sink_placeholder": "Steam Streaming Speakers",
|
||||||
|
|||||||
Reference in New Issue
Block a user