From d08a35fe30ddc2ebdaee18403fcc104d1eb41eab Mon Sep 17 00:00:00 2001 From: Joaquin Bartaburu Date: Tue, 21 Jul 2026 15:24:30 -0300 Subject: [PATCH 1/2] feat: add AAC audio support --- src/moq-output.cpp | 138 +++++++++++++++++++++++++++++++++----------- src/moq-output.h | 20 ++++++- src/moq-service.cpp | 15 ++--- src/utils.h | 17 ++++++ 4 files changed, 142 insertions(+), 48 deletions(-) diff --git a/src/moq-output.cpp b/src/moq-output.cpp index e614b8d..c74f8b6 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -67,11 +67,38 @@ bool MOQOutput::LoadVideoEncoderSettings() size_t extra_size = 0; obs_encoder_get_extra_data(venc, &extra, &extra_size); - init_data = AnnexBToAvcC(extra, extra_size); - codec_string = AvcCodecString(init_data); + video_init_data = AnnexBToAvcC(extra, extra_size); + video_codec = AvcCodecString(video_init_data); return true; } +bool MOQOutput::LoadAudioEncoderSettings() +{ + obs_encoder_t *aenc = obs_output_get_audio_encoder(output, 0); + if (!aenc) { + blog(LOG_WARNING, "[obs-moq] no audio encoder assigned"); + obs_output_set_last_error(output, obs_module_text("Error.NoAudioEncoder")); + return false; + } + + OBSDataAutoRelease settings = obs_encoder_get_settings(aenc); + 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)); + + const char *codec = obs_encoder_get_codec(aenc); + //todo: add codec validation here + + 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); + + return true; +} + bool MOQOutput::ResolveServiceConfig() { url.clear(); @@ -111,7 +138,7 @@ bool MOQOutput::ResolveServiceConfig() return true; } -bool MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) +moq_media_track_t *MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) { moq_media_track_cfg_t tcfg; moq_media_track_cfg_init(&tcfg); @@ -119,10 +146,10 @@ bool MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) tcfg.media_type = MOQ_MEDIA_TYPE_VIDEO; // todo: make this configurable and add CMAF support tcfg.packaging = MOQ_MEDIA_PACKAGING_RAW; - tcfg.codec = {(const uint8_t *)codec_string.c_str(), codec_string.size()}; + 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 = {init_data.data(), init_data.size()}; + tcfg.init_data = {video_init_data.data(), video_init_data.size()}; tcfg.is_live = true; tcfg.width = video_conf.video_width; tcfg.height = video_conf.video_height; @@ -132,18 +159,29 @@ bool MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) 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) { - blog(LOG_WARNING, "[obs-moq] moq_media_sender_add_track failed"); - obs_output_signal_stop(output, OBS_OUTPUT_ERROR); - return false; + return nullptr; } - { - std::lock_guard lock(sender_mutex); - sender = new_sender; - video_track = new_track; - } + return new_track; +} - return true; +moq_media_track_t *MOQOutput::CreateAudioTrack(moq_media_sender_t *new_sender) +{ + moq_media_track_cfg_t tcfg; + 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.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; + 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) { + return nullptr; + } + return new_track; } void MOQOutput::OnReady(void *ctx, moq_media_sender_t *sender) @@ -218,12 +256,29 @@ bool MOQOutput::Connect() obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } - - if (!CreateVideoTrack(media_sender)) { + + 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_audio_track = CreateAudioTrack(media_sender); + if (!new_audio_track) { + blog(LOG_WARNING, "[obs-moq] failed to create audio track"); + moq_media_sender_destroy(media_sender); + obs_output_signal_stop(output, OBS_OUTPUT_ERROR); return false; } + + { + std::lock_guard lock(sender_mutex); + sender = media_sender; + video_track = new_video_track; + audio_track = new_audio_track; + } return true; } @@ -278,6 +333,7 @@ void MOQOutput::Stop(bool signal) doomed = sender; sender = nullptr; video_track = nullptr; + audio_track = nullptr; } if (doomed) { moq_media_sender_destroy(doomed); @@ -292,17 +348,8 @@ void MOQOutput::Stop(bool signal) start_time_ns = os_gettime_ns(); } -void MOQOutput::Data(struct encoder_packet *packet) +void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t *track, bool is_sync, bool starts_group, bool ends_group) { - if (!packet) { - Stop(false); - obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR); - return; - } - - if (!running.load() || packet->type != OBS_ENCODER_VIDEO) { - return; - } 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. @@ -319,21 +366,21 @@ void MOQOutput::Data(struct encoder_packet *packet) obj.struct_size = sizeof(obj); obj.payload = payload; obj.properties = nullptr; - obj.is_sync = packet->keyframe; - obj.starts_group = packet->keyframe; - obj.ends_group = false; + obj.is_sync = is_sync; + obj.starts_group = starts_group; + obj.ends_group = ends_group; obj.presentation_time_us = pts_usec; obj.decode_time_us = (uint64_t)packet->dts_usec; moq_result_t res; { std::lock_guard lock(sender_mutex); - if (!sender || !video_track) { + if (!sender || !track) { // release the rcbuf since we won't be sending it moq_rcbuf_decref(payload); return; } - res = moq_media_sender_write(sender, video_track, &obj); + res = moq_media_sender_write(sender, track, &obj); } if (res != MOQ_OK) { @@ -342,6 +389,29 @@ void MOQOutput::Data(struct encoder_packet *packet) } total_bytes_sent.fetch_add(packet->size); + +} + +void MOQOutput::Data(struct encoder_packet *packet) +{ + if (!packet) { + Stop(false); + obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR); + return; + } + + if (!running.load()) { + return; + } + if (packet->type == OBS_ENCODER_VIDEO) + { + SendPacket(packet, video_track, packet->keyframe, packet->keyframe, false); + } + if (packet->type == OBS_ENCODER_AUDIO) + { + SendPacket(packet, audio_track, true, true, true); + } + } void MOQOutput::StartThread() @@ -353,8 +423,8 @@ void MOQOutput::StartThread() return; } - if (!LoadVideoEncoderSettings()) { - blog(LOG_WARNING, "[obs-moq] failed to configure video track"); + if (!LoadVideoEncoderSettings() || !LoadAudioEncoderSettings()) { + blog(LOG_WARNING, "[obs-moq] failed to configure video or audio track"); return; } @@ -369,7 +439,7 @@ 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_VIDEO | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE; + 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"; diff --git a/src/moq-output.h b/src/moq-output.h index 3e558e9..f700ab2 100644 --- a/src/moq-output.h +++ b/src/moq-output.h @@ -20,6 +20,12 @@ struct video_config { uint64_t bitrate; }; +struct audio_config { + uint32_t samplerate; + std::string channels; + uint64_t bitrate; +}; + class MOQOutput { public: MOQOutput(obs_data_t *settings, obs_output_t *output); @@ -37,7 +43,10 @@ class MOQOutput { void StartThread(); void SplitNamespace(); bool LoadVideoEncoderSettings(); - bool CreateVideoTrack(moq_media_sender_t *new_sender); + bool LoadAudioEncoderSettings(); + moq_media_track_t *CreateVideoTrack(moq_media_sender_t *new_sender); + 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, bool ends_group); bool ResolveServiceConfig(); bool Connect(); @@ -60,9 +69,13 @@ class MOQOutput { std::atomic got_ready; video_config video_conf; + audio_config audio_conf; - std::vector init_data; - std::string codec_string; + std::vector video_init_data; + std::string video_codec; + + std::vector audio_init_data; + std::string audio_codec; std::string url; moq_namespace_t namespace_val; @@ -72,6 +85,7 @@ class MOQOutput { moq_media_sender_t *sender = nullptr; moq_media_track_t *video_track = nullptr; + moq_media_track_t *audio_track = nullptr; }; void register_moq_output(); \ No newline at end of file diff --git a/src/moq-service.cpp b/src/moq-service.cpp index a3d9290..4bbce9f 100644 --- a/src/moq-service.cpp +++ b/src/moq-service.cpp @@ -1,6 +1,6 @@ #include "moq-service.h" -const char *audio_codecs[] = {nullptr}; +const char *audio_codecs[] = {"aac", nullptr}; const char *video_codecs[] = {"h264", nullptr}; MOQService::MOQService(obs_data_t *settings, obs_service_t *service) @@ -86,19 +86,12 @@ void register_moq_service() info.apply_encoder_settings = [](void *, obs_data_t *video_settings, obs_data_t *audio_settings) { MOQService::ApplyEncoderSettings(video_settings, audio_settings); }; - info.get_supported_video_codecs = [](void *) -> const char ** { return video_codecs; }; - // info.get_supported_audio_codecs = [](void *) -> const char ** { - // return audio_codecs; - // }; - - // TODO: Check - // info.get_defaults = [](obs_data_t *settings) { - // obs_data_set_default_bool(settings, "insecure_skip_verify", true); - // }; - + info.get_supported_audio_codecs = [](void *) -> const char ** { + return audio_codecs; + }; info.can_try_to_connect = [](void *priv_data) -> bool { return static_cast(priv_data)->CanTryToConnect(); }; diff --git a/src/utils.h b/src/utils.h index 06dcfd4..af54ed9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include static std::string AvcCodecString(const std::vector &avcc) @@ -30,3 +31,19 @@ static std::vector AnnexBToAvcC(const uint8_t *extra, size_t 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 ea5fb597e3aebebc81c4fd57938a017ece6a94bf Mon Sep 17 00:00:00 2001 From: Joaquin Bartaburu Date: Tue, 21 Jul 2026 16:48:04 -0300 Subject: [PATCH 2/2] style: fix format --- src/moq-output.cpp | 43 ++++++++++++++++++++----------------------- src/moq-output.h | 11 ++++++----- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/moq-output.cpp b/src/moq-output.cpp index c74f8b6..4040dda 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -72,17 +72,17 @@ bool MOQOutput::LoadVideoEncoderSettings() return true; } -bool MOQOutput::LoadAudioEncoderSettings() +bool MOQOutput::LoadAudioEncoderSettings() { - obs_encoder_t *aenc = obs_output_get_audio_encoder(output, 0); - if (!aenc) { - blog(LOG_WARNING, "[obs-moq] no audio encoder assigned"); - obs_output_set_last_error(output, obs_module_text("Error.NoAudioEncoder")); - return false; - } - - OBSDataAutoRelease settings = obs_encoder_get_settings(aenc); - audio_conf.bitrate = (uint64_t)obs_data_get_int(settings, "bitrate") * 1000; + obs_encoder_t *aenc = obs_output_get_audio_encoder(output, 0); + if (!aenc) { + blog(LOG_WARNING, "[obs-moq] no audio encoder assigned"); + obs_output_set_last_error(output, obs_module_text("Error.NoAudioEncoder")); + return false; + } + + OBSDataAutoRelease settings = obs_encoder_get_settings(aenc); + 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)); @@ -96,7 +96,7 @@ bool MOQOutput::LoadAudioEncoderSettings() audio_init_data.assign(extra, extra + extra_size); audio_codec = AacCodecString(audio_init_data); - return true; + return true; } bool MOQOutput::ResolveServiceConfig() @@ -165,7 +165,7 @@ moq_media_track_t *MOQOutput::CreateVideoTrack(moq_media_sender_t *new_sender) return new_track; } -moq_media_track_t *MOQOutput::CreateAudioTrack(moq_media_sender_t *new_sender) +moq_media_track_t *MOQOutput::CreateAudioTrack(moq_media_sender_t *new_sender) { moq_media_track_cfg_t tcfg; moq_media_track_cfg_init(&tcfg); @@ -256,7 +256,7 @@ bool MOQOutput::Connect() obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); 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"); @@ -272,7 +272,7 @@ bool MOQOutput::Connect() obs_output_signal_stop(output, OBS_OUTPUT_ERROR); return false; } - + { std::lock_guard lock(sender_mutex); sender = media_sender; @@ -348,7 +348,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; @@ -389,7 +390,6 @@ void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t *tra } total_bytes_sent.fetch_add(packet->size); - } void MOQOutput::Data(struct encoder_packet *packet) @@ -403,15 +403,12 @@ void MOQOutput::Data(struct encoder_packet *packet) if (!running.load()) { return; } - if (packet->type == OBS_ENCODER_VIDEO) - { + if (packet->type == OBS_ENCODER_VIDEO) { SendPacket(packet, video_track, packet->keyframe, packet->keyframe, false); } - if (packet->type == OBS_ENCODER_AUDIO) - { + if (packet->type == OBS_ENCODER_AUDIO) { SendPacket(packet, audio_track, true, true, true); } - } void MOQOutput::StartThread() @@ -443,8 +440,8 @@ void register_moq_output() info.protocols = "MOQ"; // todo: add support for hevc and av1 info.encoded_video_codecs = "h264"; - // todo: add support for audio - // info.encoded_audio_codecs = "aac;opus"; + // todo: add support for opus and ac3 + info.encoded_audio_codecs = "aac"; info.get_name = [](void *) -> const char * { return obs_module_text("Output.Name"); diff --git a/src/moq-output.h b/src/moq-output.h index f700ab2..ff35928 100644 --- a/src/moq-output.h +++ b/src/moq-output.h @@ -21,9 +21,9 @@ struct video_config { }; struct audio_config { - uint32_t samplerate; - std::string channels; - uint64_t bitrate; + uint32_t samplerate; + std::string channels; + uint64_t bitrate; }; class MOQOutput { @@ -46,7 +46,8 @@ class MOQOutput { bool LoadAudioEncoderSettings(); moq_media_track_t *CreateVideoTrack(moq_media_sender_t *new_sender); 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, bool ends_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(); @@ -73,7 +74,7 @@ class MOQOutput { std::vector video_init_data; std::string video_codec; - + std::vector audio_init_data; std::string audio_codec;