From 12eca135360c0dec53e5a80873240181ac2c45f6 Mon Sep 17 00:00:00 2001 From: Joaquin Bartaburu Date: Thu, 23 Jul 2026 16:23:38 -0300 Subject: [PATCH 1/7] feat: handle init_data when encoder sends it on first frame encoded --- src/moq-output.cpp | 59 ++++++++++++++++++++++++++++++++++++---------- src/moq-output.h | 3 ++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/moq-output.cpp b/src/moq-output.cpp index 4040dda..ea1c091 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -165,6 +165,27 @@ moq_media_track_t *MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) return new_track; } +moq_media_track_t *MOQOutput::CreateVideoTrackFromPacket(moq_media_sender_t *cur_sender, struct encoder_packet *packet) +{ + std::vector init = AnnexBToAvcC(packet->data, packet->size); + if (init.empty()) { + return nullptr; + } + + video_init_data = std::move(init); + video_codec = AvcCodecString(video_init_data); + + moq_media_track_t *new_track = CreateVideoTrack(cur_sender); + if (!new_track) { + blog(LOG_WARNING, "[obs-moq] failed to create video track from first frame"); + return nullptr; + } + + blog(LOG_INFO, "[obs-moq] video track created from first frame (codec %s, init_data %zu bytes)", + video_codec.c_str(), video_init_data.size()); + return new_track; +} + moq_media_track_t *MOQOutput::CreateAudioTrack(moq_media_sender_t *new_sender) { moq_media_track_cfg_t tcfg; @@ -257,12 +278,15 @@ bool MOQOutput::Connect() return false; } - moq_media_track_t *new_video_track = CreateVideoTrack(media_sender); - if (!new_video_track) { - blog(LOG_WARNING, "[obs-moq] failed to create video track"); - moq_media_sender_destroy(media_sender); - obs_output_signal_stop(output, OBS_OUTPUT_ERROR); - return false; + moq_media_track_t *new_video_track = nullptr; + if (!video_init_data.empty()) { + new_video_track = CreateVideoTrack(media_sender); + if (!new_video_track) { + blog(LOG_WARNING, "[obs-moq] failed to create video track"); + moq_media_sender_destroy(media_sender); + obs_output_signal_stop(output, OBS_OUTPUT_ERROR); + return false; + } } moq_media_track_t *new_audio_track = CreateAudioTrack(media_sender); @@ -348,8 +372,8 @@ void MOQOutput::Stop(bool signal) start_time_ns = os_gettime_ns(); } -void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t *track, bool is_sync, bool starts_group, - bool ends_group) +void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t **track, bool is_sync, + bool starts_group, bool ends_group) { moq_rcbuf_t *payload = nullptr; @@ -376,12 +400,21 @@ void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t *tra moq_result_t res; { std::lock_guard lock(sender_mutex); - if (!sender || !track) { - // release the rcbuf since we won't be sending it + if (!sender) { moq_rcbuf_decref(payload); return; } - res = moq_media_sender_write(sender, track, &obj); + + if (!*track && packet->type == OBS_ENCODER_VIDEO && packet->keyframe) { + *track = CreateVideoTrackFromPacket(sender, packet); + } + + if (!*track) { + moq_rcbuf_decref(payload); + return; + } + + res = moq_media_sender_write(sender, *track, &obj); } if (res != MOQ_OK) { @@ -404,10 +437,10 @@ void MOQOutput::Data(struct encoder_packet *packet) return; } if (packet->type == OBS_ENCODER_VIDEO) { - SendPacket(packet, video_track, packet->keyframe, packet->keyframe, false); + SendPacket(packet, &video_track, packet->keyframe, packet->keyframe, false); } if (packet->type == OBS_ENCODER_AUDIO) { - SendPacket(packet, audio_track, true, true, true); + SendPacket(packet, &audio_track, true, true, true); } } diff --git a/src/moq-output.h b/src/moq-output.h index ff35928..e95329e 100644 --- a/src/moq-output.h +++ b/src/moq-output.h @@ -45,8 +45,9 @@ class MOQOutput { bool LoadVideoEncoderSettings(); bool LoadAudioEncoderSettings(); moq_media_track_t *CreateVideoTrack(moq_media_sender_t *new_sender); + moq_media_track_t *CreateVideoTrackFromPacket(moq_media_sender_t *cur_sender, struct encoder_packet *packet); moq_media_track_t *CreateAudioTrack(moq_media_sender_t *new_sender); - void SendPacket(struct encoder_packet *packet, moq_media_track_t *track, bool is_sync, bool starts_group, + void SendPacket(struct encoder_packet *packet, moq_media_track_t **track, bool is_sync, bool starts_group, bool ends_group); bool ResolveServiceConfig(); bool Connect(); From 433e8653487dc7c756593377512b9ac80cd9159a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Bogar=C3=ADn?= Date: Mon, 27 Jul 2026 14:59:39 -0300 Subject: [PATCH 2/7] integrate moq5's codec signaling library --- CMakeLists.txt | 3 +- src/codec-signaling.h | 81 +++++++++++++++++++++++++++++++++++++++++++ src/moq-output.cpp | 21 ++++++----- src/utils.h | 49 -------------------------- 4 files changed, 95 insertions(+), 59 deletions(-) create mode 100644 src/codec-signaling.h delete mode 100644 src/utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fdc58f..ec56176 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ find_package(libobs REQUIRED) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::libobs) find_package(libmoq REQUIRED COMPONENTS service) -target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE moq::service) +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE moq::service moq::codec-signaling) if(ENABLE_FRONTEND_API) find_package(obs-frontend-api REQUIRED) @@ -43,6 +43,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/obs-moq.cpp src/moq-output.cpp src/moq-output.h + src/codec-signaling.h src/moq-service.cpp src/moq-service.h ) diff --git a/src/codec-signaling.h b/src/codec-signaling.h new file mode 100644 index 0000000..3238efe --- /dev/null +++ b/src/codec-signaling.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +struct TrackCodec { + moq_codec_source_format_t source_format; + moq_codec_config_format_t config_format; + const char *sample_entry; + bool has_object_type_indication; + uint8_t object_type_indication; +}; + +inline constexpr TrackCodec kCodecH264{MOQ_CODEC_SOURCE_AVC_ANNEXB, MOQ_CODEC_CONFIG_AVCC, "avc1", false, 0x00}; +inline constexpr TrackCodec kCodecH265{MOQ_CODEC_SOURCE_HEVC_ANNEXB, MOQ_CODEC_CONFIG_HVCC, "hvc1", false, 0x00}; +inline constexpr TrackCodec kCodecAac{MOQ_CODEC_SOURCE_AAC_ASC, MOQ_CODEC_CONFIG_AAC_ASC, "mp4a", true, 0x40}; + + +inline const TrackCodec *ResolveTrackCodec(const char *codec) +{ + if (!codec) + return nullptr; + if (strcmp(codec, "h264") == 0) + return &kCodecH264; + if (strcmp(codec, "hevc") == 0) + return &kCodecH265; + if (strcmp(codec, "aac") == 0) + return &kCodecAac; + return nullptr; +} + +inline std::vector BuildInitData(const char *codec, const uint8_t *src, size_t len) +{ + const TrackCodec *tc = ResolveTrackCodec(codec); + if (!tc || !src || len == 0) + return {}; + + moq_codec_init_data_cfg_t cfg; + moq_codec_init_data_cfg_init(&cfg); + cfg.source_format = tc->source_format; + cfg.source = {src, len}; + + size_t need = 0; + moq_result_t r = moq_codec_init_data_build(&cfg, nullptr, 0, &need); + if ((r != MOQ_ERR_BUFFER && r != MOQ_OK) || need == 0) + return {}; + + std::vector init_data(need); + if (moq_codec_init_data_build(&cfg, init_data.data(), init_data.size(), &need) != MOQ_OK) + return {}; + + init_data.resize(need); + return init_data; +} + +inline std::string BuildCodecString(const char *codec, const std::vector &init_data) +{ + const TrackCodec *tc = ResolveTrackCodec(codec); + if (!tc) + return {}; + + moq_codec_string_cfg_t cfg; + moq_codec_string_cfg_init(&cfg); + cfg.config_format = tc->config_format; + cfg.sample_entry = moq_bytes_cstr(tc->sample_entry); + cfg.has_mp4_object_type_indication = tc->has_object_type_indication; + cfg.mp4_object_type_indication = tc->object_type_indication; + cfg.decoder_config = {init_data.data(), init_data.size()}; + + uint8_t buf[64]; + size_t need = 0; + if (moq_codec_string_format(&cfg, buf, sizeof(buf), &need) == MOQ_OK) + return std::string((const char *)buf, need); + + return {}; +} diff --git a/src/moq-output.cpp b/src/moq-output.cpp index ea1c091..93313df 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -1,5 +1,5 @@ #include "moq-output.h" -#include "utils.h" +#include "codec-signaling.h" #include #include @@ -67,8 +67,8 @@ bool MOQOutput::LoadVideoEncoderSettings() size_t extra_size = 0; obs_encoder_get_extra_data(venc, &extra, &extra_size); - video_init_data = AnnexBToAvcC(extra, extra_size); - video_codec = AvcCodecString(video_init_data); + video_init_data = BuildInitData(codec, extra, extra_size); + video_codec = BuildCodecString(codec, video_init_data); return true; } @@ -93,8 +93,8 @@ bool MOQOutput::LoadAudioEncoderSettings() uint8_t *extra = nullptr; size_t extra_size = 0; obs_encoder_get_extra_data(aenc, &extra, &extra_size); - audio_init_data.assign(extra, extra + extra_size); - audio_codec = AacCodecString(audio_init_data); + audio_init_data = BuildInitData(codec, extra, extra_size); + audio_codec = BuildCodecString(codec, audio_init_data); return true; } @@ -167,13 +167,16 @@ moq_media_track_t *MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) moq_media_track_t *MOQOutput::CreateVideoTrackFromPacket(moq_media_sender_t *cur_sender, struct encoder_packet *packet) { - std::vector init = AnnexBToAvcC(packet->data, packet->size); + obs_encoder_t *venc = obs_output_get_video_encoder(output); + const char *codec = venc ? obs_encoder_get_codec(venc) : nullptr; + + std::vector init = BuildInitData(codec, packet->data, packet->size); if (init.empty()) { return nullptr; } video_init_data = std::move(init); - video_codec = AvcCodecString(video_init_data); + video_codec = BuildCodecString(codec, video_init_data); moq_media_track_t *new_track = CreateVideoTrack(cur_sender); if (!new_track) { @@ -471,8 +474,8 @@ void register_moq_output() // todo: change to OBS_OUTPUT_AV when audio is supported info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE; info.protocols = "MOQ"; - // todo: add support for hevc and av1 - info.encoded_video_codecs = "h264"; + // todo: add support for av1 + info.encoded_video_codecs = "h264;hevc"; // todo: add support for opus and ac3 info.encoded_audio_codecs = "aac"; diff --git a/src/utils.h b/src/utils.h deleted file mode 100644 index af54ed9..0000000 --- a/src/utils.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include - -static std::string AvcCodecString(const std::vector &avcc) -{ - uint8_t profile = 0x42, constraints = 0xe0, level = 0x1f; - - if (avcc.size() >= 4 && avcc[0] == 1) { - profile = avcc[1]; - constraints = avcc[2]; - level = avcc[3]; - } - - char buf[16]; - snprintf(buf, sizeof(buf), "avc1.%02x%02x%02x", profile, constraints, level); - return buf; -} - -static std::vector AnnexBToAvcC(const uint8_t *extra, size_t size) -{ - uint8_t *avcc = nullptr; - size_t avcc_size = obs_parse_avc_header(&avcc, extra, size); - if (!avcc || avcc_size == 0) { - bfree(avcc); - return {}; - } - std::vector out(avcc, avcc + avcc_size); - bfree(avcc); - return out; -} - -static std::string AacCodecString(const std::vector &asc) -{ - uint8_t object_type = 2; - - if (asc.size() >= 2) { - object_type = (asc[0] >> 3) & 0x1f; - if (object_type == 31 && asc.size() >= 3) { - object_type = 32 + (((asc[1] & 0x07) << 3) | ((asc[2] >> 5) & 0x07)); - } - } - - char buf[16]; - snprintf(buf, sizeof(buf), "mp4a.40.%u", object_type); - return buf; -} From c6cc61d2abaed6e0ff7527139277c40f9ba9f344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Bogar=C3=ADn?= Date: Tue, 28 Jul 2026 11:00:07 -0300 Subject: [PATCH 3/7] disable bframes for hardware encoders --- src/moq-service.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/moq-service.cpp b/src/moq-service.cpp index f82ec2b..5ac7673 100644 --- a/src/moq-service.cpp +++ b/src/moq-service.cpp @@ -35,6 +35,7 @@ void MOQService::ApplyEncoderSettings(obs_data_t *video_settings, obs_data_t *au blog(LOG_INFO, "[obs-moq] apply encoder settings"); if (video_settings) { obs_data_set_int(video_settings, "bf", 0); + obs_data_set_bool(video_settings, "bframes", false); //todo: check if this is needed obs_data_set_bool(video_settings, "repeat_headers", true); obs_data_set_int(video_settings, "keyint_sec", 2); From 6fbccf5a12bfc681ac6cb62c96f74ee3636304db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Bogar=C3=ADn?= Date: Tue, 28 Jul 2026 11:10:32 -0300 Subject: [PATCH 4/7] declare missing h265 in service's supported video codecs --- src/moq-service.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moq-service.cpp b/src/moq-service.cpp index 5ac7673..cdd5115 100644 --- a/src/moq-service.cpp +++ b/src/moq-service.cpp @@ -4,7 +4,7 @@ #include const char *audio_codecs[] = {"aac", nullptr}; -const char *video_codecs[] = {"h264", nullptr}; +const char *video_codecs[] = {"h264", "h265", nullptr}; MOQService::MOQService(obs_data_t *settings, obs_service_t *service) { From a5b1e80f602f838f8cc184d3bce4da29ca914abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Bogar=C3=ADn?= Date: Tue, 28 Jul 2026 11:27:42 -0300 Subject: [PATCH 5/7] add opus support --- src/codec-signaling.h | 3 +++ src/moq-output.cpp | 4 ++-- src/moq-service.cpp | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/codec-signaling.h b/src/codec-signaling.h index 3238efe..28fd91c 100644 --- a/src/codec-signaling.h +++ b/src/codec-signaling.h @@ -19,6 +19,7 @@ struct TrackCodec { inline constexpr TrackCodec kCodecH264{MOQ_CODEC_SOURCE_AVC_ANNEXB, MOQ_CODEC_CONFIG_AVCC, "avc1", false, 0x00}; inline constexpr TrackCodec kCodecH265{MOQ_CODEC_SOURCE_HEVC_ANNEXB, MOQ_CODEC_CONFIG_HVCC, "hvc1", false, 0x00}; inline constexpr TrackCodec kCodecAac{MOQ_CODEC_SOURCE_AAC_ASC, MOQ_CODEC_CONFIG_AAC_ASC, "mp4a", true, 0x40}; +inline constexpr TrackCodec kCodecOpus{MOQ_CODEC_SOURCE_OPUS_HEAD, MOQ_CODEC_CONFIG_OPUS, "opus", false, 0x00}; inline const TrackCodec *ResolveTrackCodec(const char *codec) @@ -31,6 +32,8 @@ inline const TrackCodec *ResolveTrackCodec(const char *codec) return &kCodecH265; if (strcmp(codec, "aac") == 0) return &kCodecAac; + if (strcmp(codec, "opus") == 0) + return &kCodecOpus; return nullptr; } diff --git a/src/moq-output.cpp b/src/moq-output.cpp index 93313df..a6d299c 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -476,8 +476,8 @@ void register_moq_output() info.protocols = "MOQ"; // todo: add support for av1 info.encoded_video_codecs = "h264;hevc"; - // todo: add support for opus and ac3 - info.encoded_audio_codecs = "aac"; + // todo: add support for ac3 + info.encoded_audio_codecs = "aac;opus"; info.get_name = [](void *) -> const char * { return obs_module_text("Output.Name"); diff --git a/src/moq-service.cpp b/src/moq-service.cpp index cdd5115..4630d70 100644 --- a/src/moq-service.cpp +++ b/src/moq-service.cpp @@ -3,8 +3,8 @@ #include #include -const char *audio_codecs[] = {"aac", nullptr}; -const char *video_codecs[] = {"h264", "h265", nullptr}; +const char *audio_codecs[] = {"aac", "opus", nullptr}; +const char *video_codecs[] = {"h264", "hevc", nullptr}; MOQService::MOQService(obs_data_t *settings, obs_service_t *service) { From ba8a08fbddd49c5e5254a58b5a8fbe66d6786e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Bogar=C3=ADn?= Date: Tue, 28 Jul 2026 12:25:12 -0300 Subject: [PATCH 6/7] add av1 support --- src/codec-signaling.h | 3 +++ src/moq-output.cpp | 5 +---- src/moq-service.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/codec-signaling.h b/src/codec-signaling.h index 28fd91c..5d4ee43 100644 --- a/src/codec-signaling.h +++ b/src/codec-signaling.h @@ -18,6 +18,7 @@ struct TrackCodec { inline constexpr TrackCodec kCodecH264{MOQ_CODEC_SOURCE_AVC_ANNEXB, MOQ_CODEC_CONFIG_AVCC, "avc1", false, 0x00}; inline constexpr TrackCodec kCodecH265{MOQ_CODEC_SOURCE_HEVC_ANNEXB, MOQ_CODEC_CONFIG_HVCC, "hvc1", false, 0x00}; +inline constexpr TrackCodec kCodecAv1{MOQ_CODEC_SOURCE_AV1_OBU, MOQ_CODEC_CONFIG_AV1C, "av01", false, 0x00}; inline constexpr TrackCodec kCodecAac{MOQ_CODEC_SOURCE_AAC_ASC, MOQ_CODEC_CONFIG_AAC_ASC, "mp4a", true, 0x40}; inline constexpr TrackCodec kCodecOpus{MOQ_CODEC_SOURCE_OPUS_HEAD, MOQ_CODEC_CONFIG_OPUS, "opus", false, 0x00}; @@ -30,6 +31,8 @@ inline const TrackCodec *ResolveTrackCodec(const char *codec) return &kCodecH264; if (strcmp(codec, "hevc") == 0) return &kCodecH265; + if (strcmp(codec, "av1") == 0) + return &kCodecAv1; if (strcmp(codec, "aac") == 0) return &kCodecAac; if (strcmp(codec, "opus") == 0) diff --git a/src/moq-output.cpp b/src/moq-output.cpp index a6d299c..5cfb926 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -471,12 +471,9 @@ void register_moq_output() { struct obs_output_info info = {}; info.id = "moq_output"; - // todo: change to OBS_OUTPUT_AV when audio is supported info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE; info.protocols = "MOQ"; - // todo: add support for av1 - info.encoded_video_codecs = "h264;hevc"; - // todo: add support for ac3 + info.encoded_video_codecs = "h264;hevc;av1"; info.encoded_audio_codecs = "aac;opus"; info.get_name = [](void *) -> const char * { diff --git a/src/moq-service.cpp b/src/moq-service.cpp index 4630d70..08ccbad 100644 --- a/src/moq-service.cpp +++ b/src/moq-service.cpp @@ -4,7 +4,7 @@ #include const char *audio_codecs[] = {"aac", "opus", nullptr}; -const char *video_codecs[] = {"h264", "hevc", nullptr}; +const char *video_codecs[] = {"h264", "hevc", "av1", nullptr}; MOQService::MOQService(obs_data_t *settings, obs_service_t *service) { From 2a062e20caeefc9535e9f1920227ab3e633e8e0f Mon Sep 17 00:00:00 2001 From: Joaquin Bartaburu Date: Fri, 7 Aug 2026 16:01:09 -0300 Subject: [PATCH 7/7] feat: cmaf packaging working --- CMakeLists.txt | 7 +- src/moq-cmaf.h | 67 +++++++++++++++++++ src/moq-output.cpp | 157 +++++++++++++++++++++++++++++++++------------ src/moq-output.h | 17 +++++ 4 files changed, 206 insertions(+), 42 deletions(-) create mode 100644 src/moq-cmaf.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ec56176..f8f3cf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,11 @@ find_package(libobs REQUIRED) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::libobs) find_package(libmoq REQUIRED COMPONENTS service) -target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE moq::service moq::codec-signaling) + +target_link_libraries( + ${CMAKE_PROJECT_NAME} + PRIVATE moq::service moq::codec-signaling moq::cmaf-packager +) if(ENABLE_FRONTEND_API) find_package(obs-frontend-api REQUIRED) @@ -44,6 +48,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/moq-output.cpp src/moq-output.h src/codec-signaling.h + src/moq-cmaf.h src/moq-service.cpp src/moq-service.h ) diff --git a/src/moq-cmaf.h b/src/moq-cmaf.h new file mode 100644 index 0000000..ffd1573 --- /dev/null +++ b/src/moq-cmaf.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include + +#include "moq-output.h" + +static moq_cmaf_codec_kind_t codec_kind_from_name(const char *codec) +{ + if (!codec) + return MOQ_CMAF_CODEC_UNKNOWN; + if (strcmp(codec, "h264") == 0) + return MOQ_CMAF_CODEC_AVC; + if (strcmp(codec, "hevc") == 0) + return MOQ_CMAF_CODEC_HEVC; + if (strcmp(codec, "av1") == 0) + return MOQ_CMAF_CODEC_AV1; + if (strcmp(codec, "aac") == 0) + return MOQ_CMAF_CODEC_AAC; + if (strcmp(codec, "opus") == 0) + return MOQ_CMAF_CODEC_OPUS; + return MOQ_CMAF_CODEC_UNKNOWN; +} + +static CMAFPackagerPtr create_packager(const moq_cmaf_packager_cfg_t *cfg, const char *codec) +{ + moq_cmaf_packager_t *packager = nullptr; + const moq_result_t rc = moq_cmaf_packager_create(nullptr, cfg, &packager); + if (rc != MOQ_OK) { + blog(LOG_WARNING, "[obs-moq] CMAF: packager init failed for '%s': %s", codec, moq_strerror(rc)); + return nullptr; + } + + return CMAFPackagerPtr(packager); +} + +static int64_t rescale_timestamp(int64_t v, int64_t num, int64_t den) +{ + if (den <= 0 || num <= 0 || num == den) + return v; + return (v / den) * num + ((v % den) * num) / den; +} + +static bool package_packet(moq_cmaf_packager_t *packager, const struct encoder_packet *packet, moq_bytes_t *out) +{ + if (!packager || !packet || !packet->data || packet->size == 0) + return false; + + const int64_t num = packet->timebase_num ? packet->timebase_num : 1; + const int64_t den = packet->timebase_den ? packet->timebase_den : 1; + const int64_t scale = static_cast(moq_cmaf_packager_timescale(packager)) * num; + + moq_cmaf_packager_sample_t sample = {}; + sample.data = {packet->data, packet->size}; + sample.pts = rescale_timestamp(packet->pts, scale, den); + sample.dts = rescale_timestamp(packet->dts, scale, den); + sample.keyframe = packet->keyframe; + + const moq_result_t rc = moq_cmaf_packager_write(packager, &sample, 1, out); + if (rc != MOQ_OK) { + return false; + } + + return true; +} diff --git a/src/moq-output.cpp b/src/moq-output.cpp index a548c65..7f5cb14 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -1,11 +1,14 @@ #include "moq-output.h" #include "codec-signaling.h" +#include "moq-cmaf.h" #include #include #define VIDEO_TIMESCALE 1000000u +#define CMAF_VIDEO_TIMESCALE 90000u + MOQOutput::MOQOutput(obs_data_t *settings, obs_output_t *output) : output(output) { blog(LOG_INFO, "[obs-moq] output created"); @@ -52,8 +55,6 @@ bool MOQOutput::LoadVideoEncoderSettings() video_conf.bitrate = (uint64_t)obs_data_get_int(settings, "bitrate") * 1000; const char *codec = obs_encoder_get_codec(venc); - //todo: add codec validation here - video_conf.video_width = obs_encoder_get_width(venc); video_conf.video_height = obs_encoder_get_height(venc); @@ -62,11 +63,9 @@ bool MOQOutput::LoadVideoEncoderSettings() video_conf.fps_num = ovi.fps_num; video_conf.fps_den = ovi.fps_den; - //initialize init_data uint8_t *extra = nullptr; size_t extra_size = 0; obs_encoder_get_extra_data(venc, &extra, &extra_size); - video_init_data = BuildInitData(codec, extra, extra_size); video_codec = BuildCodecString(codec, video_init_data); return true; @@ -85,10 +84,10 @@ bool MOQOutput::LoadAudioEncoderSettings() audio_conf.bitrate = (uint64_t)obs_data_get_int(settings, "bitrate") * 1000; audio_t *audio = obs_encoder_audio(aenc); audio_conf.samplerate = audio_output_get_sample_rate(audio); - audio_conf.channels = std::to_string(audio_output_get_channels(audio)); + audio_conf.channel_count = (uint32_t)audio_output_get_channels(audio); + audio_conf.channels = std::to_string(audio_conf.channel_count); const char *codec = obs_encoder_get_codec(aenc); - //todo: add codec validation here uint8_t *extra = nullptr; size_t extra_size = 0; @@ -99,6 +98,53 @@ bool MOQOutput::LoadAudioEncoderSettings() return true; } +bool MOQOutput::InitCMAFVideoPackager() +{ + obs_encoder_t *venc = obs_output_get_video_encoder(output); + const char *codec = venc ? obs_encoder_get_codec(venc) : nullptr; + + moq_cmaf_packager_cfg_t cfg; + moq_cmaf_packager_cfg_init(&cfg); + cfg.codec_kind = codec_kind_from_name(codec); + cfg.codec_config = {video_init_data.data(), video_init_data.size()}; + cfg.timescale = CMAF_VIDEO_TIMESCALE; + cfg.width = video_conf.video_width; + cfg.height = video_conf.video_height; + cfg.fps_num = video_conf.fps_num; + cfg.fps_den = video_conf.fps_den; + cfg.rebase_timestamps = true; + + video_packager = create_packager(&cfg, codec); + if (!video_packager) { + obs_output_set_last_error(output, obs_module_text("Error.NoEncoder")); + return false; + } + return true; +} + +bool MOQOutput::InitCMAFAudioPackager() +{ + obs_encoder_t *aenc = obs_output_get_audio_encoder(output, 0); + const char *codec = aenc ? obs_encoder_get_codec(aenc) : nullptr; + + moq_cmaf_packager_cfg_t cfg; + moq_cmaf_packager_cfg_init(&cfg); + cfg.codec_kind = codec_kind_from_name(codec); + cfg.codec_config = {audio_init_data.data(), audio_init_data.size()}; + cfg.timescale = 0; + cfg.samplerate = audio_conf.samplerate; + cfg.channel_count = audio_conf.channel_count; + cfg.avg_bitrate = static_cast(audio_conf.bitrate); + cfg.rebase_timestamps = true; + + audio_packager = create_packager(&cfg, codec); + if (!audio_packager) { + obs_output_set_last_error(output, obs_module_text("Error.NoAudioEncoder")); + return false; + } + return true; +} + bool MOQOutput::ResolveServiceConfig() { url.clear(); @@ -144,12 +190,20 @@ moq_media_track_t *MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) moq_media_track_cfg_init(&tcfg); tcfg.name = {(const uint8_t *)"video", 5}; tcfg.media_type = MOQ_MEDIA_TYPE_VIDEO; - // todo: make this configurable and add CMAF support - tcfg.packaging = MOQ_MEDIA_PACKAGING_RAW; + tcfg.packaging = cmaf_enabled ? MOQ_MEDIA_PACKAGING_CMAF : MOQ_MEDIA_PACKAGING_RAW; tcfg.codec = {(const uint8_t *)video_codec.c_str(), video_codec.size()}; - tcfg.timescale = VIDEO_TIMESCALE; - // todo: analyze actual need for this and how it fits w/other codecs - tcfg.init_data = {video_init_data.data(), video_init_data.size()}; + + if (cmaf_enabled) { + if (!InitCMAFVideoPackager()) + return nullptr; + + tcfg.init_data = moq_cmaf_packager_init_segment(video_packager.get()); + tcfg.timescale = moq_cmaf_packager_timescale(video_packager.get()); + } else { + tcfg.init_data = {video_init_data.data(), video_init_data.size()}; + tcfg.timescale = VIDEO_TIMESCALE; + } + tcfg.is_live = true; tcfg.width = video_conf.video_width; tcfg.height = video_conf.video_height; @@ -181,6 +235,7 @@ moq_media_track_t *MOQOutput::CreateVideoTrackFromPacket(moq_media_sender_t *cur moq_media_track_t *new_track = CreateVideoTrack(cur_sender); if (!new_track) { blog(LOG_WARNING, "[obs-moq] failed to create video track from first frame"); + video_packager.reset(); return nullptr; } @@ -195,11 +250,20 @@ moq_media_track_t *MOQOutput::CreateAudioTrack(moq_media_sender_t *new_sender) moq_media_track_cfg_init(&tcfg); tcfg.name = {(const uint8_t *)"audio", 5}; tcfg.media_type = MOQ_MEDIA_TYPE_AUDIO; - tcfg.packaging = MOQ_MEDIA_PACKAGING_RAW; + tcfg.packaging = cmaf_enabled ? MOQ_MEDIA_PACKAGING_CMAF : MOQ_MEDIA_PACKAGING_RAW; tcfg.codec = {(const uint8_t *)audio_codec.c_str(), audio_codec.size()}; tcfg.samplerate = audio_conf.samplerate; tcfg.channel_config = {(const uint8_t *)audio_conf.channels.c_str(), audio_conf.channels.size()}; tcfg.bitrate = audio_conf.bitrate; + + if (cmaf_enabled) { + if (!InitCMAFAudioPackager()) + return nullptr; + + tcfg.init_data = moq_cmaf_packager_init_segment(audio_packager.get()); + tcfg.timescale = moq_cmaf_packager_timescale(audio_packager.get()); + } + moq_media_track_t *new_track = nullptr; moq_result_t result = moq_media_sender_add_track(new_sender, &tcfg, &new_track); if (result != MOQ_OK) { @@ -366,6 +430,9 @@ void MOQOutput::Stop(bool signal) moq_media_sender_destroy(doomed); } + video_packager.reset(); + audio_packager.reset(); + if (signal) { obs_output_signal_stop(output, OBS_OUTPUT_SUCCESS); } @@ -375,57 +442,65 @@ void MOQOutput::Stop(bool signal) start_time_ns = os_gettime_ns(); } -void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t **track, bool is_sync, - bool starts_group, bool ends_group) +void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t **track, bool is_sync, bool starts_group, + bool ends_group) { - moq_rcbuf_t *payload = nullptr; - // moq_rcbuf_create will copy the data into a new rcbuf, and increment the refcount. We will need to decref it after sending, or if we don't send it. - if (moq_rcbuf_create(moq_alloc_default(), packet->data, packet->size, &payload) != MOQ_OK) { - blog(LOG_WARNING, "[obs-moq] rcbuf alloc failed"); - return; - } - - uint64_t pts_usec = 0; - pts_usec = util_mul_div64((uint64_t)packet->pts, 1000000ull * (uint64_t)packet->timebase_num, - (uint64_t)packet->timebase_den); - moq_media_send_object_t obj = {}; obj.struct_size = sizeof(obj); - obj.payload = payload; obj.properties = nullptr; obj.is_sync = is_sync; obj.starts_group = starts_group; obj.ends_group = ends_group; - obj.presentation_time_us = pts_usec; + obj.presentation_time_us = util_mul_div64((uint64_t)packet->pts, 1000000ull * (uint64_t)packet->timebase_num, + (uint64_t)packet->timebase_den); obj.decode_time_us = (uint64_t)packet->dts_usec; - moq_result_t res; + if (cmaf_enabled && starts_group) { + obj.has_sap_type = true; + obj.sap_type = MOQ_SAP_TYPE_1; + } + + size_t sent_size = 0; { std::lock_guard lock(sender_mutex); - if (!sender) { - moq_rcbuf_decref(payload); + if (!sender) return; - } - if (!*track && packet->type == OBS_ENCODER_VIDEO && packet->keyframe) { + if (!*track && packet->type == OBS_ENCODER_VIDEO && packet->keyframe) *track = CreateVideoTrackFromPacket(sender, packet); - } - if (!*track) { - moq_rcbuf_decref(payload); + if (!*track) return; + + moq_bytes_t fragment = {}; + if (cmaf_enabled) { + moq_cmaf_packager_t *packager = (packet->type == OBS_ENCODER_VIDEO) ? video_packager.get() + : audio_packager.get(); + if (!package_packet(packager, packet, &fragment)) { + blog(LOG_WARNING, "[obs-moq] dropping a packet that could not be packaged"); + return; + } + } else { + fragment = {packet->data, packet->size}; } - res = moq_media_sender_write(sender, *track, &obj); - } + moq_rcbuf_t *payload = nullptr; + if (moq_rcbuf_create(moq_alloc_default(), fragment.data, fragment.len, &payload) != MOQ_OK) { + blog(LOG_WARNING, "[obs-moq] rcbuf alloc failed"); + return; + } + obj.payload = payload; - if (res != MOQ_OK) { - moq_rcbuf_decref(payload); - return; + /* Ownership transfers only on success. */ + if (moq_media_sender_write(sender, *track, &obj) != MOQ_OK) { + moq_rcbuf_decref(payload); + return; + } + sent_size = fragment.len; } - total_bytes_sent.fetch_add(packet->size); + total_bytes_sent.fetch_add(sent_size); } void MOQOutput::Data(struct encoder_packet *packet) diff --git a/src/moq-output.h b/src/moq-output.h index e95329e..7553c4e 100644 --- a/src/moq-output.h +++ b/src/moq-output.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,6 +12,13 @@ #include #include #include +#include + +struct CMAFPackagerDeleter { + void operator()(moq_cmaf_packager_t *p) const { moq_cmaf_packager_destroy(p); } +}; + +using CMAFPackagerPtr = std::unique_ptr; struct video_config { uint32_t video_width; @@ -22,6 +30,7 @@ struct video_config { struct audio_config { uint32_t samplerate; + uint32_t channel_count; std::string channels; uint64_t bitrate; }; @@ -44,6 +53,8 @@ class MOQOutput { void SplitNamespace(); bool LoadVideoEncoderSettings(); bool LoadAudioEncoderSettings(); + bool InitCMAFVideoPackager(); + bool InitCMAFAudioPackager(); moq_media_track_t *CreateVideoTrack(moq_media_sender_t *new_sender); moq_media_track_t *CreateVideoTrackFromPacket(moq_media_sender_t *cur_sender, struct encoder_packet *packet); moq_media_track_t *CreateAudioTrack(moq_media_sender_t *new_sender); @@ -88,6 +99,12 @@ class MOQOutput { moq_media_sender_t *sender = nullptr; moq_media_track_t *video_track = nullptr; moq_media_track_t *audio_track = nullptr; + + // todo: make this configurable + bool cmaf_enabled = true; + + CMAFPackagerPtr video_packager; + CMAFPackagerPtr audio_packager; }; void register_moq_output(); \ No newline at end of file