obs-outputs: Implement multitrack dynamic bitrate

This commit is contained in:
Ruwen Hahn
2026-06-05 14:11:55 -04:00
committed by Ryan Foster
parent dd1e93f1ab
commit 4c97c62ccf
3 changed files with 215 additions and 43 deletions
+2
View File
@@ -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>
+207 -43
View File
@@ -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)
+6
View File
@@ -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];