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

This commit is contained in:
derrod
2024-12-22 16:11:51 +01:00
committed by Ryan Foster
parent 345700b199
commit 966f086c96

View File

@@ -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>
@@ -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;
};
@@ -59,8 +60,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;
@@ -139,19 +140,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_destroy(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;
@@ -161,21 +208,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);
}
@@ -286,6 +329,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);
@@ -381,11 +427,6 @@ 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...");
@@ -393,11 +434,7 @@ static bool change_file(struct mp4_output *out, struct encoder_packet *pkt)
/* 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);
@@ -441,14 +478,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) {
@@ -465,10 +498,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);
}
@@ -564,10 +594,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)