Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

obs-outputs: Use packet time callback for frame-accurate chapter markers #11659

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 68 additions & 42 deletions plugins/obs-outputs/mp4-output.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include <obs-module.h>
#include <util/platform.h>
#include <util/deque.h>
#include <util/dstr.h>
#include <util/threading.h>
#include <util/buffered-file-serializer.h>
Expand All @@ -34,7 +35,7 @@
#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__)

struct chapter {
int64_t dts_usec;
uint64_t ts;
char *name;
};

Expand All @@ -56,8 +57,8 @@ struct mp4_output {
struct mp4_mux *muxer;
int flags;

int64_t last_dts_usec;
DARRAY(struct chapter) chapters;
size_t chapter_ctr;
struct deque chapters;

/* File splitting stuff */
bool split_file_enabled;
Expand Down Expand Up @@ -136,19 +137,65 @@ static const char *mp4_output_name(void *unused)
return obs_module_text("MP4Output");
}

static void mp4_clear_chapters(struct mp4_output *out)
{
while (out->chapters.size) {
struct chapter *chap = deque_data(&out->chapters, 0);
bfree(chap->name);
deque_pop_front(&out->chapters, NULL, sizeof(struct chapter));
}
out->chapter_ctr = 0;
}

static void mp4_output_destory(void *data)
{
struct mp4_output *out = data;

for (size_t i = 0; i < out->chapters.num; i++)
bfree(out->chapters.array[i].name);
da_free(out->chapters);

pthread_mutex_destroy(&out->mutex);
mp4_clear_chapters(out);
deque_free(&out->chapters);
dstr_free(&out->path);
bfree(out);
}

void mp4_pkt_callback(obs_output_t *output, struct encoder_packet *pkt, struct encoder_packet_time *pkt_time,
void *param)
{
UNUSED_PARAMETER(output);
struct mp4_output *out = param;

/* Only the primary video track is used as the reference for the chapter track. */
if (!pkt_time || !pkt || pkt->type != OBS_ENCODER_VIDEO || pkt->track_idx != 0)
return;

pthread_mutex_lock(&out->mutex);
struct chapter *chap = NULL;
/* Write all queued chapters with a timestamp <= the current frame's composition time. */
while (out->chapters.size) {
chap = deque_data(&out->chapters, 0);
if (chap->ts > pkt_time->cts)
break;

/* Video frames can be out of order (b-frames), so instead of using the video packet's dts_usec we need to calculate
* the chapter DTS from the frame's PTS (for chapters DTS == PTS). */
int64_t chap_dts_usec = pkt->pts * 1000000 / pkt->timebase_den;
int64_t chap_dts_msec = chap_dts_usec / 1000;
int64_t chap_dts_sec = chap_dts_msec / 1000;

int milliseconds = (int)(chap_dts_msec % 1000);
int seconds = (int)chap_dts_sec % 60;
int minutes = ((int)chap_dts_sec / 60) % 60;
int hours = (int)chap_dts_sec / 3600;
info("Adding chapter \"%s\" at %02d:%02d:%02d.%03d", chap->name, hours, minutes, seconds, milliseconds);

mp4_mux_add_chapter(out->muxer, chap_dts_usec, chap->name);
/* Free name and remove chapter from queue. */
bfree(chap->name);
deque_pop_front(&out->chapters, NULL, sizeof(struct chapter));
}
pthread_mutex_unlock(&out->mutex);
}

