mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-08-24 10:14:13 -05:00
obs-outputs: Implement multitrack dynamic bitrate
This commit is contained in:
@@ -4,6 +4,7 @@ set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
|
||||
find_package(MbedTLS REQUIRED)
|
||||
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG FALSE)
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_package(jansson REQUIRED)
|
||||
|
||||
if(NOT TARGET happy-eyeballs)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/happy-eyeballs" "${CMAKE_BINARY_DIR}/shared/happy-eyeballs")
|
||||
@@ -80,6 +81,7 @@ target_link_libraries(
|
||||
OBS::bpm
|
||||
MbedTLS::mbedtls
|
||||
ZLIB::ZLIB
|
||||
jansson::jansson
|
||||
$<$<PLATFORM_ID:Windows>:OBS::w32-pthreads>
|
||||
$<$<PLATFORM_ID:Windows>:crypt32>
|
||||
$<$<PLATFORM_ID:Windows>:iphlpapi>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <obs-avc.h>
|
||||
#include <obs-hevc.h>
|
||||
|
||||
#include <jansson.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <util/windows/win-version.h>
|
||||
#endif
|
||||
@@ -135,6 +137,7 @@ static void rtmp_stream_destroy(void *data)
|
||||
deque_free(&stream->droptest_info);
|
||||
#endif
|
||||
deque_free(&stream->dbr_frames);
|
||||
da_free(stream->dbr_interpolation_table);
|
||||
pthread_mutex_destroy(&stream->dbr_mutex);
|
||||
|
||||
os_event_destroy(stream->buffer_space_available_event);
|
||||
@@ -757,7 +760,7 @@ static bool send_meta_data(struct rtmp_stream *stream)
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool send_audio_header(struct rtmp_stream *stream, size_t idx, bool *next)
|
||||
static bool send_audio_header(struct rtmp_stream *stream, size_t idx)
|
||||
{
|
||||
obs_output_t *context = stream->output;
|
||||
obs_encoder_t *aencoder = obs_output_get_audio_encoder(context, idx);
|
||||
@@ -766,7 +769,6 @@ static bool send_audio_header(struct rtmp_stream *stream, size_t idx, bool *next
|
||||
struct encoder_packet packet = {.type = OBS_ENCODER_AUDIO, .timebase_den = 1};
|
||||
|
||||
if (!aencoder) {
|
||||
*next = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -928,23 +930,22 @@ static bool send_video_footer(struct rtmp_stream *stream, size_t idx)
|
||||
static inline bool send_headers(struct rtmp_stream *stream)
|
||||
{
|
||||
stream->sent_headers = true;
|
||||
size_t i = 0;
|
||||
bool next = true;
|
||||
|
||||
if (!send_audio_header(stream, i++, &next))
|
||||
return false;
|
||||
|
||||
for (size_t j = 0; j < MAX_OUTPUT_VIDEO_ENCODERS; j++) {
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, j);
|
||||
for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) {
|
||||
obs_encoder_t *enc = obs_output_get_audio_encoder(stream->output, i);
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
if (!send_video_metadata(stream, j) || !send_video_header(stream, j))
|
||||
if (!send_audio_header(stream, i))
|
||||
return false;
|
||||
}
|
||||
|
||||
while (next) {
|
||||
if (!send_audio_header(stream, i++, &next))
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, i);
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
if (!send_video_metadata(stream, i) || !send_video_header(stream, i))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1228,6 +1229,100 @@ static int try_connect(struct rtmp_stream *stream)
|
||||
return init_send(stream);
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds the DBR interpolation table from a JSON string.
|
||||
*
|
||||
* The input JSON is structured as [encoder_index][quality_level], e.g. for a 5-encoder config
|
||||
* (2560x1440 HEVC, 1920x1080, 1280x720, 852x480, 640x360):
|
||||
* [
|
||||
* [0, 5940, 7200, 9000], // encoder 0 (2560x1440)
|
||||
* [0, 4950, 6000, 7500], // encoder 1 (1920x1080)
|
||||
* [0, 2800, 3500, 3500], // encoder 2 (1280x720)
|
||||
* [0, 1000, 1000, 1000], // encoder 3 (852x480)
|
||||
* [0, 500, 500, 500] // encoder 4 (640x360)
|
||||
* ]
|
||||
*
|
||||
* This is transposed into the interpolation table where each entry represents a quality level
|
||||
* containing per-encoder bitrates:
|
||||
* table[0].bitrates[] = {0, 0, 0, 0, 0} // level 0 (minimum)
|
||||
* table[1].bitrates[] = {5940, 4950, 2800, 1000, 500} // level 1
|
||||
* table[2].bitrates[] = {7200, 6000, 3500, 1000, 500} // level 2
|
||||
* table[3].bitrates[] = {9000, 7500, 3500, 1000, 500} // level 3 (maximum)
|
||||
*
|
||||
* The encoder index must match the output encoder index used with obs_output_get_video_encoder2().
|
||||
* All encoder arrays must be the same size.
|
||||
*
|
||||
* dbr_set_bitrate() walks this table to find the appropriate quality level based on the current
|
||||
* aggregate bitrate and applies per-encoder bitrates via linear interpolation between adjacent levels.
|
||||
*/
|
||||
static bool build_dbr_interpolation_table(struct rtmp_stream *stream, const char *interpolation_data)
|
||||
{
|
||||
json_error_t error;
|
||||
json_t *root = json_loads(interpolation_data, JSON_REJECT_DUPLICATES, &error);
|
||||
if (!root) {
|
||||
warn("loading dbr interpolation table failed: %s", error.text);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t all_array_size = 0;
|
||||
bool malformed_data = false;
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; ++i) {
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, i);
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
const char *name = obs_encoder_get_name(enc);
|
||||
const json_t *array = json_array_get(root, i);
|
||||
if (!array) {
|
||||
warn("missing bitrate interpolation data for encoder '%s'", name);
|
||||
malformed_data = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t size = json_array_size(array);
|
||||
if (!all_array_size) {
|
||||
all_array_size = size;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (all_array_size != size) {
|
||||
warn("size %zu of bitrate interpolation data for encoder '%s' does not match overall size %zu",
|
||||
size, name, all_array_size);
|
||||
malformed_data = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (malformed_data)
|
||||
goto error;
|
||||
|
||||
da_clear(stream->dbr_interpolation_table);
|
||||
da_reserve(stream->dbr_interpolation_table, all_array_size);
|
||||
for (size_t i = 0; i < all_array_size; ++i) {
|
||||
struct dbr_interpolation_point *dbr_point = da_push_back_new(stream->dbr_interpolation_table);
|
||||
for (size_t j = 0; j < MAX_OUTPUT_VIDEO_ENCODERS; ++j) {
|
||||
const json_t *array = json_array_get(root, j);
|
||||
if (!array)
|
||||
continue;
|
||||
|
||||
const json_t *entry = json_array_get(array, i);
|
||||
if (!json_is_integer(entry)) {
|
||||
warn("interpolation entry %zu for track %zu is not an integer (type: %d)", i, j,
|
||||
json_typeof(entry));
|
||||
malformed_data = true;
|
||||
continue;
|
||||
}
|
||||
dbr_point->bitrates[j] = (long)json_integer_value(entry);
|
||||
}
|
||||
}
|
||||
|
||||
json_decref(root);
|
||||
return true;
|
||||
|
||||
error:
|
||||
json_decref(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool init_connect(struct rtmp_stream *stream)
|
||||
{
|
||||
obs_service_t *service;
|
||||
@@ -1236,7 +1331,6 @@ static bool init_connect(struct rtmp_stream *stream)
|
||||
const char *ip_family;
|
||||
int64_t drop_p;
|
||||
int64_t drop_b;
|
||||
uint32_t caps;
|
||||
|
||||
if (stopping(stream)) {
|
||||
pthread_join(stream->send_thread, NULL);
|
||||
@@ -1266,55 +1360,81 @@ static bool init_connect(struct rtmp_stream *stream)
|
||||
drop_p = (int64_t)obs_data_get_int(settings, OPT_PFRAME_DROP_THRESHOLD);
|
||||
stream->max_shutdown_time_sec = (int)obs_data_get_int(settings, OPT_MAX_SHUTDOWN_TIME_SEC);
|
||||
|
||||
obs_encoder_t *venc = obs_output_get_video_encoder(stream->output);
|
||||
obs_encoder_t *aenc = obs_output_get_audio_encoder(stream->output, 0);
|
||||
obs_data_t *vsettings = obs_encoder_get_settings(venc);
|
||||
obs_data_t *asettings = obs_encoder_get_settings(aenc);
|
||||
long long overall_audio_bitrate = 0;
|
||||
long long overall_video_bitrate = 0;
|
||||
bool dbr_capable = true;
|
||||
for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) {
|
||||
obs_encoder_t *enc = obs_output_get_audio_encoder(stream->output, i);
|
||||
if (enc) {
|
||||
const char *codec = obs_encoder_get_codec(enc);
|
||||
stream->audio_codec[i] = to_audio_type(codec);
|
||||
}
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
const char *codec = obs_encoder_get_codec(enc);
|
||||
stream->audio_codec[i] = to_audio_type(codec);
|
||||
|
||||
obs_data_t *settings = obs_encoder_get_settings(enc);
|
||||
overall_audio_bitrate += obs_data_get_int(settings, "bitrate");
|
||||
obs_data_release(settings);
|
||||
}
|
||||
|
||||
da_reserve(stream->dbr_interpolation_table, 2);
|
||||
da_push_back_new(stream->dbr_interpolation_table);
|
||||
struct dbr_interpolation_point *dbr_point = da_push_back_new(stream->dbr_interpolation_table);
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, i);
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
if (enc) {
|
||||
const char *codec = obs_encoder_get_codec(enc);
|
||||
stream->video_codec[i] = to_video_type(codec);
|
||||
const char *codec = obs_encoder_get_codec(enc);
|
||||
stream->video_codec[i] = to_video_type(codec);
|
||||
|
||||
if ((obs_encoder_get_caps(enc) & OBS_ENCODER_CAP_DYN_BITRATE) == 0) {
|
||||
dbr_capable = false;
|
||||
info("Dynamic bitrate disabled. "
|
||||
"The encoder '%s' does not support on-the-fly "
|
||||
"bitrate reconfiguration.",
|
||||
obs_encoder_get_name(enc));
|
||||
continue;
|
||||
}
|
||||
|
||||
obs_data_t *settings = obs_encoder_get_settings(enc);
|
||||
long long bitrate = obs_data_get_int(settings, "bitrate");
|
||||
overall_video_bitrate += bitrate;
|
||||
dbr_point->bitrates[i] = (long)bitrate;
|
||||
obs_data_release(settings);
|
||||
}
|
||||
|
||||
deque_free(&stream->dbr_frames);
|
||||
stream->audio_bitrate = (long)obs_data_get_int(asettings, "bitrate");
|
||||
stream->audio_bitrate = (long)overall_audio_bitrate;
|
||||
stream->dbr_data_size = 0;
|
||||
stream->dbr_orig_bitrate = (long)obs_data_get_int(vsettings, "bitrate");
|
||||
stream->dbr_orig_bitrate = (long)overall_video_bitrate;
|
||||
stream->dbr_cur_bitrate = stream->dbr_orig_bitrate;
|
||||
stream->dbr_est_bitrate = 0;
|
||||
stream->dbr_inc_bitrate = stream->dbr_orig_bitrate / 10;
|
||||
stream->dbr_inc_timeout = 0;
|
||||
stream->dbr_enabled = obs_data_get_bool(settings, OPT_DYN_BITRATE);
|
||||
|
||||
caps = obs_encoder_get_caps(venc);
|
||||
if ((caps & OBS_ENCODER_CAP_DYN_BITRATE) == 0) {
|
||||
stream->dbr_enabled = false;
|
||||
info("Dynamic bitrate disabled. "
|
||||
"The encoder does not support on-the-fly bitrate reconfiguration.");
|
||||
}
|
||||
stream->dbr_enabled = dbr_capable && obs_data_get_bool(settings, OPT_DYN_BITRATE);
|
||||
|
||||
if (obs_output_get_delay(stream->output) != 0) {
|
||||
info("Dynamic bitrate disabled. Stream delay and dynamic bitrate are incompatible.");
|
||||
stream->dbr_enabled = false;
|
||||
}
|
||||
|
||||
if (stream->dbr_enabled && !obs_data_has_user_value(settings, OPT_DYN_BITRATE_INTERPOLATION_TABLE_DATA)) {
|
||||
info("Dynamic bitrate using default interpolation");
|
||||
} else if (obs_data_has_user_value(settings, OPT_DYN_BITRATE_INTERPOLATION_TABLE_DATA)) {
|
||||
const char *dbr_interpolation_data =
|
||||
obs_data_get_string(settings, OPT_DYN_BITRATE_INTERPOLATION_TABLE_DATA);
|
||||
if (!build_dbr_interpolation_table(stream, dbr_interpolation_data)) {
|
||||
warn("Loading interpolation settings failed, disabling dynamic bitrate");
|
||||
stream->dbr_enabled = false;
|
||||
} else {
|
||||
info("Successfully loaded dynamic bitrate interpolation settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (stream->dbr_enabled) {
|
||||
info("Dynamic bitrate enabled. Dropped frames begone!");
|
||||
}
|
||||
|
||||
obs_data_release(vsettings);
|
||||
obs_data_release(asettings);
|
||||
|
||||
if (drop_p < (drop_b + 200))
|
||||
drop_p = drop_b + 200;
|
||||
|
||||
@@ -1370,7 +1490,8 @@ static void *connect_thread(void *data)
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
|
||||
if (stream->video_codec[i] && stream->video_codec[i] != CODEC_H264 &&
|
||||
stream->video_codec[i] != CODEC_HEVC) {
|
||||
video_t *video = obs_get_video();
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, i);
|
||||
video_t *video = obs_encoder_video(enc);
|
||||
const struct video_output_info *info = video_output_get_info(video);
|
||||
|
||||
if (info->colorspace == VIDEO_CS_2100_HLG || info->colorspace == VIDEO_CS_2100_PQ) {
|
||||
@@ -1540,13 +1661,56 @@ static bool dbr_bitrate_lowered(struct rtmp_stream *stream)
|
||||
|
||||
static void dbr_set_bitrate(struct rtmp_stream *stream)
|
||||
{
|
||||
obs_encoder_t *vencoder = obs_output_get_video_encoder(stream->output);
|
||||
obs_data_t *settings = obs_encoder_get_settings(vencoder);
|
||||
if (stream->dbr_interpolation_table.array == NULL || stream->dbr_interpolation_table.num == 0)
|
||||
return;
|
||||
|
||||
obs_data_set_int(settings, "bitrate", stream->dbr_cur_bitrate);
|
||||
obs_encoder_update(vencoder, settings);
|
||||
size_t dbr_base_column = stream->dbr_interpolation_table.num - 1;
|
||||
for (size_t column = 0; column < stream->dbr_interpolation_table.num; ++column) {
|
||||
struct dbr_interpolation_point *dbr_point = &stream->dbr_interpolation_table.array[column];
|
||||
long column_bitrate = 0;
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; ++i) {
|
||||
column_bitrate += dbr_point->bitrates[i];
|
||||
}
|
||||
if (column_bitrate > stream->dbr_cur_bitrate)
|
||||
break;
|
||||
|
||||
obs_data_release(settings);
|
||||
dbr_base_column = column;
|
||||
}
|
||||
|
||||
size_t dbr_upper_column = dbr_base_column + 1;
|
||||
if (dbr_upper_column >= stream->dbr_interpolation_table.num)
|
||||
dbr_upper_column = stream->dbr_interpolation_table.num - 1;
|
||||
|
||||
struct dbr_interpolation_point *dbr_base_point = &stream->dbr_interpolation_table.array[dbr_base_column];
|
||||
struct dbr_interpolation_point *dbr_upper_point = &stream->dbr_interpolation_table.array[dbr_upper_column];
|
||||
long deltas[MAX_OUTPUT_VIDEO_ENCODERS] = {0};
|
||||
long remaining_bitrate = stream->dbr_cur_bitrate;
|
||||
long overall_delta = 0;
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; ++i) {
|
||||
deltas[i] = dbr_upper_point->bitrates[i] - dbr_base_point->bitrates[i];
|
||||
overall_delta += deltas[i];
|
||||
remaining_bitrate -= dbr_base_point->bitrates[i];
|
||||
}
|
||||
|
||||
if (overall_delta < 1)
|
||||
overall_delta = 1;
|
||||
|
||||
double ratio = remaining_bitrate / (double)overall_delta;
|
||||
double delta_scale = ratio < 1.0 ? ratio : 1.0;
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; ++i) {
|
||||
obs_encoder_t *enc = obs_output_get_video_encoder2(stream->output, i);
|
||||
if (!enc)
|
||||
continue;
|
||||
|
||||
long bitrate = dbr_base_point->bitrates[i] + (long)(deltas[i] * delta_scale);
|
||||
|
||||
obs_data_t *settings = obs_encoder_get_settings(enc);
|
||||
if (obs_data_get_int(settings, "bitrate") != bitrate) {
|
||||
obs_data_set_int(settings, "bitrate", bitrate);
|
||||
obs_encoder_update(enc, settings);
|
||||
}
|
||||
obs_data_release(settings);
|
||||
}
|
||||
}
|
||||
|
||||
static void dbr_inc_bitrate(struct rtmp_stream *stream)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__)
|
||||
|
||||
#define OPT_DYN_BITRATE "dyn_bitrate"
|
||||
#define OPT_DYN_BITRATE_INTERPOLATION_TABLE_DATA "interpolation_table_data"
|
||||
#define OPT_DROP_THRESHOLD "drop_threshold_ms"
|
||||
#define OPT_PFRAME_DROP_THRESHOLD "pframe_drop_threshold_ms"
|
||||
#define OPT_MAX_SHUTDOWN_TIME_SEC "max_shutdown_time_sec"
|
||||
@@ -52,6 +53,10 @@ struct dbr_frame {
|
||||
size_t size;
|
||||
};
|
||||
|
||||
struct dbr_interpolation_point {
|
||||
long bitrates[MAX_OUTPUT_VIDEO_ENCODERS];
|
||||
};
|
||||
|
||||
struct rtmp_stream {
|
||||
obs_output_t *output;
|
||||
|
||||
@@ -112,6 +117,7 @@ struct rtmp_stream {
|
||||
long dbr_cur_bitrate;
|
||||
long dbr_inc_bitrate;
|
||||
bool dbr_enabled;
|
||||
DARRAY(struct dbr_interpolation_point) dbr_interpolation_table;
|
||||
|
||||
enum audio_id_t audio_codec[MAX_OUTPUT_AUDIO_ENCODERS];
|
||||
enum video_id_t video_codec[MAX_OUTPUT_VIDEO_ENCODERS];
|
||||
|
||||
Reference in New Issue
Block a user