diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fdc58f..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) + +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) @@ -43,6 +47,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/obs-moq.cpp 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/codec-signaling.h b/src/codec-signaling.h new file mode 100644 index 0000000..5d4ee43 --- /dev/null +++ b/src/codec-signaling.h @@ -0,0 +1,87 @@ +#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 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}; + + +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, "av1") == 0) + return &kCodecAv1; + if (strcmp(codec, "aac") == 0) + return &kCodecAac; + if (strcmp(codec, "opus") == 0) + return &kCodecOpus; + 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-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 2b0464f..7f5cb14 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -1,11 +1,14 @@ #include "moq-output.h" -#include "utils.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,13 +63,11 @@ 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 = 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; } @@ -85,22 +84,64 @@ 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; obs_encoder_get_extra_data(aenc, &extra, &extra_size); + audio_init_data = BuildInitData(codec, extra, extra_size); + audio_codec = BuildCodecString(codec, audio_init_data); - if (codec && strcmp(codec, "opus") == 0) { - audio_init_data.clear(); - audio_codec = "opus"; - } else { - audio_init_data.assign(extra, extra + extra_size); - audio_codec = AacCodecString(audio_init_data); + 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; } @@ -149,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; @@ -172,17 +221,21 @@ 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) { blog(LOG_WARNING, "[obs-moq] failed to create video track from first frame"); + video_packager.reset(); return nullptr; } @@ -197,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) { @@ -368,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); } @@ -381,53 +446,61 @@ void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t **tr 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) @@ -473,13 +546,10 @@ 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 hevc and av1 - info.encoded_video_codecs = "h264"; - // todo: add support for opus and ac3 - info.encoded_audio_codecs = "aac,opus"; + info.encoded_video_codecs = "h264;hevc;av1"; + info.encoded_audio_codecs = "aac;opus"; 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 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 diff --git a/src/moq-service.cpp b/src/moq-service.cpp index 5e4bbdd..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", nullptr}; +const char *video_codecs[] = {"h264", "hevc", "av1", nullptr}; MOQService::MOQService(obs_data_t *settings, obs_service_t *service) { @@ -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); 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; -}