static void mp4_add_chapter_proc(void *data, calldata_t *cd)
{
struct mp4_output *out = data;
Expand All @@ -158,21 +205,17 @@ static void mp4_add_chapter_proc(void *data, calldata_t *cd)

if (name.len == 0) {
/* Generate name if none provided. */
dstr_catf(&name, "%s %zu", obs_module_text("MP4Output.UnnamedChapter"), out->chapters.num + 1);
dstr_catf(&name, "%s %zu", obs_module_text("MP4Output.UnnamedChapter"), out->chapter_ctr + 1);
}

int64_t totalRecordSeconds = out->last_dts_usec / 1000 / 1000;
int seconds = (int)totalRecordSeconds % 60;
int totalMinutes = (int)totalRecordSeconds / 60;
int minutes = totalMinutes % 60;
int hours = totalMinutes / 60;

info("Adding chapter \"%s\" at %02d:%02d:%02d", name.array, hours, minutes, seconds);

pthread_mutex_lock(&out->mutex);
struct chapter *chap = da_push_back_new(out->chapters);
chap->dts_usec = out->last_dts_usec;
chap->name = name.array;
/* Enqueue chapter marker to be inserted once a packet containing the video frame with the corresponding timestamp is
* being processed. */
struct chapter chap = {0};
chap.ts = obs_get_video_frame_time();
chap.name = name.array;
deque_push_back(&out->chapters, &chap, sizeof(struct chapter));
out->chapter_ctr++;
pthread_mutex_unlock(&out->mutex);
}

Expand Down Expand Up @@ -272,6 +315,9 @@ static bool mp4_output_start(void *data)
return false;
}

/* Add packet callback for accurate chapter markers. */
obs_output_add_packet_callback(out->output, mp4_pkt_callback, (void *)out);

/* Initialise muxer and start capture */
out->muxer = mp4_mux_create(out->output, &out->serializer, out->flags);
os_atomic_set_bool(&out->active, true);
Expand Down Expand Up @@ -367,23 +413,14 @@ static bool change_file(struct mp4_output *out, struct encoder_packet *pkt)
uint64_t start_time = os_gettime_ns();

/* finalise file */
for (size_t i = 0; i < out->chapters.num; i++) {
struct chapter *chap = &out->chapters.array[i];
mp4_mux_add_chapter(out->muxer, chap->dts_usec, chap->name);
}

mp4_mux_finalise(out->muxer);

info("Waiting for file writer to finish...");

/* flush/close file and destroy old muxer */
buffered_file_serializer_free(&out->serializer);
mp4_mux_destroy(out->muxer);

for (size_t i = 0; i < out->chapters.num; i++)
bfree(out->chapters.array[i].name);

da_clear(out->chapters);
mp4_clear_chapters(out);

info("MP4 file split complete. Finalization took %" PRIu64 " ms.", (os_gettime_ns() - start_time) / 1000000);

Expand Down Expand Up @@ -427,14 +464,10 @@ static void mp4_mux_destroy_task(void *ptr)
static void mp4_output_actual_stop(struct mp4_output *out, int code)
{
os_atomic_set_bool(&out->active, false);
obs_output_remove_packet_callback(out->output, mp4_pkt_callback, NULL);

uint64_t start_time = os_gettime_ns();

for (size_t i = 0; i < out->chapters.num; i++) {
struct chapter *chap = &out->chapters.array[i];
mp4_mux_add_chapter(out->muxer, chap->dts_usec, chap->name);
}

mp4_mux_finalise(out->muxer);

if (code) {
Expand All @@ -451,10 +484,7 @@ static void mp4_output_actual_stop(struct mp4_output *out, int code)
out->muxer = NULL;

/* Clear chapter data */
for (size_t i = 0; i < out->chapters.num; i++)
bfree(out->chapters.array[i].name);

da_clear(out->chapters);
mp4_clear_chapters(out);

info("MP4 file output complete. Finalization took %" PRIu64 " ms.", (os_gettime_ns() - start_time) / 1000000);
}
Expand Down Expand Up @@ -550,10 +580,6 @@ static void mp4_output_packet(void *data, struct encoder_packet *packet)
if (out->split_file_enabled)
ts_offset_update(out, packet);

/* Update PTS for chapter markers */
if (packet->type == OBS_ENCODER_VIDEO && packet->track_idx == 0)
out->last_dts_usec = packet->dts_usec - out->start_time;

submit_packet(out, packet);

if (serializer_get_pos(&out->serializer) == -1)
Expand Down
Loading