obs-outputs: Add dynamic bitrate to RTMP output
The dynamic bitrate operates based upon estimating the current bitrate output, and then adjusting the bitrate on the fly as necessary when congestion is detected as a replacement for dropping frames. This may still need adjustment, as it is difficult to accurately emulate real-world frame drop scenarios. This does not currently drop frames at all, and because of that, very high congestion may cause additional stream delay to viewers (because data will be buffered), but from limited testing, most congestion will not cause that and it can safely recover pretty quickly without adding significant delay.
This commit is contained in:
@@ -17,6 +17,24 @@
|
||||
|
||||
#include "rtmp-stream.h"
|
||||
|
||||
#ifndef SEC_TO_NSEC
|
||||
#define SEC_TO_NSEC 1000000000ULL
|
||||
#endif
|
||||
|
||||
#ifndef MSEC_TO_USEC
|
||||
#define MSEC_TO_USEC 1000ULL
|
||||
#endif
|
||||
|
||||
#ifndef MSEC_TO_NSEC
|
||||
#define MSEC_TO_NSEC 1000000ULL
|
||||
#endif
|
||||
|
||||
/* dynamic bitrate coefficients */
|
||||
#define DBR_INC_TIMER (30ULL * SEC_TO_NSEC)
|
||||
#define DBR_TRIGGER_USEC (200ULL * MSEC_TO_USEC)
|
||||
#define MIN_ESTIMATE_DURATION_MS 1000
|
||||
#define MAX_ESTIMATE_DURATION_MS 2000
|
||||
|
||||
static const char *rtmp_stream_getname(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
@@ -107,6 +125,8 @@ static void rtmp_stream_destroy(void *data)
|
||||
#ifdef TEST_FRAMEDROPS
|
||||
circlebuf_free(&stream->droptest_info);
|
||||
#endif
|
||||
circlebuf_free(&stream->dbr_frames);
|
||||
pthread_mutex_destroy(&stream->dbr_mutex);
|
||||
|
||||
os_event_destroy(stream->buffer_space_available_event);
|
||||
os_event_destroy(stream->buffer_has_data_event);
|
||||
@@ -139,6 +159,11 @@ static void *rtmp_stream_create(obs_data_t *settings, obs_output_t *output)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&stream->dbr_mutex, NULL) != 0) {
|
||||
warn("Failed to initialize dbr mutex");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (os_event_init(&stream->buffer_space_available_event,
|
||||
OS_EVENT_TYPE_AUTO) != 0) {
|
||||
warn("Failed to initialize write buffer event");
|
||||
@@ -495,6 +520,39 @@ static void set_output_error(struct rtmp_stream *stream)
|
||||
obs_output_set_last_error(stream->output, msg);
|
||||
}
|
||||
|
||||
static void dbr_add_frame(struct rtmp_stream *stream, struct dbr_frame *back)
|
||||
{
|
||||
struct dbr_frame front;
|
||||
uint64_t dur;
|
||||
|
||||
circlebuf_push_back(&stream->dbr_frames, back, sizeof(*back));
|
||||
circlebuf_peek_front(&stream->dbr_frames, &front, sizeof(front));
|
||||
|
||||
stream->dbr_data_size += back->size;
|
||||
|
||||
dur = (back->send_end - front.send_beg) / 1000000;
|
||||
|
||||
if (dur >= MAX_ESTIMATE_DURATION_MS) {
|
||||
stream->dbr_data_size -= front.size;
|
||||
circlebuf_pop_front(&stream->dbr_frames, NULL, sizeof(front));
|
||||
}
|
||||
|
||||
stream->dbr_est_bitrate =
|
||||
(dur >= MIN_ESTIMATE_DURATION_MS)
|
||||
? (long)(stream->dbr_data_size * 1000 / dur)
|
||||
: 0;
|
||||
stream->dbr_est_bitrate *= 8;
|
||||
stream->dbr_est_bitrate /= 1000;
|
||||
|
||||
if (stream->dbr_est_bitrate) {
|
||||
stream->dbr_est_bitrate -= stream->audio_bitrate;
|
||||
if (stream->dbr_est_bitrate < 50)
|
||||
stream->dbr_est_bitrate = 50;
|
||||
}
|
||||
}
|
||||
|
||||
static void dbr_set_bitrate(struct rtmp_stream *stream);
|
||||
|
||||
static void *send_thread(void *data)
|
||||
{
|
||||
struct rtmp_stream *stream = data;
|
||||
@@ -503,6 +561,7 @@ static void *send_thread(void *data)
|
||||
|
||||
while (os_sem_wait(stream->send_sem) == 0) {
|
||||
struct encoder_packet packet;
|
||||
struct dbr_frame dbr_frame;
|
||||
|
||||
if (stopping(stream) && stream->stop_ts == 0) {
|
||||
break;
|
||||
@@ -525,10 +584,23 @@ static void *send_thread(void *data)
|
||||
}
|
||||
}
|
||||
|
||||
if (stream->dbr_enabled) {
|
||||
dbr_frame.send_beg = os_gettime_ns();
|
||||
dbr_frame.size = packet.size;
|
||||
}
|
||||
|
||||
if (send_packet(stream, &packet, false, packet.track_idx) < 0) {
|
||||
os_atomic_set_bool(&stream->disconnected, true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (stream->dbr_enabled) {
|
||||
dbr_frame.send_end = os_gettime_ns();
|
||||
|
||||
pthread_mutex_lock(&stream->dbr_mutex);
|
||||
dbr_add_frame(stream, &dbr_frame);
|
||||
pthread_mutex_unlock(&stream->dbr_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
bool encode_error = os_atomic_load_bool(&stream->encode_error);
|
||||
@@ -565,6 +637,15 @@ static void *send_thread(void *data)
|
||||
os_event_reset(stream->stop_event);
|
||||
os_atomic_set_bool(&stream->active, false);
|
||||
stream->sent_headers = false;
|
||||
|
||||
/* reset bitrate on stop */
|
||||
if (stream->dbr_enabled) {
|
||||
if (stream->dbr_cur_bitrate != stream->dbr_orig_bitrate) {
|
||||
stream->dbr_cur_bitrate = stream->dbr_orig_bitrate;
|
||||
dbr_set_bitrate(stream);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -923,6 +1004,7 @@ static bool init_connect(struct rtmp_stream *stream)
|
||||
const char *bind_ip;
|
||||
int64_t drop_p;
|
||||
int64_t drop_b;
|
||||
uint32_t caps;
|
||||
|
||||
if (stopping(stream)) {
|
||||
pthread_join(stream->send_thread, NULL);
|
||||
@@ -953,6 +1035,37 @@ static bool init_connect(struct rtmp_stream *stream)
|
||||
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);
|
||||
|
||||
circlebuf_free(&stream->dbr_frames);
|
||||
stream->audio_bitrate = (long)obs_data_get_int(asettings, "bitrate");
|
||||
stream->dbr_data_size = 0;
|
||||
stream->dbr_orig_bitrate = (long)obs_data_get_int(vsettings, "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;
|
||||
}
|
||||
|
||||
if (obs_output_get_delay(stream->output) != 0) {
|
||||
stream->dbr_enabled = false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1087,6 +1200,96 @@ static bool find_first_video_packet(struct rtmp_stream *stream,
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool dbr_bitrate_lowered(struct rtmp_stream *stream)
|
||||
{
|
||||
long prev_bitrate = stream->dbr_prev_bitrate;
|
||||
long est_bitrate = 0;
|
||||
long new_bitrate;
|
||||
|
||||
if (stream->dbr_est_bitrate &&
|
||||
stream->dbr_est_bitrate < stream->dbr_cur_bitrate) {
|
||||
stream->dbr_data_size = 0;
|
||||
circlebuf_pop_front(&stream->dbr_frames, NULL,
|
||||
stream->dbr_frames.size);
|
||||
est_bitrate = stream->dbr_est_bitrate / 100 * 100;
|
||||
if (est_bitrate < 50) {
|
||||
est_bitrate = 50;
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
if (prev_bitrate && est_bitrate) {
|
||||
if (prev_bitrate < est_bitrate) {
|
||||
blog(LOG_INFO, "going back to prev bitrate: "
|
||||
"prev_bitrate (%d) < est_bitrate (%d)",
|
||||
prev_bitrate,
|
||||
est_bitrate);
|
||||
new_bitrate = prev_bitrate;
|
||||
} else {
|
||||
new_bitrate = est_bitrate;
|
||||
}
|
||||
new_bitrate = est_bitrate;
|
||||
} else if (prev_bitrate) {
|
||||
new_bitrate = prev_bitrate;
|
||||
info("going back to prev bitrate");
|
||||
|
||||
} else if (est_bitrate) {
|
||||
new_bitrate = est_bitrate;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (est_bitrate) {
|
||||
new_bitrate = est_bitrate;
|
||||
|
||||
} else if (prev_bitrate) {
|
||||
new_bitrate = prev_bitrate;
|
||||
info("going back to prev bitrate");
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (new_bitrate == stream->dbr_cur_bitrate) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
stream->dbr_prev_bitrate = 0;
|
||||
stream->dbr_cur_bitrate = new_bitrate;
|
||||
stream->dbr_inc_timeout = os_gettime_ns() + DBR_INC_TIMER;
|
||||
info("bitrate decreased to: %ld", stream->dbr_cur_bitrate);
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
obs_data_set_int(settings, "bitrate", stream->dbr_cur_bitrate);
|
||||
obs_encoder_update(vencoder, settings);
|
||||
|
||||
obs_data_release(settings);
|
||||
}
|
||||
|
||||
static void dbr_inc_bitrate(struct rtmp_stream *stream)
|
||||
{
|
||||
stream->dbr_prev_bitrate = stream->dbr_cur_bitrate;
|
||||
stream->dbr_cur_bitrate += stream->dbr_inc_bitrate;
|
||||
|
||||
if (stream->dbr_cur_bitrate >= stream->dbr_orig_bitrate) {
|
||||
stream->dbr_cur_bitrate = stream->dbr_orig_bitrate;
|
||||
info("bitrate increased to: %ld, done",
|
||||
stream->dbr_cur_bitrate);
|
||||
} else if (stream->dbr_cur_bitrate < stream->dbr_orig_bitrate) {
|
||||
stream->dbr_inc_timeout = os_gettime_ns() + DBR_INC_TIMER;
|
||||
info("bitrate increased to: %ld, waiting",
|
||||
stream->dbr_cur_bitrate);
|
||||
}
|
||||
}
|
||||
|
||||
static void check_to_drop_frames(struct rtmp_stream *stream, bool pframes)
|
||||
{
|
||||
struct encoder_packet first;
|
||||
@@ -1098,6 +1301,18 @@ static void check_to_drop_frames(struct rtmp_stream *stream, bool pframes)
|
||||
int64_t drop_threshold = pframes ? stream->pframe_drop_threshold_usec
|
||||
: stream->drop_threshold_usec;
|
||||
|
||||
if (!pframes && stream->dbr_enabled) {
|
||||
if (stream->dbr_inc_timeout) {
|
||||
uint64_t t = os_gettime_ns();
|
||||
|
||||
if (t >= stream->dbr_inc_timeout) {
|
||||
stream->dbr_inc_timeout = 0;
|
||||
dbr_inc_bitrate(stream);
|
||||
dbr_set_bitrate(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (num_packets < 5) {
|
||||
if (!pframes)
|
||||
stream->congestion = 0.0f;
|
||||
@@ -1116,6 +1331,31 @@ static void check_to_drop_frames(struct rtmp_stream *stream, bool pframes)
|
||||
(float)buffer_duration_usec / (float)drop_threshold;
|
||||
}
|
||||
|
||||
/* alternatively, drop only pframes:
|
||||
* (!pframes && stream->dbr_enabled)
|
||||
* but let's test without dropping frames
|
||||
* at all first */
|
||||
if (stream->dbr_enabled) {
|
||||
bool bitrate_changed = false;
|
||||
|
||||
if (pframes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer_duration_usec >= DBR_TRIGGER_USEC) {
|
||||
pthread_mutex_lock(&stream->dbr_mutex);
|
||||
bitrate_changed = dbr_bitrate_lowered(stream);
|
||||
pthread_mutex_unlock(&stream->dbr_mutex);
|
||||
}
|
||||
|
||||
if (bitrate_changed) {
|
||||
debug("buffer_duration_msec: %" PRId64,
|
||||
buffer_duration_usec / 1000);
|
||||
dbr_set_bitrate(stream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer_duration_usec > drop_threshold) {
|
||||
debug("buffer_duration_usec: %" PRId64, buffer_duration_usec);
|
||||
drop_frames(stream, name, priority, pframes);
|
||||
|
Reference in New Issue
Block a user