From 0357d8bb17e5961cb0e7cb039ca1a0fa14d52985 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 07:15:02 -0600 Subject: [PATCH 01/12] Add hang_source: MoQ video source plugin Implement hang_source, a new OBS source that consumes MoQ streams from a relay using the libmoq library. Features: - Configurable URL and BROADCAST fields in OBS source properties - H.264 decoder-safe: waits for keyframes with SPS/PPS before decoding - Realtime-first: drops stale frames to minimize latency - Clean session handling: fully destroys and recreates context on URL/broadcast changes to prevent stale frame processing Files added: - src/hang-source.c/cpp/h: Core source implementation Files modified: - CMakeLists.txt: Build integration for hang_source - src/logger.h: Enhanced logging support - src/moq-output.cpp/h: Output module updates - src/obs-moq.cpp: Plugin registration --- CMakeLists.txt | 8 + src/hang-source.c | 519 +++++++++++++++++++++++++++++++++++++++++++ src/hang-source.cpp | 523 ++++++++++++++++++++++++++++++++++++++++++++ src/hang-source.h | 3 + src/logger.h | 12 +- src/moq-output.cpp | 63 ++++-- src/moq-output.h | 1 + src/obs-moq.cpp | 4 +- 8 files changed, 1103 insertions(+), 30 deletions(-) create mode 100644 src/hang-source.c create mode 100644 src/hang-source.cpp create mode 100644 src/hang-source.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f50200..e22c39e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,9 +58,17 @@ if(ENABLE_QT) ) endif() +# FFmpeg dependency +include(FindPkgConfig) +pkg_check_modules(FFMPEG REQUIRED libavcodec libavutil libswscale libswresample) +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${FFMPEG_INCLUDE_DIRS}) +target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${FFMPEG_LIBRARY_DIRS}) +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${FFMPEG_LIBRARIES}) + target_sources( ${CMAKE_PROJECT_NAME} PRIVATE src/obs-moq.cpp src/moq-output.h src/moq-service.h src/moq-output.cpp src/moq-service.cpp + src/hang-source.cpp src/hang-source.h ) set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) diff --git a/src/hang-source.c b/src/hang-source.c new file mode 100644 index 0000000..d2e4db1 --- /dev/null +++ b/src/hang-source.c @@ -0,0 +1,519 @@ +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include "moq.h" +} + +#include "hang-source.h" +#include "logger.h" + +#define FRAME_WIDTH 1920 +#define FRAME_HEIGHT 1080 + +struct hang_source { + obs_source_t *source; + + // Settings + char *url; + char *broadcast; + + // Session handles (all negative = invalid) + volatile uint32_t generation; // Increments on reconnect + int32_t origin; + int32_t session; + int32_t consume; + int32_t catalog_handle; + int32_t video_track; + + // Decoder state + AVCodecContext *codec_ctx; + struct SwsContext *sws_ctx; + bool got_keyframe; + + // Output frame buffer + struct obs_source_frame frame; + uint8_t *frame_buffer; + + // Threading + pthread_mutex_t mutex; +}; + +// Forward declarations +static void hang_source_update(void *data, obs_data_t *settings); +static void hang_source_destroy(void *data); +static void hang_source_tick(void *data, float seconds); +static void hang_source_render(void *data, gs_effect_t *effect); +static obs_properties_t *hang_source_properties(void *data); +static void hang_source_get_defaults(obs_data_t *settings); + +// MoQ callbacks +static void on_session_status(void *user_data, int32_t code); +static void on_catalog(void *user_data, int32_t catalog); +static void on_video_frame(void *user_data, int32_t frame_id); + +// Helper functions +static void hang_source_reconnect(struct hang_source *ctx); +static void hang_source_disconnect(struct hang_source *ctx); +static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); +static void hang_source_destroy_decoder(struct hang_source *ctx); +static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); + +static void *hang_source_create(obs_data_t *settings, obs_source_t *source) +{ + struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); + ctx->source = source; + + // Initialize handles to invalid values + ctx->generation = 0; + ctx->origin = -1; + ctx->session = -1; + ctx->consume = -1; + ctx->catalog_handle = -1; + ctx->video_track = -1; + + // Initialize decoder state + ctx->codec_ctx = NULL; + ctx->sws_ctx = NULL; + ctx->got_keyframe = false; + ctx->frame_buffer = NULL; + + // Initialize threading + pthread_mutex_init(&ctx->mutex, NULL); + + // Initialize OBS frame structure + ctx->frame.width = FRAME_WIDTH; + ctx->frame.height = FRAME_HEIGHT; + ctx->frame.format = VIDEO_FORMAT_RGBA; + ctx->frame.linesize[0] = FRAME_WIDTH * 4; + + hang_source_update(ctx, settings); + + return ctx; +} + +static void hang_source_destroy(void *data) +{ + struct hang_source *ctx = (struct hang_source *)data; + + hang_source_disconnect(ctx); + hang_source_destroy_decoder(ctx); + + bfree(ctx->url); + bfree(ctx->broadcast); + bfree(ctx->frame_buffer); + + pthread_mutex_destroy(&ctx->mutex); + + bfree(ctx); +} + +static void hang_source_update(void *data, obs_data_t *settings) +{ + struct hang_source *ctx = (struct hang_source *)data; + + const char *url = obs_data_get_string(settings, "url"); + const char *broadcast = obs_data_get_string(settings, "broadcast"); + + bool changed = false; + + if (!ctx->url || strcmp(ctx->url, url) != 0) { + bfree(ctx->url); + ctx->url = bstrdup(url); + changed = true; + } + + if (!ctx->broadcast || strcmp(ctx->broadcast, broadcast) != 0) { + bfree(ctx->broadcast); + ctx->broadcast = bstrdup(broadcast); + changed = true; + } + + if (changed && ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0) { + hang_source_reconnect(ctx); + } +} + +static void hang_source_get_defaults(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "url", "https://attention.us-central-2.ooda.video:4443"); + obs_data_set_default_string(settings, "broadcast", "flyover-ranch/cam_192_168_42_190"); +} + +static obs_properties_t *hang_source_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_text(props, "url", "URL", OBS_TEXT_DEFAULT); + obs_properties_add_text(props, "broadcast", "Broadcast", OBS_TEXT_DEFAULT); + + return props; +} + +static void hang_source_tick(void *data, float seconds) +{ + UNUSED_PARAMETER(data); + UNUSED_PARAMETER(seconds); + // No per-frame updates needed +} + +static void hang_source_render(void *data, gs_effect_t *effect) +{ + UNUSED_PARAMETER(effect); + + struct hang_source *ctx = (struct hang_source *)data; + + if (!ctx->frame_buffer) + return; + + obs_source_draw(ctx->source, ctx->frame_buffer, FRAME_WIDTH * 4, FRAME_HEIGHT, false); +} + +// MoQ callback implementations +static void on_session_status(void *user_data, int32_t code) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + if (code == 0) { + LOG_INFO("MoQ session connected successfully"); + } else { + LOG_ERROR("MoQ session failed with code: %d", code); + } +} + +static void on_catalog(void *user_data, int32_t catalog) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + pthread_mutex_lock(&ctx->mutex); + + // Check if this is still the current generation + uint32_t current_gen = ctx->generation; + pthread_mutex_unlock(&ctx->mutex); + + if (catalog < 0) { + LOG_ERROR("Failed to get catalog: %d", catalog); + return; + } + + // Get video configuration + struct moq_video_config video_config; + if (moq_consume_video_config(catalog, 0, &video_config) < 0) { + LOG_ERROR("Failed to get video config"); + moq_consume_catalog_close(catalog); + return; + } + + // Initialize decoder with the video config + if (!hang_source_init_decoder(ctx, &video_config)) { + LOG_ERROR("Failed to initialize decoder"); + moq_consume_catalog_close(catalog); + return; + } + + // Subscribe to video track with minimal buffering + int32_t track = moq_consume_video_track(ctx->consume, 0, 0, on_video_frame, ctx); + if (track < 0) { + LOG_ERROR("Failed to subscribe to video track: %d", track); + moq_consume_catalog_close(catalog); + return; + } + + pthread_mutex_lock(&ctx->mutex); + if (ctx->generation == current_gen) { + ctx->video_track = track; + ctx->catalog_handle = catalog; + } + pthread_mutex_unlock(&ctx->mutex); + + LOG_INFO("Subscribed to video track successfully"); +} + +static void on_video_frame(void *user_data, int32_t frame_id) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + if (frame_id < 0) { + // End of stream or error + return; + } + + hang_source_decode_frame(ctx, frame_id); +} + +// Helper function implementations +static void hang_source_reconnect(struct hang_source *ctx) +{ + // Increment generation to invalidate old callbacks + pthread_mutex_lock(&ctx->mutex); + ctx->generation++; + uint32_t current_gen = ctx->generation; + hang_source_disconnect(ctx); + pthread_mutex_unlock(&ctx->mutex); + + // Create origin for consuming + ctx->origin = moq_origin_create(); + if (ctx->origin < 0) { + LOG_ERROR("Failed to create origin: %d", ctx->origin); + return; + } + + // Connect to MoQ server + ctx->session = moq_session_connect( + ctx->url, strlen(ctx->url), + 0, // origin_publish + ctx->origin, // origin_consume + on_session_status, ctx + ); + + if (ctx->session < 0) { + LOG_ERROR("Failed to connect to MoQ server: %d", ctx->session); + return; + } + + // Consume broadcast by path + ctx->consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); + if (ctx->consume < 0) { + LOG_ERROR("Failed to consume broadcast: %d", ctx->consume); + return; + } + + // Subscribe to catalog updates + int32_t catalog = moq_consume_catalog(ctx->consume, on_catalog, ctx); + if (catalog < 0) { + LOG_ERROR("Failed to subscribe to catalog: %d", catalog); + return; + } + + LOG_INFO("Connecting to MoQ broadcast: %s @ %s", ctx->broadcast, ctx->url); +} + +static void hang_source_disconnect(struct hang_source *ctx) +{ + if (ctx->video_track >= 0) { + moq_consume_video_track_close(ctx->video_track); + ctx->video_track = -1; + } + + if (ctx->catalog_handle >= 0) { + moq_consume_catalog_close(ctx->catalog_handle); + ctx->catalog_handle = -1; + } + + if (ctx->consume >= 0) { + moq_consume_close(ctx->consume); + ctx->consume = -1; + } + + if (ctx->session >= 0) { + moq_session_close(ctx->session); + ctx->session = -1; + } + + if (ctx->origin >= 0) { + moq_origin_close(ctx->origin); + ctx->origin = -1; + } + + hang_source_destroy_decoder(ctx); + ctx->got_keyframe = false; +} + +static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) +{ + hang_source_destroy_decoder(ctx); + + // Find H.264 decoder + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOG_ERROR("H.264 codec not found"); + return false; + } + + // Create codec context + ctx->codec_ctx = avcodec_alloc_context3(codec); + if (!ctx->codec_ctx) { + LOG_ERROR("Failed to allocate codec context"); + return false; + } + + // Set codec parameters from config + if (config->coded_width && *config->coded_width > 0) { + ctx->codec_ctx->width = *config->coded_width; + ctx->frame.width = *config->coded_width; + } + if (config->coded_height && *config->coded_height > 0) { + ctx->codec_ctx->height = *config->coded_height; + ctx->frame.height = *config->coded_height; + } + + // Use codec description as extradata (contains SPS/PPS) + if (config->description && config->description_len > 0) { + ctx->codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); + if (ctx->codec_ctx->extradata) { + memcpy(ctx->codec_ctx->extradata, config->description, config->description_len); + ctx->codec_ctx->extradata_size = config->description_len; + } + } + + // Open codec + if (avcodec_open2(ctx->codec_ctx, codec, NULL) < 0) { + LOG_ERROR("Failed to open codec"); + hang_source_destroy_decoder(ctx); + return false; + } + + // Allocate frame buffer (RGBA for OBS) + ctx->frame.linesize[0] = ctx->frame.width * 4; + size_t buffer_size = ctx->frame.width * ctx->frame.height * 4; + ctx->frame_buffer = (uint8_t *)bmalloc(buffer_size); + + // Create scaling context for YUV420P -> RGBA conversion + ctx->sws_ctx = sws_getContext( + ctx->frame.width, ctx->frame.height, AV_PIX_FMT_YUV420P, + ctx->frame.width, ctx->frame.height, AV_PIX_FMT_RGBA, + SWS_BILINEAR, NULL, NULL, NULL + ); + + if (!ctx->sws_ctx) { + LOG_ERROR("Failed to create scaling context"); + hang_source_destroy_decoder(ctx); + return false; + } + + ctx->frame.format = VIDEO_FORMAT_RGBA; + ctx->frame.timestamp = 0; + + LOG_INFO("Decoder initialized: %dx%d", ctx->frame.width, ctx->frame.height); + return true; +} + +static void hang_source_destroy_decoder(struct hang_source *ctx) +{ + if (ctx->sws_ctx) { + sws_freeContext(ctx->sws_ctx); + ctx->sws_ctx = NULL; + } + + if (ctx->codec_ctx) { + avcodec_free_context(&ctx->codec_ctx); + ctx->codec_ctx = NULL; + } + + if (ctx->frame_buffer) { + bfree(ctx->frame_buffer); + ctx->frame_buffer = NULL; + } +} + +static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) +{ + if (!ctx->codec_ctx) + return; + + // Get frame data + struct moq_frame frame_data; + if (moq_consume_frame_chunk(frame_id, 0, &frame_data) < 0) { + LOG_ERROR("Failed to get frame data"); + moq_consume_frame_close(frame_id); + return; + } + + // Skip non-keyframes until we get the first one + if (!ctx->got_keyframe && !frame_data.keyframe) { + moq_consume_frame_close(frame_id); + return; + } + + // Create AVPacket from frame data + AVPacket *packet = av_packet_alloc(); + if (!packet) { + moq_consume_frame_close(frame_id); + return; + } + + packet->data = (uint8_t *)frame_data.payload; + packet->size = frame_data.payload_size; + packet->pts = frame_data.timestamp_us / 1000; // Convert to milliseconds + packet->dts = packet->pts; + + // Send packet to decoder + int ret = avcodec_send_packet(ctx->codec_ctx, packet); + av_packet_free(&packet); + + if (ret < 0) { + if (ret != AVERROR(EAGAIN)) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ret, errbuf, sizeof(errbuf)); + LOG_ERROR("Error sending packet to decoder: %s", errbuf); + } + moq_consume_frame_close(frame_id); + return; + } + + // Receive decoded frames + AVFrame *frame = av_frame_alloc(); + if (!frame) { + moq_consume_frame_close(frame_id); + return; + } + + ret = avcodec_receive_frame(ctx->codec_ctx, frame); + if (ret < 0) { + if (ret != AVERROR(EAGAIN)) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ret, errbuf, sizeof(errbuf)); + LOG_ERROR("Error receiving frame from decoder: %s", errbuf); + } + av_frame_free(&frame); + moq_consume_frame_close(frame_id); + return; + } + + // Mark that we've received a keyframe + if (frame->key_frame) + ctx->got_keyframe = true; + + // Convert YUV420P to RGBA + uint8_t *dst_data[4] = {ctx->frame_buffer, NULL, NULL, NULL}; + int dst_linesize[4] = {ctx->frame.width * 4, 0, 0, 0}; + + sws_scale(ctx->sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, + 0, ctx->frame.height, dst_data, dst_linesize); + + // Update OBS frame timestamp and output + ctx->frame.timestamp = frame_data.timestamp_us; + obs_source_output_video(ctx->source, &ctx->frame); + + av_frame_free(&frame); + moq_consume_frame_close(frame_id); +} + +// Registration function +void register_hang_source() +{ + struct obs_source_info info = {}; + info.id = "hang_source"; + info.type = OBS_SOURCE_TYPE_INPUT; + info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_DO_NOT_DUPLICATE; + info.get_name = [](void *) -> const char * { + return "Hang Source (MoQ)"; + }; + info.create = hang_source_create; + info.destroy = hang_source_destroy; + info.update = hang_source_update; + info.get_defaults = hang_source_get_defaults; + info.get_properties = hang_source_properties; + info.video_tick = hang_source_tick; + info.video_render = hang_source_render; + + obs_register_source(&info); +} diff --git a/src/hang-source.cpp b/src/hang-source.cpp new file mode 100644 index 0000000..a2d69a1 --- /dev/null +++ b/src/hang-source.cpp @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include "moq.h" +} + +#include "hang-source.h" +#include "logger.h" + +#define FRAME_WIDTH 1920 +#define FRAME_HEIGHT 1080 + +struct hang_source { + obs_source_t *source; + + // Settings + char *url; + char *broadcast; + + // Session handles (all negative = invalid) + volatile uint32_t generation; // Increments on reconnect + int32_t origin; + int32_t session; + int32_t consume; + int32_t catalog_handle; + int32_t video_track; + + // Decoder state + AVCodecContext *codec_ctx; + struct SwsContext *sws_ctx; + bool got_keyframe; + + // Output frame buffer + struct obs_source_frame frame; + uint8_t *frame_buffer; + + // Threading + pthread_mutex_t mutex; +}; + +// Forward declarations +static void hang_source_update(void *data, obs_data_t *settings); +static void hang_source_destroy(void *data); +static obs_properties_t *hang_source_properties(void *data); +static void hang_source_get_defaults(obs_data_t *settings); + +// MoQ callbacks +static void on_session_status(void *user_data, int32_t code); +static void on_catalog(void *user_data, int32_t catalog); +static void on_video_frame(void *user_data, int32_t frame_id); + +// Helper functions +static void hang_source_reconnect(struct hang_source *ctx); +static void hang_source_disconnect(struct hang_source *ctx); +static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); +static void hang_source_destroy_decoder(struct hang_source *ctx); +static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); + +static void *hang_source_create(obs_data_t *settings, obs_source_t *source) +{ + struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); + ctx->source = source; + + // Initialize handles to invalid values + ctx->generation = 0; + ctx->origin = -1; + ctx->session = -1; + ctx->consume = -1; + ctx->catalog_handle = -1; + ctx->video_track = -1; + + // Initialize decoder state + ctx->codec_ctx = NULL; + ctx->sws_ctx = NULL; + ctx->got_keyframe = false; + ctx->frame_buffer = NULL; + + // Initialize threading + pthread_mutex_init(&ctx->mutex, NULL); + + // Initialize OBS frame structure + ctx->frame.width = FRAME_WIDTH; + ctx->frame.height = FRAME_HEIGHT; + ctx->frame.format = VIDEO_FORMAT_RGBA; + ctx->frame.linesize[0] = FRAME_WIDTH * 4; + + hang_source_update(ctx, settings); + + return ctx; +} + +static void hang_source_destroy(void *data) +{ + struct hang_source *ctx = (struct hang_source *)data; + + hang_source_disconnect(ctx); + hang_source_destroy_decoder(ctx); + + bfree(ctx->url); + bfree(ctx->broadcast); + bfree(ctx->frame_buffer); + + pthread_mutex_destroy(&ctx->mutex); + + bfree(ctx); +} + +static void hang_source_update(void *data, obs_data_t *settings) +{ + struct hang_source *ctx = (struct hang_source *)data; + + const char *url = obs_data_get_string(settings, "url"); + const char *broadcast = obs_data_get_string(settings, "broadcast"); + + bool changed = false; + + if (!ctx->url || strcmp(ctx->url, url) != 0) { + bfree(ctx->url); + ctx->url = bstrdup(url); + changed = true; + } + + if (!ctx->broadcast || strcmp(ctx->broadcast, broadcast) != 0) { + bfree(ctx->broadcast); + ctx->broadcast = bstrdup(broadcast); + changed = true; + } + + if (changed && ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0) { + hang_source_reconnect(ctx); + } +} + +static void hang_source_get_defaults(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "url", "https://attention.us-central-2.ooda.video:4443"); + obs_data_set_default_string(settings, "broadcast", "flyover-ranch/cam_192_168_42_190"); +} + +static obs_properties_t *hang_source_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_text(props, "url", "URL", OBS_TEXT_DEFAULT); + obs_properties_add_text(props, "broadcast", "Broadcast", OBS_TEXT_DEFAULT); + + return props; +} + +// Note: We use OBS_SOURCE_ASYNC_VIDEO, so OBS handles rendering automatically +// via obs_source_output_video(). No video_tick or video_render callbacks needed. + +// Forward declaration for use in callback +static void hang_source_start_consume(struct hang_source *ctx); + +// MoQ callback implementations +static void on_session_status(void *user_data, int32_t code) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + if (code == 0) { + LOG_INFO("MoQ session connected successfully"); + // Now that we're connected, start consuming the broadcast + hang_source_start_consume(ctx); + } else { + LOG_ERROR("MoQ session failed with code: %d", code); + } +} + +static void on_catalog(void *user_data, int32_t catalog) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + LOG_INFO("Catalog callback received: %d", catalog); + + pthread_mutex_lock(&ctx->mutex); + + // Check if this is still the current generation + uint32_t current_gen = ctx->generation; + pthread_mutex_unlock(&ctx->mutex); + + if (catalog < 0) { + LOG_ERROR("Failed to get catalog: %d", catalog); + return; + } + + // Get video configuration + struct moq_video_config video_config; + if (moq_consume_video_config(catalog, 0, &video_config) < 0) { + LOG_ERROR("Failed to get video config"); + moq_consume_catalog_close(catalog); + return; + } + + // Initialize decoder with the video config + if (!hang_source_init_decoder(ctx, &video_config)) { + LOG_ERROR("Failed to initialize decoder"); + moq_consume_catalog_close(catalog); + return; + } + + // Subscribe to video track with minimal buffering + // Note: moq_consume_video_track takes the catalog handle, not the consume handle + int32_t track = moq_consume_video_track(catalog, 0, 0, on_video_frame, ctx); + if (track < 0) { + LOG_ERROR("Failed to subscribe to video track: %d", track); + moq_consume_catalog_close(catalog); + return; + } + + pthread_mutex_lock(&ctx->mutex); + if (ctx->generation == current_gen) { + ctx->video_track = track; + ctx->catalog_handle = catalog; + } + pthread_mutex_unlock(&ctx->mutex); + + LOG_INFO("Subscribed to video track successfully"); +} + +static void on_video_frame(void *user_data, int32_t frame_id) +{ + struct hang_source *ctx = (struct hang_source *)user_data; + + if (frame_id < 0) { + LOG_ERROR("Video frame callback with error: %d", frame_id); + return; + } + + LOG_DEBUG("Received video frame: %d", frame_id); + hang_source_decode_frame(ctx, frame_id); +} + +// Helper function implementations +static void hang_source_reconnect(struct hang_source *ctx) +{ + LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation, ctx->generation + 1); + + // Increment generation to invalidate old callbacks + pthread_mutex_lock(&ctx->mutex); + ctx->generation++; + hang_source_disconnect(ctx); + pthread_mutex_unlock(&ctx->mutex); + + // Create origin for consuming + ctx->origin = moq_origin_create(); + if (ctx->origin < 0) { + LOG_ERROR("Failed to create origin: %d", ctx->origin); + return; + } + + // Connect to MoQ server (consume will happen in on_session_status callback) + ctx->session = moq_session_connect( + ctx->url, strlen(ctx->url), + 0, // origin_publish + ctx->origin, // origin_consume + on_session_status, ctx + ); + + if (ctx->session < 0) { + LOG_ERROR("Failed to connect to MoQ server: %d", ctx->session); + return; + } + + LOG_INFO("Connecting to MoQ server: %s", ctx->url); +} + +// Called after session is connected successfully +static void hang_source_start_consume(struct hang_source *ctx) +{ + // Consume broadcast by path + ctx->consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); + if (ctx->consume < 0) { + LOG_ERROR("Failed to consume broadcast: %d", ctx->consume); + return; + } + + // Subscribe to catalog updates + int32_t catalog_handle = moq_consume_catalog(ctx->consume, on_catalog, ctx); + if (catalog_handle < 0) { + LOG_ERROR("Failed to subscribe to catalog: %d", catalog_handle); + return; + } + + LOG_INFO("Consuming broadcast: %s", ctx->broadcast); +} + +static void hang_source_disconnect(struct hang_source *ctx) +{ + if (ctx->video_track >= 0) { + moq_consume_video_track_close(ctx->video_track); + ctx->video_track = -1; + } + + if (ctx->catalog_handle >= 0) { + moq_consume_catalog_close(ctx->catalog_handle); + ctx->catalog_handle = -1; + } + + if (ctx->consume >= 0) { + moq_consume_close(ctx->consume); + ctx->consume = -1; + } + + if (ctx->session >= 0) { + moq_session_close(ctx->session); + ctx->session = -1; + } + + if (ctx->origin >= 0) { + moq_origin_close(ctx->origin); + ctx->origin = -1; + } + + hang_source_destroy_decoder(ctx); + ctx->got_keyframe = false; +} + +static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) +{ + hang_source_destroy_decoder(ctx); + + // Find H.264 decoder + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOG_ERROR("H.264 codec not found"); + return false; + } + + // Create codec context + ctx->codec_ctx = avcodec_alloc_context3(codec); + if (!ctx->codec_ctx) { + LOG_ERROR("Failed to allocate codec context"); + return false; + } + + // Set codec parameters from config + if (config->coded_width && *config->coded_width > 0) { + ctx->codec_ctx->width = *config->coded_width; + ctx->frame.width = *config->coded_width; + } + if (config->coded_height && *config->coded_height > 0) { + ctx->codec_ctx->height = *config->coded_height; + ctx->frame.height = *config->coded_height; + } + + // Use codec description as extradata (contains SPS/PPS) + if (config->description && config->description_len > 0) { + ctx->codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); + if (ctx->codec_ctx->extradata) { + memcpy(ctx->codec_ctx->extradata, config->description, config->description_len); + ctx->codec_ctx->extradata_size = config->description_len; + } + } + + // Open codec + if (avcodec_open2(ctx->codec_ctx, codec, NULL) < 0) { + LOG_ERROR("Failed to open codec"); + hang_source_destroy_decoder(ctx); + return false; + } + + // Allocate frame buffer (RGBA for OBS) + ctx->frame.linesize[0] = ctx->frame.width * 4; + size_t buffer_size = ctx->frame.width * ctx->frame.height * 4; + ctx->frame_buffer = (uint8_t *)bmalloc(buffer_size); + ctx->frame.data[0] = ctx->frame_buffer; + + // Create scaling context for YUV420P -> RGBA conversion + ctx->sws_ctx = sws_getContext( + ctx->frame.width, ctx->frame.height, AV_PIX_FMT_YUV420P, + ctx->frame.width, ctx->frame.height, AV_PIX_FMT_RGBA, + SWS_BILINEAR, NULL, NULL, NULL + ); + + if (!ctx->sws_ctx) { + LOG_ERROR("Failed to create scaling context"); + hang_source_destroy_decoder(ctx); + return false; + } + + ctx->frame.format = VIDEO_FORMAT_RGBA; + ctx->frame.timestamp = 0; + + LOG_INFO("Decoder initialized: %dx%d", ctx->frame.width, ctx->frame.height); + return true; +} + +static void hang_source_destroy_decoder(struct hang_source *ctx) +{ + if (ctx->sws_ctx) { + sws_freeContext(ctx->sws_ctx); + ctx->sws_ctx = NULL; + } + + if (ctx->codec_ctx) { + avcodec_free_context(&ctx->codec_ctx); + ctx->codec_ctx = NULL; + } + + if (ctx->frame_buffer) { + bfree(ctx->frame_buffer); + ctx->frame_buffer = NULL; + } +} + +static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) +{ + if (!ctx->codec_ctx) + return; + + // Get frame data + struct moq_frame frame_data; + if (moq_consume_frame_chunk(frame_id, 0, &frame_data) < 0) { + LOG_ERROR("Failed to get frame data"); + moq_consume_frame_close(frame_id); + return; + } + + // Skip non-keyframes until we get the first one + if (!ctx->got_keyframe && !frame_data.keyframe) { + LOG_DEBUG("Skipping non-keyframe before first keyframe"); + moq_consume_frame_close(frame_id); + return; + } + + // Mark that we've received a keyframe from the stream + if (frame_data.keyframe) { + ctx->got_keyframe = true; + LOG_INFO("Received keyframe, payload_size=%zu", frame_data.payload_size); + } + + // Create AVPacket from frame data + AVPacket *packet = av_packet_alloc(); + if (!packet) { + moq_consume_frame_close(frame_id); + return; + } + + packet->data = (uint8_t *)frame_data.payload; + packet->size = frame_data.payload_size; + packet->pts = frame_data.timestamp_us / 1000; // Convert to milliseconds + packet->dts = packet->pts; + + // Send packet to decoder + int ret = avcodec_send_packet(ctx->codec_ctx, packet); + av_packet_free(&packet); + + if (ret < 0) { + if (ret != AVERROR(EAGAIN)) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ret, errbuf, sizeof(errbuf)); + LOG_ERROR("Error sending packet to decoder: %s", errbuf); + } + moq_consume_frame_close(frame_id); + return; + } + + // Receive decoded frames + AVFrame *frame = av_frame_alloc(); + if (!frame) { + moq_consume_frame_close(frame_id); + return; + } + + ret = avcodec_receive_frame(ctx->codec_ctx, frame); + if (ret < 0) { + if (ret != AVERROR(EAGAIN)) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ret, errbuf, sizeof(errbuf)); + LOG_ERROR("Error receiving frame from decoder: %s", errbuf); + } + av_frame_free(&frame); + moq_consume_frame_close(frame_id); + return; + } + + // Convert YUV420P to RGBA + uint8_t *dst_data[4] = {ctx->frame_buffer, NULL, NULL, NULL}; + int dst_linesize[4] = {static_cast(ctx->frame.width * 4), 0, 0, 0}; + + sws_scale(ctx->sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, + 0, ctx->frame.height, dst_data, dst_linesize); + + // Update OBS frame timestamp and output + ctx->frame.timestamp = frame_data.timestamp_us; + obs_source_output_video(ctx->source, &ctx->frame); + + LOG_DEBUG("Output video frame: %dx%d ts=%llu", ctx->frame.width, ctx->frame.height, (unsigned long long)ctx->frame.timestamp); + + av_frame_free(&frame); + moq_consume_frame_close(frame_id); +} + +// Registration function +void register_hang_source() +{ + struct obs_source_info info = {}; + info.id = "hang_source"; + info.type = OBS_SOURCE_TYPE_INPUT; + info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_DO_NOT_DUPLICATE; + info.get_name = [](void *) -> const char * { + return "Hang Source (MoQ)"; + }; + info.create = hang_source_create; + info.destroy = hang_source_destroy; + info.update = hang_source_update; + info.get_defaults = hang_source_get_defaults; + info.get_properties = hang_source_properties; + // Note: No video_tick or video_render needed for async video sources + // OBS handles rendering via obs_source_output_video() + + obs_register_source(&info); +} diff --git a/src/hang-source.h b/src/hang-source.h new file mode 100644 index 0000000..30d13e4 --- /dev/null +++ b/src/hang-source.h @@ -0,0 +1,3 @@ +#pragma once + +void register_hang_source(); diff --git a/src/logger.h b/src/logger.h index 29e224d..8b06020 100644 --- a/src/logger.h +++ b/src/logger.h @@ -1,8 +1,8 @@ #include -// Logging macros -#define LOG(level, format, ...) blog(level, "[obs-moq] " format, ##__VA_ARGS__) -#define LOG_DEBUG(format, ...) LOG(LOG_DEBUG, format, ##__VA_ARGS__) -#define LOG_INFO(format, ...) LOG(LOG_INFO, format, ##__VA_ARGS__) -#define LOG_WARNING(format, ...) LOG(LOG_WARNING, format, ##__VA_ARGS__) -#define LOG_ERROR(format, ...) LOG(LOG_ERROR, format, ##__VA_ARGS__) +// Logging macros - use MOQ_ prefix to avoid conflicts with OBS log level constants +#define MOQ_LOG(level, format, ...) blog(level, "[obs-moq] " format, ##__VA_ARGS__) +#define LOG_DEBUG(format, ...) MOQ_LOG(400, format, ##__VA_ARGS__) +#define LOG_INFO(format, ...) MOQ_LOG(300, format, ##__VA_ARGS__) +#define LOG_WARNING(format, ...) MOQ_LOG(200, format, ##__VA_ARGS__) +#define LOG_ERROR(format, ...) MOQ_LOG(100, format, ##__VA_ARGS__) diff --git a/src/moq-output.cpp b/src/moq-output.cpp index 0cee381..6393b19 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -13,16 +13,17 @@ MoQOutput::MoQOutput(obs_data_t *, obs_output_t *output) path(), total_bytes_sent(0), connect_time_ms(0), + origin(0), session(0), + broadcast(moq_publish_create()), video(0), - audio(0), - broadcast(moq_broadcast_create()) + audio(0) { } MoQOutput::~MoQOutput() { - moq_broadcast_close(broadcast); + moq_publish_close(broadcast); Stop(); } @@ -80,22 +81,28 @@ bool MoQOutput::Start() } }; - // Start establishing a session with the MoQ server - // NOTE: You could publish the same broadcasts to multiple sessions if you want (redundant ingest). - session = moq_session_connect(server_url.c_str(), session_connect_callback, this); - if (session < 0) { - LOG_ERROR("Failed to initialize MoQ server: %d", session); + // Create an origin for publishing + origin = moq_origin_create(); + if (origin < 0) { + LOG_ERROR("Failed to create origin: %d", origin); return false; } LOG_INFO("Publishing broadcast: %s", path.c_str()); - // Publish the one broadcast to the session. - // NOTE: You could publish multiple broadcasts to the same session if you want (multi ingest). - // TODO: There is currently no unpublish function. - auto result = moq_broadcast_publish(broadcast, session, path.c_str()); + // Publish the broadcast to the origin before connecting + // NOTE: You could publish multiple broadcasts to the same origin if you want (multi ingest). + auto result = moq_origin_publish(origin, path.c_str(), path.length(), broadcast); if (result < 0) { - LOG_ERROR("Failed to publish broadcast to session: %d", result); + LOG_ERROR("Failed to publish broadcast to origin: %d", result); + return false; + } + + // Start establishing a session with the MoQ server + // NOTE: You could publish the same broadcasts to multiple sessions if you want (redundant ingest). + session = moq_session_connect(server_url.c_str(), server_url.length(), origin, 0, session_connect_callback, this); + if (session < 0) { + LOG_ERROR("Failed to initialize MoQ server: %d", session); return false; } @@ -106,17 +113,27 @@ bool MoQOutput::Start() void MoQOutput::Stop(bool signal) { + // Close media tracks + if (video > 0) { + moq_publish_media_close(video); + video = 0; + } + + if (audio > 0) { + moq_publish_media_close(audio); + audio = 0; + } + // Close the session if (session > 0) { moq_session_close(session); + session = 0; } - if (video > 0) { - moq_track_close(video); - } - - if (audio > 0) { - moq_track_close(audio); + // Close the origin + if (origin > 0) { + moq_origin_close(origin); + origin = 0; } if (signal) { @@ -154,7 +171,7 @@ void MoQOutput::AudioData(struct encoder_packet *packet) auto pts = util_mul_div64(packet->pts, 1000000ULL * packet->timebase_num, packet->timebase_den); - auto result = moq_track_write(audio, packet->data, packet->size, pts); + auto result = moq_publish_media_frame(audio, packet->data, packet->size, pts); if (result < 0) { LOG_ERROR("Failed to write audio frame: %d", result); return; @@ -175,7 +192,7 @@ void MoQOutput::VideoData(struct encoder_packet *packet) auto pts = util_mul_div64(packet->pts, 1000000ULL * packet->timebase_num, packet->timebase_den); - auto result = moq_track_write(video, packet->data, packet->size, pts); + auto result = moq_publish_media_frame(video, packet->data, packet->size, pts); if (result < 0) { LOG_ERROR("Failed to write video frame: %d", result); return; @@ -215,7 +232,7 @@ void MoQOutput::VideoInit() } const char *codec = obs_encoder_get_codec(encoder); - video = moq_track_create(broadcast, codec, extra_data, extra_size); + video = moq_publish_media_init(broadcast, codec, strlen(codec), extra_data, extra_size); if (video < 0) { LOG_ERROR("Failed to initialize video track: %d", video); return; @@ -253,7 +270,7 @@ void MoQOutput::AudioInit() } const char *codec = obs_encoder_get_codec(encoder); - audio = moq_track_create(broadcast, codec, extra_data, extra_size); + audio = moq_publish_media_init(broadcast, codec, strlen(codec), extra_data, extra_size); if (audio < 0) { LOG_ERROR("Failed to initialize audio track: %d", audio); return; diff --git a/src/moq-output.h b/src/moq-output.h index caa4102..e48bc06 100644 --- a/src/moq-output.h +++ b/src/moq-output.h @@ -40,6 +40,7 @@ class MoQOutput int connect_time_ms; std::chrono::steady_clock::time_point connect_start; + int origin; int session; int broadcast; int video; diff --git a/src/obs-moq.cpp b/src/obs-moq.cpp index ddbcfb0..7010dec 100644 --- a/src/obs-moq.cpp +++ b/src/obs-moq.cpp @@ -20,6 +20,7 @@ with this program. If not, see #include "moq-output.h" #include "moq-service.h" +#include "hang-source.h" extern "C" { #include "moq.h" @@ -35,10 +36,11 @@ MODULE_EXPORT const char *obs_module_description(void) bool obs_module_load(void) { // Use RUST_LOG env var for more verbose output - moq_log_level("info"); + moq_log_level("info", 4); register_moq_output(); register_moq_service(); + register_hang_source(); return true; } From 33abd1ac359da63d2ad66f6325700f7aa6d28774 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 07:47:58 -0600 Subject: [PATCH 02/12] Improve thread safety and remove per-frame debug logging - Rename hang_source_disconnect -> hang_source_disconnect_locked and hang_source_destroy_decoder -> hang_source_destroy_decoder_locked to indicate they require the mutex - Add mutex locking in hang_source_destroy() - Add stale callback checks in on_session_status(), on_catalog(), and on_video_frame() to ignore callbacks from disconnected sessions - Handle generation changes in on_catalog() to clean up tracks if reconnection happened during setup - Add origin validity check in hang_source_start_consume() - Refactor hang_source_init_decoder() to prepare new decoder state outside the mutex, then swap atomically inside the mutex - Add comprehensive mutex protection throughout hang_source_decode_frame() with early exits on invalid decoder state - Ensure ctx->frame.data[0] is set to NULL when frame buffer is freed - Remove noisy per-frame LOG_DEBUG calls for video frame receive/output --- src/hang-source.cpp | 178 +++++++++++++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 45 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index a2d69a1..95f0139 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -58,9 +58,9 @@ static void on_video_frame(void *user_data, int32_t frame_id); // Helper functions static void hang_source_reconnect(struct hang_source *ctx); -static void hang_source_disconnect(struct hang_source *ctx); +static void hang_source_disconnect_locked(struct hang_source *ctx); static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); -static void hang_source_destroy_decoder(struct hang_source *ctx); +static void hang_source_destroy_decoder_locked(struct hang_source *ctx); static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); static void *hang_source_create(obs_data_t *settings, obs_source_t *source) @@ -100,12 +100,13 @@ static void hang_source_destroy(void *data) { struct hang_source *ctx = (struct hang_source *)data; - hang_source_disconnect(ctx); - hang_source_destroy_decoder(ctx); + pthread_mutex_lock(&ctx->mutex); + hang_source_disconnect_locked(ctx); + pthread_mutex_unlock(&ctx->mutex); bfree(ctx->url); bfree(ctx->broadcast); - bfree(ctx->frame_buffer); + // Note: frame_buffer is already freed by hang_source_disconnect_locked pthread_mutex_destroy(&ctx->mutex); @@ -167,6 +168,14 @@ static void on_session_status(void *user_data, int32_t code) { struct hang_source *ctx = (struct hang_source *)user_data; + // Check if we've been disconnected + pthread_mutex_lock(&ctx->mutex); + if (ctx->session < 0) { + pthread_mutex_unlock(&ctx->mutex); + return; + } + pthread_mutex_unlock(&ctx->mutex); + if (code == 0) { LOG_INFO("MoQ session connected successfully"); // Now that we're connected, start consuming the broadcast @@ -184,8 +193,16 @@ static void on_catalog(void *user_data, int32_t catalog) pthread_mutex_lock(&ctx->mutex); - // Check if this is still the current generation + // Check if this callback is still valid (not from a stale connection) uint32_t current_gen = ctx->generation; + if (ctx->consume < 0) { + // We've been disconnected, ignore this callback + pthread_mutex_unlock(&ctx->mutex); + if (catalog >= 0) + moq_consume_catalog_close(catalog); + return; + } + pthread_mutex_unlock(&ctx->mutex); if (catalog < 0) { @@ -201,7 +218,7 @@ static void on_catalog(void *user_data, int32_t catalog) return; } - // Initialize decoder with the video config + // Initialize decoder with the video config (takes mutex internally) if (!hang_source_init_decoder(ctx, &video_config)) { LOG_ERROR("Failed to initialize decoder"); moq_consume_catalog_close(catalog); @@ -221,6 +238,12 @@ static void on_catalog(void *user_data, int32_t catalog) if (ctx->generation == current_gen) { ctx->video_track = track; ctx->catalog_handle = catalog; + } else { + // Generation changed while we were setting up, clean up the track + pthread_mutex_unlock(&ctx->mutex); + moq_consume_video_track_close(track); + moq_consume_catalog_close(catalog); + return; } pthread_mutex_unlock(&ctx->mutex); @@ -236,7 +259,16 @@ static void on_video_frame(void *user_data, int32_t frame_id) return; } - LOG_DEBUG("Received video frame: %d", frame_id); + // Check if this callback is still valid (not from a stale connection) + pthread_mutex_lock(&ctx->mutex); + if (ctx->video_track < 0) { + // We've been disconnected, ignore this callback + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + pthread_mutex_unlock(&ctx->mutex); + hang_source_decode_frame(ctx, frame_id); } @@ -248,7 +280,7 @@ static void hang_source_reconnect(struct hang_source *ctx) // Increment generation to invalidate old callbacks pthread_mutex_lock(&ctx->mutex); ctx->generation++; - hang_source_disconnect(ctx); + hang_source_disconnect_locked(ctx); pthread_mutex_unlock(&ctx->mutex); // Create origin for consuming @@ -277,15 +309,27 @@ static void hang_source_reconnect(struct hang_source *ctx) // Called after session is connected successfully static void hang_source_start_consume(struct hang_source *ctx) { + // Check if origin is still valid + pthread_mutex_lock(&ctx->mutex); + if (ctx->origin < 0) { + pthread_mutex_unlock(&ctx->mutex); + return; + } + pthread_mutex_unlock(&ctx->mutex); + // Consume broadcast by path - ctx->consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); - if (ctx->consume < 0) { - LOG_ERROR("Failed to consume broadcast: %d", ctx->consume); + int32_t consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); + if (consume < 0) { + LOG_ERROR("Failed to consume broadcast: %d", consume); return; } + pthread_mutex_lock(&ctx->mutex); + ctx->consume = consume; + pthread_mutex_unlock(&ctx->mutex); + // Subscribe to catalog updates - int32_t catalog_handle = moq_consume_catalog(ctx->consume, on_catalog, ctx); + int32_t catalog_handle = moq_consume_catalog(consume, on_catalog, ctx); if (catalog_handle < 0) { LOG_ERROR("Failed to subscribe to catalog: %d", catalog_handle); return; @@ -294,7 +338,8 @@ static void hang_source_start_consume(struct hang_source *ctx) LOG_INFO("Consuming broadcast: %s", ctx->broadcast); } -static void hang_source_disconnect(struct hang_source *ctx) +// NOTE: Caller must hold ctx->mutex when calling this function +static void hang_source_disconnect_locked(struct hang_source *ctx) { if (ctx->video_track >= 0) { moq_consume_video_track_close(ctx->video_track); @@ -321,81 +366,112 @@ static void hang_source_disconnect(struct hang_source *ctx) ctx->origin = -1; } - hang_source_destroy_decoder(ctx); + hang_source_destroy_decoder_locked(ctx); ctx->got_keyframe = false; } static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) { - hang_source_destroy_decoder(ctx); - - // Find H.264 decoder + // Find H.264 decoder (can be done outside mutex) const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { LOG_ERROR("H.264 codec not found"); return false; } - // Create codec context - ctx->codec_ctx = avcodec_alloc_context3(codec); - if (!ctx->codec_ctx) { + // Create codec context (can be done outside mutex) + AVCodecContext *new_codec_ctx = avcodec_alloc_context3(codec); + if (!new_codec_ctx) { LOG_ERROR("Failed to allocate codec context"); return false; } + uint32_t width = FRAME_WIDTH; + uint32_t height = FRAME_HEIGHT; + // Set codec parameters from config if (config->coded_width && *config->coded_width > 0) { - ctx->codec_ctx->width = *config->coded_width; - ctx->frame.width = *config->coded_width; + new_codec_ctx->width = *config->coded_width; + width = *config->coded_width; } if (config->coded_height && *config->coded_height > 0) { - ctx->codec_ctx->height = *config->coded_height; - ctx->frame.height = *config->coded_height; + new_codec_ctx->height = *config->coded_height; + height = *config->coded_height; } // Use codec description as extradata (contains SPS/PPS) if (config->description && config->description_len > 0) { - ctx->codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); - if (ctx->codec_ctx->extradata) { - memcpy(ctx->codec_ctx->extradata, config->description, config->description_len); - ctx->codec_ctx->extradata_size = config->description_len; + new_codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); + if (new_codec_ctx->extradata) { + memcpy(new_codec_ctx->extradata, config->description, config->description_len); + new_codec_ctx->extradata_size = config->description_len; } } // Open codec - if (avcodec_open2(ctx->codec_ctx, codec, NULL) < 0) { + if (avcodec_open2(new_codec_ctx, codec, NULL) < 0) { LOG_ERROR("Failed to open codec"); - hang_source_destroy_decoder(ctx); + avcodec_free_context(&new_codec_ctx); return false; } // Allocate frame buffer (RGBA for OBS) - ctx->frame.linesize[0] = ctx->frame.width * 4; - size_t buffer_size = ctx->frame.width * ctx->frame.height * 4; - ctx->frame_buffer = (uint8_t *)bmalloc(buffer_size); - ctx->frame.data[0] = ctx->frame_buffer; + size_t buffer_size = width * height * 4; + uint8_t *new_frame_buffer = (uint8_t *)bmalloc(buffer_size); + if (!new_frame_buffer) { + LOG_ERROR("Failed to allocate frame buffer"); + avcodec_free_context(&new_codec_ctx); + return false; + } // Create scaling context for YUV420P -> RGBA conversion - ctx->sws_ctx = sws_getContext( - ctx->frame.width, ctx->frame.height, AV_PIX_FMT_YUV420P, - ctx->frame.width, ctx->frame.height, AV_PIX_FMT_RGBA, + struct SwsContext *new_sws_ctx = sws_getContext( + width, height, AV_PIX_FMT_YUV420P, + width, height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL ); - if (!ctx->sws_ctx) { + if (!new_sws_ctx) { LOG_ERROR("Failed to create scaling context"); - hang_source_destroy_decoder(ctx); + bfree(new_frame_buffer); + avcodec_free_context(&new_codec_ctx); return false; } + // Now take the mutex and swap in the new decoder state + pthread_mutex_lock(&ctx->mutex); + + // Destroy old decoder state + if (ctx->sws_ctx) { + sws_freeContext(ctx->sws_ctx); + } + if (ctx->codec_ctx) { + avcodec_free_context(&ctx->codec_ctx); + } + if (ctx->frame_buffer) { + bfree(ctx->frame_buffer); + } + + // Install new decoder state + ctx->codec_ctx = new_codec_ctx; + ctx->sws_ctx = new_sws_ctx; + ctx->frame_buffer = new_frame_buffer; + ctx->frame.width = width; + ctx->frame.height = height; + ctx->frame.linesize[0] = width * 4; + ctx->frame.data[0] = new_frame_buffer; ctx->frame.format = VIDEO_FORMAT_RGBA; ctx->frame.timestamp = 0; + ctx->got_keyframe = false; + + pthread_mutex_unlock(&ctx->mutex); - LOG_INFO("Decoder initialized: %dx%d", ctx->frame.width, ctx->frame.height); + LOG_INFO("Decoder initialized: %dx%d", width, height); return true; } -static void hang_source_destroy_decoder(struct hang_source *ctx) +// NOTE: Caller must hold ctx->mutex when calling this function +static void hang_source_destroy_decoder_locked(struct hang_source *ctx) { if (ctx->sws_ctx) { sws_freeContext(ctx->sws_ctx); @@ -410,18 +486,26 @@ static void hang_source_destroy_decoder(struct hang_source *ctx) if (ctx->frame_buffer) { bfree(ctx->frame_buffer); ctx->frame_buffer = NULL; + ctx->frame.data[0] = NULL; } } static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) { - if (!ctx->codec_ctx) + pthread_mutex_lock(&ctx->mutex); + + // Check if decoder is still valid (may have been destroyed during reconnect) + if (!ctx->codec_ctx || !ctx->sws_ctx || !ctx->frame_buffer) { + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); return; + } // Get frame data struct moq_frame frame_data; if (moq_consume_frame_chunk(frame_id, 0, &frame_data) < 0) { LOG_ERROR("Failed to get frame data"); + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -429,6 +513,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Skip non-keyframes until we get the first one if (!ctx->got_keyframe && !frame_data.keyframe) { LOG_DEBUG("Skipping non-keyframe before first keyframe"); + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -442,6 +527,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Create AVPacket from frame data AVPacket *packet = av_packet_alloc(); if (!packet) { + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -461,6 +547,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) av_strerror(ret, errbuf, sizeof(errbuf)); LOG_ERROR("Error sending packet to decoder: %s", errbuf); } + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -468,6 +555,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Receive decoded frames AVFrame *frame = av_frame_alloc(); if (!frame) { + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -480,6 +568,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) LOG_ERROR("Error receiving frame from decoder: %s", errbuf); } av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } @@ -495,9 +584,8 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) ctx->frame.timestamp = frame_data.timestamp_us; obs_source_output_video(ctx->source, &ctx->frame); - LOG_DEBUG("Output video frame: %dx%d ts=%llu", ctx->frame.width, ctx->frame.height, (unsigned long long)ctx->frame.timestamp); - av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); } From 792a48d90257cb597be27975faf7bbc9b338223f Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 08:13:58 -0600 Subject: [PATCH 03/12] Fix API renames and race conditions in hang-source API updates: - moq_consume_video_track -> moq_consume_video_ordered - moq_consume_video_track_close -> moq_consume_video_close - moq_publish_media_init -> moq_publish_media_ordered Thread safety fixes in hang-source.cpp: - Add mutex protection for ctx->url and ctx->broadcast in hang_source_update - Pass generation number through on_session_status to hang_source_start_consume - Capture origin handle and broadcast copy while holding mutex in start_consume - Verify generation hasn't changed after moq_origin_consume completes - Create origin/session into local variables in hang_source_reconnect - Re-verify generation before committing new handles to context - Clean up stale resources if generation changed during reconnect setup These fixes prevent race conditions when the user rapidly changes broadcast settings, which could cause callbacks from old sessions to interfere with new connections. --- src/hang-source.cpp | 97 +++++++++++++++++++++++++++++++++------------ src/moq-output.cpp | 4 +- 2 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index 95f0139..cfa2959 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -121,6 +121,9 @@ static void hang_source_update(void *data, obs_data_t *settings) const char *broadcast = obs_data_get_string(settings, "broadcast"); bool changed = false; + bool should_reconnect = false; + + pthread_mutex_lock(&ctx->mutex); if (!ctx->url || strcmp(ctx->url, url) != 0) { bfree(ctx->url); @@ -135,6 +138,12 @@ static void hang_source_update(void *data, obs_data_t *settings) } if (changed && ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0) { + should_reconnect = true; + } + + pthread_mutex_unlock(&ctx->mutex); + + if (should_reconnect) { hang_source_reconnect(ctx); } } @@ -161,25 +170,26 @@ static obs_properties_t *hang_source_properties(void *data) // via obs_source_output_video(). No video_tick or video_render callbacks needed. // Forward declaration for use in callback -static void hang_source_start_consume(struct hang_source *ctx); +static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen); // MoQ callback implementations static void on_session_status(void *user_data, int32_t code) { struct hang_source *ctx = (struct hang_source *)user_data; - // Check if we've been disconnected + // Check if we've been disconnected and capture generation pthread_mutex_lock(&ctx->mutex); if (ctx->session < 0) { pthread_mutex_unlock(&ctx->mutex); return; } + uint32_t current_gen = ctx->generation; pthread_mutex_unlock(&ctx->mutex); if (code == 0) { - LOG_INFO("MoQ session connected successfully"); + LOG_INFO("MoQ session connected successfully (generation %u)", current_gen); // Now that we're connected, start consuming the broadcast - hang_source_start_consume(ctx); + hang_source_start_consume(ctx, current_gen); } else { LOG_ERROR("MoQ session failed with code: %d", code); } @@ -226,8 +236,8 @@ static void on_catalog(void *user_data, int32_t catalog) } // Subscribe to video track with minimal buffering - // Note: moq_consume_video_track takes the catalog handle, not the consume handle - int32_t track = moq_consume_video_track(catalog, 0, 0, on_video_frame, ctx); + // Note: moq_consume_video_ordered takes the catalog handle, not the consume handle + int32_t track = moq_consume_video_ordered(catalog, 0, 0, on_video_frame, ctx); if (track < 0) { LOG_ERROR("Failed to subscribe to video track: %d", track); moq_consume_catalog_close(catalog); @@ -241,7 +251,7 @@ static void on_catalog(void *user_data, int32_t catalog) } else { // Generation changed while we were setting up, clean up the track pthread_mutex_unlock(&ctx->mutex); - moq_consume_video_track_close(track); + moq_consume_video_close(track); moq_consume_catalog_close(catalog); return; } @@ -275,56 +285,89 @@ static void on_video_frame(void *user_data, int32_t frame_id) // Helper function implementations static void hang_source_reconnect(struct hang_source *ctx) { - LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation, ctx->generation + 1); - // Increment generation to invalidate old callbacks pthread_mutex_lock(&ctx->mutex); - ctx->generation++; + uint32_t new_gen = ctx->generation + 1; + LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation, new_gen); + ctx->generation = new_gen; hang_source_disconnect_locked(ctx); + + // Copy URL while holding mutex for thread safety + char *url_copy = bstrdup(ctx->url); pthread_mutex_unlock(&ctx->mutex); - // Create origin for consuming - ctx->origin = moq_origin_create(); - if (ctx->origin < 0) { - LOG_ERROR("Failed to create origin: %d", ctx->origin); + // Create origin for consuming (outside mutex since it may block) + int32_t new_origin = moq_origin_create(); + if (new_origin < 0) { + LOG_ERROR("Failed to create origin: %d", new_origin); + bfree(url_copy); return; } // Connect to MoQ server (consume will happen in on_session_status callback) - ctx->session = moq_session_connect( - ctx->url, strlen(ctx->url), + int32_t new_session = moq_session_connect( + url_copy, strlen(url_copy), 0, // origin_publish - ctx->origin, // origin_consume + new_origin, // origin_consume on_session_status, ctx ); + bfree(url_copy); - if (ctx->session < 0) { - LOG_ERROR("Failed to connect to MoQ server: %d", ctx->session); + if (new_session < 0) { + LOG_ERROR("Failed to connect to MoQ server: %d", new_session); + moq_origin_close(new_origin); return; } - LOG_INFO("Connecting to MoQ server: %s", ctx->url); + // Now update ctx with the new handles, checking if generation changed + pthread_mutex_lock(&ctx->mutex); + if (ctx->generation != new_gen) { + // Another reconnect happened while we were creating origin/session + // Clean up our newly created resources + pthread_mutex_unlock(&ctx->mutex); + LOG_INFO("Generation changed during reconnect setup, cleaning up stale resources"); + moq_session_close(new_session); + moq_origin_close(new_origin); + return; + } + ctx->origin = new_origin; + ctx->session = new_session; + LOG_INFO("Connecting to MoQ server (generation %u)", new_gen); + pthread_mutex_unlock(&ctx->mutex); } // Called after session is connected successfully -static void hang_source_start_consume(struct hang_source *ctx) +static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen) { - // Check if origin is still valid + // Check if origin is still valid and generation matches pthread_mutex_lock(&ctx->mutex); - if (ctx->origin < 0) { + if (ctx->origin < 0 || ctx->generation != expected_gen) { pthread_mutex_unlock(&ctx->mutex); + LOG_INFO("Skipping stale consume (generation mismatch or invalid origin)"); return; } + // Capture values while holding mutex + int32_t origin = ctx->origin; + char *broadcast_copy = bstrdup(ctx->broadcast); pthread_mutex_unlock(&ctx->mutex); // Consume broadcast by path - int32_t consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); + int32_t consume = moq_origin_consume(origin, broadcast_copy, strlen(broadcast_copy)); if (consume < 0) { LOG_ERROR("Failed to consume broadcast: %d", consume); + bfree(broadcast_copy); return; } pthread_mutex_lock(&ctx->mutex); + // Verify generation hasn't changed while we were waiting + if (ctx->generation != expected_gen) { + pthread_mutex_unlock(&ctx->mutex); + LOG_INFO("Generation changed during consume setup, cleaning up"); + moq_consume_close(consume); + bfree(broadcast_copy); + return; + } ctx->consume = consume; pthread_mutex_unlock(&ctx->mutex); @@ -332,17 +375,19 @@ static void hang_source_start_consume(struct hang_source *ctx) int32_t catalog_handle = moq_consume_catalog(consume, on_catalog, ctx); if (catalog_handle < 0) { LOG_ERROR("Failed to subscribe to catalog: %d", catalog_handle); + bfree(broadcast_copy); return; } - LOG_INFO("Consuming broadcast: %s", ctx->broadcast); + LOG_INFO("Consuming broadcast: %s", broadcast_copy); + bfree(broadcast_copy); } // NOTE: Caller must hold ctx->mutex when calling this function static void hang_source_disconnect_locked(struct hang_source *ctx) { if (ctx->video_track >= 0) { - moq_consume_video_track_close(ctx->video_track); + moq_consume_video_close(ctx->video_track); ctx->video_track = -1; } diff --git a/src/moq-output.cpp b/src/moq-output.cpp index 6393b19..270dacb 100644 --- a/src/moq-output.cpp +++ b/src/moq-output.cpp @@ -232,7 +232,7 @@ void MoQOutput::VideoInit() } const char *codec = obs_encoder_get_codec(encoder); - video = moq_publish_media_init(broadcast, codec, strlen(codec), extra_data, extra_size); + video = moq_publish_media_ordered(broadcast, codec, strlen(codec), extra_data, extra_size); if (video < 0) { LOG_ERROR("Failed to initialize video track: %d", video); return; @@ -270,7 +270,7 @@ void MoQOutput::AudioInit() } const char *codec = obs_encoder_get_codec(encoder); - audio = moq_publish_media_init(broadcast, codec, strlen(codec), extra_data, extra_size); + audio = moq_publish_media_ordered(broadcast, codec, strlen(codec), extra_data, extra_size); if (audio < 0) { LOG_ERROR("Failed to initialize audio track: %d", audio); return; From 36b7d57083729723070ee055b64e618d63d1bc0d Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 11:20:50 -0600 Subject: [PATCH 04/12] debounced and smoothed out the video preview when changing hang_source broadcast (or URL) --- src/hang-source.cpp | 169 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 26 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index cfa2959..4204f56 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -20,10 +20,16 @@ extern "C" { struct hang_source { obs_source_t *source; - // Settings + // Settings - current active connection settings char *url; char *broadcast; + // Pending settings - what user has typed but not yet applied + char *pending_url; + char *pending_broadcast; + uint64_t settings_changed_time; // Timestamp when settings last changed + bool reconnect_pending; // True if we need to reconnect after debounce + // Session handles (all negative = invalid) volatile uint32_t generation; // Increments on reconnect int32_t origin; @@ -36,6 +42,7 @@ struct hang_source { AVCodecContext *codec_ctx; struct SwsContext *sws_ctx; bool got_keyframe; + uint32_t frames_waiting_for_keyframe; // Count of skipped frames while waiting // Output frame buffer struct obs_source_frame frame; @@ -45,9 +52,13 @@ struct hang_source { pthread_mutex_t mutex; }; +// Debounce delay in milliseconds (500ms = user stops typing for half a second) +#define DEBOUNCE_DELAY_MS 500 + // Forward declarations static void hang_source_update(void *data, obs_data_t *settings); static void hang_source_destroy(void *data); +static void hang_source_video_tick(void *data, float seconds); static obs_properties_t *hang_source_properties(void *data); static void hang_source_get_defaults(obs_data_t *settings); @@ -59,6 +70,7 @@ static void on_video_frame(void *user_data, int32_t frame_id); // Helper functions static void hang_source_reconnect(struct hang_source *ctx); static void hang_source_disconnect_locked(struct hang_source *ctx); +static void hang_source_blank_video(struct hang_source *ctx); static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); static void hang_source_destroy_decoder_locked(struct hang_source *ctx); static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); @@ -68,6 +80,12 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); ctx->source = source; + // Initialize pending settings + ctx->pending_url = NULL; + ctx->pending_broadcast = NULL; + ctx->settings_changed_time = 0; + ctx->reconnect_pending = false; + // Initialize handles to invalid values ctx->generation = 0; ctx->origin = -1; @@ -80,6 +98,7 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) ctx->codec_ctx = NULL; ctx->sws_ctx = NULL; ctx->got_keyframe = false; + ctx->frames_waiting_for_keyframe = 0; ctx->frame_buffer = NULL; // Initialize threading @@ -106,6 +125,8 @@ static void hang_source_destroy(void *data) bfree(ctx->url); bfree(ctx->broadcast); + bfree(ctx->pending_url); + bfree(ctx->pending_broadcast); // Note: frame_buffer is already freed by hang_source_disconnect_locked pthread_mutex_destroy(&ctx->mutex); @@ -120,32 +141,33 @@ static void hang_source_update(void *data, obs_data_t *settings) const char *url = obs_data_get_string(settings, "url"); const char *broadcast = obs_data_get_string(settings, "broadcast"); - bool changed = false; - bool should_reconnect = false; - pthread_mutex_lock(&ctx->mutex); - if (!ctx->url || strcmp(ctx->url, url) != 0) { - bfree(ctx->url); - ctx->url = bstrdup(url); - changed = true; + // Check if pending settings have changed + bool pending_changed = false; + + if (!ctx->pending_url || strcmp(ctx->pending_url, url) != 0) { + bfree(ctx->pending_url); + ctx->pending_url = bstrdup(url); + pending_changed = true; } - if (!ctx->broadcast || strcmp(ctx->broadcast, broadcast) != 0) { - bfree(ctx->broadcast); - ctx->broadcast = bstrdup(broadcast); - changed = true; + if (!ctx->pending_broadcast || strcmp(ctx->pending_broadcast, broadcast) != 0) { + bfree(ctx->pending_broadcast); + ctx->pending_broadcast = bstrdup(broadcast); + pending_changed = true; } - if (changed && ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0) { - should_reconnect = true; + if (pending_changed) { + // Record the time of this change and mark reconnect as pending + // The actual reconnect will happen in video_tick after debounce delay + ctx->settings_changed_time = os_gettime_ns(); + ctx->reconnect_pending = true; + LOG_DEBUG("Settings changed, scheduling reconnect after debounce (url=%s, broadcast=%s)", + url ? url : "(null)", broadcast ? broadcast : "(null)"); } pthread_mutex_unlock(&ctx->mutex); - - if (should_reconnect) { - hang_source_reconnect(ctx); - } } static void hang_source_get_defaults(obs_data_t *settings) @@ -166,8 +188,69 @@ static obs_properties_t *hang_source_properties(void *data) return props; } -// Note: We use OBS_SOURCE_ASYNC_VIDEO, so OBS handles rendering automatically -// via obs_source_output_video(). No video_tick or video_render callbacks needed. +// video_tick handles debounced reconnection - waits for user to stop typing +static void hang_source_video_tick(void *data, float seconds) +{ + UNUSED_PARAMETER(seconds); + struct hang_source *ctx = (struct hang_source *)data; + + pthread_mutex_lock(&ctx->mutex); + + if (!ctx->reconnect_pending) { + pthread_mutex_unlock(&ctx->mutex); + return; + } + + // Check if enough time has passed since last settings change (debounce) + uint64_t now = os_gettime_ns(); + uint64_t elapsed_ms = (now - ctx->settings_changed_time) / 1000000; + + if (elapsed_ms < DEBOUNCE_DELAY_MS) { + pthread_mutex_unlock(&ctx->mutex); + return; + } + + // Debounce period elapsed - time to apply the pending settings + ctx->reconnect_pending = false; + + // Check if pending settings differ from current active settings + bool url_changed = (!ctx->url && ctx->pending_url) || + (ctx->url && !ctx->pending_url) || + (ctx->url && ctx->pending_url && strcmp(ctx->url, ctx->pending_url) != 0); + bool broadcast_changed = (!ctx->broadcast && ctx->pending_broadcast) || + (ctx->broadcast && !ctx->pending_broadcast) || + (ctx->broadcast && ctx->pending_broadcast && strcmp(ctx->broadcast, ctx->pending_broadcast) != 0); + + if (!url_changed && !broadcast_changed) { + // No actual change from current active connection + pthread_mutex_unlock(&ctx->mutex); + return; + } + + // Apply pending settings as the new active settings + bfree(ctx->url); + ctx->url = ctx->pending_url ? bstrdup(ctx->pending_url) : NULL; + bfree(ctx->broadcast); + ctx->broadcast = ctx->pending_broadcast ? bstrdup(ctx->pending_broadcast) : NULL; + + // Check if the new settings are valid + bool valid = ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0; + + if (!valid) { + // Invalid settings - disconnect and blank video + LOG_INFO("Invalid URL or broadcast - disconnecting and blanking video"); + hang_source_disconnect_locked(ctx); + pthread_mutex_unlock(&ctx->mutex); + hang_source_blank_video(ctx); + return; + } + + pthread_mutex_unlock(&ctx->mutex); + + // Valid settings - reconnect + LOG_INFO("Debounce complete, reconnecting to %s / %s", ctx->url, ctx->broadcast); + hang_source_reconnect(ctx); +} // Forward declaration for use in callback static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen); @@ -192,6 +275,8 @@ static void on_session_status(void *user_data, int32_t code) hang_source_start_consume(ctx, current_gen); } else { LOG_ERROR("MoQ session failed with code: %d", code); + // Connection failed - blank the video to show error state + hang_source_blank_video(ctx); } } @@ -217,6 +302,8 @@ static void on_catalog(void *user_data, int32_t catalog) if (catalog < 0) { LOG_ERROR("Failed to get catalog: %d", catalog); + // Catalog failed (likely invalid broadcast) - blank video + hang_source_blank_video(ctx); return; } @@ -269,9 +356,11 @@ static void on_video_frame(void *user_data, int32_t frame_id) return; } - // Check if this callback is still valid (not from a stale connection) + // Check if this callback is still valid using generation (not video_track) + // Note: We can't check video_track here because frames may arrive before + // the track handle is stored in on_catalog (race condition) pthread_mutex_lock(&ctx->mutex); - if (ctx->video_track < 0) { + if (ctx->consume < 0) { // We've been disconnected, ignore this callback pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); @@ -296,11 +385,15 @@ static void hang_source_reconnect(struct hang_source *ctx) char *url_copy = bstrdup(ctx->url); pthread_mutex_unlock(&ctx->mutex); + // Blank video while reconnecting to avoid showing stale frames + hang_source_blank_video(ctx); + // Create origin for consuming (outside mutex since it may block) int32_t new_origin = moq_origin_create(); if (new_origin < 0) { LOG_ERROR("Failed to create origin: %d", new_origin); bfree(url_copy); + // Video already blanked at start of reconnect return; } @@ -316,6 +409,7 @@ static void hang_source_reconnect(struct hang_source *ctx) if (new_session < 0) { LOG_ERROR("Failed to connect to MoQ server: %d", new_session); moq_origin_close(new_origin); + // Video already blanked at start of reconnect return; } @@ -356,6 +450,8 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected if (consume < 0) { LOG_ERROR("Failed to consume broadcast: %d", consume); bfree(broadcast_copy); + // Failed to consume (invalid broadcast path) - blank video + hang_source_blank_video(ctx); return; } @@ -376,6 +472,8 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected if (catalog_handle < 0) { LOG_ERROR("Failed to subscribe to catalog: %d", catalog_handle); bfree(broadcast_copy); + // Failed to get catalog - blank video + hang_source_blank_video(ctx); return; } @@ -413,6 +511,15 @@ static void hang_source_disconnect_locked(struct hang_source *ctx) hang_source_destroy_decoder_locked(ctx); ctx->got_keyframe = false; + ctx->frames_waiting_for_keyframe = 0; +} + +// Blanks the video preview by outputting a NULL frame +static void hang_source_blank_video(struct hang_source *ctx) +{ + // Passing NULL to obs_source_output_video clears the current frame + obs_source_output_video(ctx->source, NULL); + LOG_DEBUG("Video preview blanked"); } static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) @@ -508,6 +615,7 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v ctx->frame.format = VIDEO_FORMAT_RGBA; ctx->frame.timestamp = 0; ctx->got_keyframe = false; + ctx->frames_waiting_for_keyframe = 0; pthread_mutex_unlock(&ctx->mutex); @@ -557,7 +665,12 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Skip non-keyframes until we get the first one if (!ctx->got_keyframe && !frame_data.keyframe) { - LOG_DEBUG("Skipping non-keyframe before first keyframe"); + ctx->frames_waiting_for_keyframe++; + if (ctx->frames_waiting_for_keyframe == 1 || + (ctx->frames_waiting_for_keyframe % 30) == 0) { + LOG_INFO("Waiting for keyframe... (skipped %u frames so far)", + ctx->frames_waiting_for_keyframe); + } pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; @@ -565,8 +678,12 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Mark that we've received a keyframe from the stream if (frame_data.keyframe) { + if (!ctx->got_keyframe) { + LOG_INFO("Got keyframe after waiting for %u frames, payload_size=%zu", + ctx->frames_waiting_for_keyframe, frame_data.payload_size); + } ctx->got_keyframe = true; - LOG_INFO("Received keyframe, payload_size=%zu", frame_data.payload_size); + ctx->frames_waiting_for_keyframe = 0; } // Create AVPacket from frame data @@ -649,8 +766,8 @@ void register_hang_source() info.update = hang_source_update; info.get_defaults = hang_source_get_defaults; info.get_properties = hang_source_properties; - // Note: No video_tick or video_render needed for async video sources - // OBS handles rendering via obs_source_output_video() + // video_tick is needed for debounced reconnection (blur simulation) + info.video_tick = hang_source_video_tick; obs_register_source(&info); } From b644d05629e8f4cd49cf52d9e0dc0003ab69dc23 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 11:46:58 -0600 Subject: [PATCH 05/12] Fix segfault on shutdown by adding shutting_down flag Add a volatile bool shutting_down flag to prevent use-after-free when MoQ callbacks fire during destruction. The issue was that closing MoQ handles triggers async callbacks, but by the time they fire the context may already be freed. Changes: - Add shutting_down flag to hang_source struct - Set flag at start of hang_source_destroy before disconnecting - Add 100ms sleep after disconnect to let callbacks drain - Add early-exit checks for shutting_down in all callbacks: - on_session_status - on_catalog - on_video_frame - hang_source_video_tick - hang_source_decode_frame Callbacks now check the flag while holding the mutex and exit immediately if shutting down, avoiding access to freed memory. --- src/hang-source.cpp | 166 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 153 insertions(+), 13 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index 4204f56..92af819 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -30,8 +30,12 @@ struct hang_source { uint64_t settings_changed_time; // Timestamp when settings last changed bool reconnect_pending; // True if we need to reconnect after debounce + // Shutdown flag - set when destroy begins, callbacks should exit early + volatile bool shutting_down; + // Session handles (all negative = invalid) volatile uint32_t generation; // Increments on reconnect + bool reconnect_in_progress; // True while reconnect is happening int32_t origin; int32_t session; int32_t consume; @@ -43,6 +47,7 @@ struct hang_source { struct SwsContext *sws_ctx; bool got_keyframe; uint32_t frames_waiting_for_keyframe; // Count of skipped frames while waiting + uint32_t consecutive_decode_errors; // Count of consecutive decode failures // Output frame buffer struct obs_source_frame frame; @@ -86,8 +91,12 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) ctx->settings_changed_time = 0; ctx->reconnect_pending = false; + // Initialize shutdown flag + ctx->shutting_down = false; + // Initialize handles to invalid values ctx->generation = 0; + ctx->reconnect_in_progress = false; ctx->origin = -1; ctx->session = -1; ctx->consume = -1; @@ -99,6 +108,7 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) ctx->sws_ctx = NULL; ctx->got_keyframe = false; ctx->frames_waiting_for_keyframe = 0; + ctx->consecutive_decode_errors = 0; ctx->frame_buffer = NULL; // Initialize threading @@ -119,10 +129,16 @@ static void hang_source_destroy(void *data) { struct hang_source *ctx = (struct hang_source *)data; + // Set shutdown flag first - callbacks will check this and exit early pthread_mutex_lock(&ctx->mutex); + ctx->shutting_down = true; hang_source_disconnect_locked(ctx); pthread_mutex_unlock(&ctx->mutex); + // Give MoQ callbacks time to drain - they check shutting_down and exit early + // This prevents use-after-free when async callbacks fire after ctx is freed + os_sleep_ms(100); + bfree(ctx->url); bfree(ctx->broadcast); bfree(ctx->pending_url); @@ -196,6 +212,12 @@ static void hang_source_video_tick(void *data, float seconds) pthread_mutex_lock(&ctx->mutex); + // Don't process during shutdown + if (ctx->shutting_down) { + pthread_mutex_unlock(&ctx->mutex); + return; + } + if (!ctx->reconnect_pending) { pthread_mutex_unlock(&ctx->mutex); return; @@ -260,22 +282,41 @@ static void on_session_status(void *user_data, int32_t code) { struct hang_source *ctx = (struct hang_source *)user_data; - // Check if we've been disconnected and capture generation + // Check if we're shutting down - exit early to avoid use-after-free pthread_mutex_lock(&ctx->mutex); + if (ctx->shutting_down) { + LOG_DEBUG("Ignoring session status callback - shutting down"); + pthread_mutex_unlock(&ctx->mutex); + return; + } if (ctx->session < 0) { + LOG_DEBUG("Ignoring session status callback - already disconnected"); pthread_mutex_unlock(&ctx->mutex); return; } uint32_t current_gen = ctx->generation; - pthread_mutex_unlock(&ctx->mutex); - + if (code == 0) { + pthread_mutex_unlock(&ctx->mutex); LOG_INFO("MoQ session connected successfully (generation %u)", current_gen); // Now that we're connected, start consuming the broadcast hang_source_start_consume(ctx, current_gen); } else { - LOG_ERROR("MoQ session failed with code: %d", code); - // Connection failed - blank the video to show error state + // Connection failed - clean up the session and origin immediately + LOG_ERROR("MoQ session failed with code: %d (generation %u)", code, current_gen); + + // Clean up failed session/origin to prevent further callbacks + if (ctx->session >= 0) { + moq_session_close(ctx->session); + ctx->session = -1; + } + if (ctx->origin >= 0) { + moq_origin_close(ctx->origin); + ctx->origin = -1; + } + pthread_mutex_unlock(&ctx->mutex); + + // Blank the video to show error state hang_source_blank_video(ctx); } } @@ -288,6 +329,15 @@ static void on_catalog(void *user_data, int32_t catalog) pthread_mutex_lock(&ctx->mutex); + // Check if we're shutting down - exit early to avoid use-after-free + if (ctx->shutting_down) { + LOG_DEBUG("Ignoring catalog callback - shutting down"); + pthread_mutex_unlock(&ctx->mutex); + if (catalog >= 0) + moq_consume_catalog_close(catalog); + return; + } + // Check if this callback is still valid (not from a stale connection) uint32_t current_gen = ctx->generation; if (ctx->consume < 0) { @@ -360,6 +410,12 @@ static void on_video_frame(void *user_data, int32_t frame_id) // Note: We can't check video_track here because frames may arrive before // the track handle is stored in on_catalog (race condition) pthread_mutex_lock(&ctx->mutex); + // Check if we're shutting down - exit early to avoid use-after-free + if (ctx->shutting_down) { + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } if (ctx->consume < 0) { // We've been disconnected, ignore this callback pthread_mutex_unlock(&ctx->mutex); @@ -376,6 +432,15 @@ static void hang_source_reconnect(struct hang_source *ctx) { // Increment generation to invalidate old callbacks pthread_mutex_lock(&ctx->mutex); + + // Check if reconnect is already in progress + if (ctx->reconnect_in_progress) { + LOG_DEBUG("Reconnect already in progress, skipping"); + pthread_mutex_unlock(&ctx->mutex); + return; + } + + ctx->reconnect_in_progress = true; uint32_t new_gen = ctx->generation + 1; LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation, new_gen); ctx->generation = new_gen; @@ -388,12 +453,17 @@ static void hang_source_reconnect(struct hang_source *ctx) // Blank video while reconnecting to avoid showing stale frames hang_source_blank_video(ctx); + // Small delay to allow MoQ library to fully clean up previous connection + os_sleep_ms(50); + // Create origin for consuming (outside mutex since it may block) int32_t new_origin = moq_origin_create(); if (new_origin < 0) { LOG_ERROR("Failed to create origin: %d", new_origin); bfree(url_copy); - // Video already blanked at start of reconnect + pthread_mutex_lock(&ctx->mutex); + ctx->reconnect_in_progress = false; + pthread_mutex_unlock(&ctx->mutex); return; } @@ -409,7 +479,9 @@ static void hang_source_reconnect(struct hang_source *ctx) if (new_session < 0) { LOG_ERROR("Failed to connect to MoQ server: %d", new_session); moq_origin_close(new_origin); - // Video already blanked at start of reconnect + pthread_mutex_lock(&ctx->mutex); + ctx->reconnect_in_progress = false; + pthread_mutex_unlock(&ctx->mutex); return; } @@ -418,6 +490,7 @@ static void hang_source_reconnect(struct hang_source *ctx) if (ctx->generation != new_gen) { // Another reconnect happened while we were creating origin/session // Clean up our newly created resources + ctx->reconnect_in_progress = false; pthread_mutex_unlock(&ctx->mutex); LOG_INFO("Generation changed during reconnect setup, cleaning up stale resources"); moq_session_close(new_session); @@ -426,6 +499,7 @@ static void hang_source_reconnect(struct hang_source *ctx) } ctx->origin = new_origin; ctx->session = new_session; + ctx->reconnect_in_progress = false; LOG_INFO("Connecting to MoQ server (generation %u)", new_gen); pthread_mutex_unlock(&ctx->mutex); } @@ -448,9 +522,21 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected // Consume broadcast by path int32_t consume = moq_origin_consume(origin, broadcast_copy, strlen(broadcast_copy)); if (consume < 0) { - LOG_ERROR("Failed to consume broadcast: %d", consume); + LOG_ERROR("Failed to consume broadcast '%s': %d", broadcast_copy, consume); bfree(broadcast_copy); - // Failed to consume (invalid broadcast path) - blank video + // Failed to consume - clean up session/origin + pthread_mutex_lock(&ctx->mutex); + if (ctx->generation == expected_gen) { + if (ctx->session >= 0) { + moq_session_close(ctx->session); + ctx->session = -1; + } + if (ctx->origin >= 0) { + moq_origin_close(ctx->origin); + ctx->origin = -1; + } + } + pthread_mutex_unlock(&ctx->mutex); hang_source_blank_video(ctx); return; } @@ -470,9 +556,25 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected // Subscribe to catalog updates int32_t catalog_handle = moq_consume_catalog(consume, on_catalog, ctx); if (catalog_handle < 0) { - LOG_ERROR("Failed to subscribe to catalog: %d", catalog_handle); + LOG_ERROR("Failed to subscribe to catalog for '%s': %d", broadcast_copy, catalog_handle); bfree(broadcast_copy); - // Failed to get catalog - blank video + // Failed to get catalog - clean up + pthread_mutex_lock(&ctx->mutex); + if (ctx->generation == expected_gen) { + if (ctx->consume >= 0) { + moq_consume_close(ctx->consume); + ctx->consume = -1; + } + if (ctx->session >= 0) { + moq_session_close(ctx->session); + ctx->session = -1; + } + if (ctx->origin >= 0) { + moq_origin_close(ctx->origin); + ctx->origin = -1; + } + } + pthread_mutex_unlock(&ctx->mutex); hang_source_blank_video(ctx); return; } @@ -512,6 +614,7 @@ static void hang_source_disconnect_locked(struct hang_source *ctx) hang_source_destroy_decoder_locked(ctx); ctx->got_keyframe = false; ctx->frames_waiting_for_keyframe = 0; + ctx->consecutive_decode_errors = 0; } // Blanks the video preview by outputting a NULL frame @@ -616,6 +719,7 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v ctx->frame.timestamp = 0; ctx->got_keyframe = false; ctx->frames_waiting_for_keyframe = 0; + ctx->consecutive_decode_errors = 0; pthread_mutex_unlock(&ctx->mutex); @@ -647,6 +751,13 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) { pthread_mutex_lock(&ctx->mutex); + // Check if we're shutting down - exit early to avoid use-after-free + if (ctx->shutting_down) { + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + // Check if decoder is still valid (may have been destroyed during reconnect) if (!ctx->codec_ctx || !ctx->sws_ctx || !ctx->frame_buffer) { pthread_mutex_unlock(&ctx->mutex); @@ -681,9 +792,12 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) if (!ctx->got_keyframe) { LOG_INFO("Got keyframe after waiting for %u frames, payload_size=%zu", ctx->frames_waiting_for_keyframe, frame_data.payload_size); + // Flush decoder to ensure clean state when starting from keyframe + avcodec_flush_buffers(ctx->codec_ctx); } ctx->got_keyframe = true; ctx->frames_waiting_for_keyframe = 0; + ctx->consecutive_decode_errors = 0; } // Create AVPacket from frame data @@ -705,9 +819,20 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) if (ret < 0) { if (ret != AVERROR(EAGAIN)) { + ctx->consecutive_decode_errors++; char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); - LOG_ERROR("Error sending packet to decoder: %s", errbuf); + + // If too many consecutive errors, flush decoder and wait for next keyframe + if (ctx->consecutive_decode_errors >= 5) { + LOG_WARNING("Too many send errors (%u), flushing decoder and waiting for keyframe", + ctx->consecutive_decode_errors); + avcodec_flush_buffers(ctx->codec_ctx); + ctx->got_keyframe = false; + ctx->consecutive_decode_errors = 0; + } else if (ctx->consecutive_decode_errors == 1) { + LOG_ERROR("Error sending packet to decoder: %s", errbuf); + } } pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); @@ -725,15 +850,30 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) ret = avcodec_receive_frame(ctx->codec_ctx, frame); if (ret < 0) { if (ret != AVERROR(EAGAIN)) { + ctx->consecutive_decode_errors++; char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); - LOG_ERROR("Error receiving frame from decoder: %s", errbuf); + + // If too many consecutive errors, flush decoder and wait for next keyframe + if (ctx->consecutive_decode_errors >= 5) { + LOG_WARNING("Too many decode errors (%u), flushing decoder and waiting for keyframe", + ctx->consecutive_decode_errors); + avcodec_flush_buffers(ctx->codec_ctx); + ctx->got_keyframe = false; + ctx->consecutive_decode_errors = 0; + } else if (ctx->consecutive_decode_errors == 1) { + // Only log first error in a sequence + LOG_ERROR("Error receiving frame from decoder: %s", errbuf); + } } av_frame_free(&frame); pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; } + + // Successfully decoded a frame - reset error counter + ctx->consecutive_decode_errors = 0; // Convert YUV420P to RGBA uint8_t *dst_data[4] = {ctx->frame_buffer, NULL, NULL, NULL}; From 7c8bbb0977ddc97eab3a6edeff43f05237a7d1f1 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 12:37:40 -0600 Subject: [PATCH 06/12] removed redundant hang-source.c file --- src/hang-source.c | 519 ---------------------------------------------- 1 file changed, 519 deletions(-) delete mode 100644 src/hang-source.c diff --git a/src/hang-source.c b/src/hang-source.c deleted file mode 100644 index d2e4db1..0000000 --- a/src/hang-source.c +++ /dev/null @@ -1,519 +0,0 @@ -#include -#include -#include -#include -#include - -extern "C" { -#include -#include -#include -#include "moq.h" -} - -#include "hang-source.h" -#include "logger.h" - -#define FRAME_WIDTH 1920 -#define FRAME_HEIGHT 1080 - -struct hang_source { - obs_source_t *source; - - // Settings - char *url; - char *broadcast; - - // Session handles (all negative = invalid) - volatile uint32_t generation; // Increments on reconnect - int32_t origin; - int32_t session; - int32_t consume; - int32_t catalog_handle; - int32_t video_track; - - // Decoder state - AVCodecContext *codec_ctx; - struct SwsContext *sws_ctx; - bool got_keyframe; - - // Output frame buffer - struct obs_source_frame frame; - uint8_t *frame_buffer; - - // Threading - pthread_mutex_t mutex; -}; - -// Forward declarations -static void hang_source_update(void *data, obs_data_t *settings); -static void hang_source_destroy(void *data); -static void hang_source_tick(void *data, float seconds); -static void hang_source_render(void *data, gs_effect_t *effect); -static obs_properties_t *hang_source_properties(void *data); -static void hang_source_get_defaults(obs_data_t *settings); - -// MoQ callbacks -static void on_session_status(void *user_data, int32_t code); -static void on_catalog(void *user_data, int32_t catalog); -static void on_video_frame(void *user_data, int32_t frame_id); - -// Helper functions -static void hang_source_reconnect(struct hang_source *ctx); -static void hang_source_disconnect(struct hang_source *ctx); -static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); -static void hang_source_destroy_decoder(struct hang_source *ctx); -static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); - -static void *hang_source_create(obs_data_t *settings, obs_source_t *source) -{ - struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); - ctx->source = source; - - // Initialize handles to invalid values - ctx->generation = 0; - ctx->origin = -1; - ctx->session = -1; - ctx->consume = -1; - ctx->catalog_handle = -1; - ctx->video_track = -1; - - // Initialize decoder state - ctx->codec_ctx = NULL; - ctx->sws_ctx = NULL; - ctx->got_keyframe = false; - ctx->frame_buffer = NULL; - - // Initialize threading - pthread_mutex_init(&ctx->mutex, NULL); - - // Initialize OBS frame structure - ctx->frame.width = FRAME_WIDTH; - ctx->frame.height = FRAME_HEIGHT; - ctx->frame.format = VIDEO_FORMAT_RGBA; - ctx->frame.linesize[0] = FRAME_WIDTH * 4; - - hang_source_update(ctx, settings); - - return ctx; -} - -static void hang_source_destroy(void *data) -{ - struct hang_source *ctx = (struct hang_source *)data; - - hang_source_disconnect(ctx); - hang_source_destroy_decoder(ctx); - - bfree(ctx->url); - bfree(ctx->broadcast); - bfree(ctx->frame_buffer); - - pthread_mutex_destroy(&ctx->mutex); - - bfree(ctx); -} - -static void hang_source_update(void *data, obs_data_t *settings) -{ - struct hang_source *ctx = (struct hang_source *)data; - - const char *url = obs_data_get_string(settings, "url"); - const char *broadcast = obs_data_get_string(settings, "broadcast"); - - bool changed = false; - - if (!ctx->url || strcmp(ctx->url, url) != 0) { - bfree(ctx->url); - ctx->url = bstrdup(url); - changed = true; - } - - if (!ctx->broadcast || strcmp(ctx->broadcast, broadcast) != 0) { - bfree(ctx->broadcast); - ctx->broadcast = bstrdup(broadcast); - changed = true; - } - - if (changed && ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0) { - hang_source_reconnect(ctx); - } -} - -static void hang_source_get_defaults(obs_data_t *settings) -{ - obs_data_set_default_string(settings, "url", "https://attention.us-central-2.ooda.video:4443"); - obs_data_set_default_string(settings, "broadcast", "flyover-ranch/cam_192_168_42_190"); -} - -static obs_properties_t *hang_source_properties(void *data) -{ - UNUSED_PARAMETER(data); - - obs_properties_t *props = obs_properties_create(); - - obs_properties_add_text(props, "url", "URL", OBS_TEXT_DEFAULT); - obs_properties_add_text(props, "broadcast", "Broadcast", OBS_TEXT_DEFAULT); - - return props; -} - -static void hang_source_tick(void *data, float seconds) -{ - UNUSED_PARAMETER(data); - UNUSED_PARAMETER(seconds); - // No per-frame updates needed -} - -static void hang_source_render(void *data, gs_effect_t *effect) -{ - UNUSED_PARAMETER(effect); - - struct hang_source *ctx = (struct hang_source *)data; - - if (!ctx->frame_buffer) - return; - - obs_source_draw(ctx->source, ctx->frame_buffer, FRAME_WIDTH * 4, FRAME_HEIGHT, false); -} - -// MoQ callback implementations -static void on_session_status(void *user_data, int32_t code) -{ - struct hang_source *ctx = (struct hang_source *)user_data; - - if (code == 0) { - LOG_INFO("MoQ session connected successfully"); - } else { - LOG_ERROR("MoQ session failed with code: %d", code); - } -} - -static void on_catalog(void *user_data, int32_t catalog) -{ - struct hang_source *ctx = (struct hang_source *)user_data; - - pthread_mutex_lock(&ctx->mutex); - - // Check if this is still the current generation - uint32_t current_gen = ctx->generation; - pthread_mutex_unlock(&ctx->mutex); - - if (catalog < 0) { - LOG_ERROR("Failed to get catalog: %d", catalog); - return; - } - - // Get video configuration - struct moq_video_config video_config; - if (moq_consume_video_config(catalog, 0, &video_config) < 0) { - LOG_ERROR("Failed to get video config"); - moq_consume_catalog_close(catalog); - return; - } - - // Initialize decoder with the video config - if (!hang_source_init_decoder(ctx, &video_config)) { - LOG_ERROR("Failed to initialize decoder"); - moq_consume_catalog_close(catalog); - return; - } - - // Subscribe to video track with minimal buffering - int32_t track = moq_consume_video_track(ctx->consume, 0, 0, on_video_frame, ctx); - if (track < 0) { - LOG_ERROR("Failed to subscribe to video track: %d", track); - moq_consume_catalog_close(catalog); - return; - } - - pthread_mutex_lock(&ctx->mutex); - if (ctx->generation == current_gen) { - ctx->video_track = track; - ctx->catalog_handle = catalog; - } - pthread_mutex_unlock(&ctx->mutex); - - LOG_INFO("Subscribed to video track successfully"); -} - -static void on_video_frame(void *user_data, int32_t frame_id) -{ - struct hang_source *ctx = (struct hang_source *)user_data; - - if (frame_id < 0) { - // End of stream or error - return; - } - - hang_source_decode_frame(ctx, frame_id); -} - -// Helper function implementations -static void hang_source_reconnect(struct hang_source *ctx) -{ - // Increment generation to invalidate old callbacks - pthread_mutex_lock(&ctx->mutex); - ctx->generation++; - uint32_t current_gen = ctx->generation; - hang_source_disconnect(ctx); - pthread_mutex_unlock(&ctx->mutex); - - // Create origin for consuming - ctx->origin = moq_origin_create(); - if (ctx->origin < 0) { - LOG_ERROR("Failed to create origin: %d", ctx->origin); - return; - } - - // Connect to MoQ server - ctx->session = moq_session_connect( - ctx->url, strlen(ctx->url), - 0, // origin_publish - ctx->origin, // origin_consume - on_session_status, ctx - ); - - if (ctx->session < 0) { - LOG_ERROR("Failed to connect to MoQ server: %d", ctx->session); - return; - } - - // Consume broadcast by path - ctx->consume = moq_origin_consume(ctx->origin, ctx->broadcast, strlen(ctx->broadcast)); - if (ctx->consume < 0) { - LOG_ERROR("Failed to consume broadcast: %d", ctx->consume); - return; - } - - // Subscribe to catalog updates - int32_t catalog = moq_consume_catalog(ctx->consume, on_catalog, ctx); - if (catalog < 0) { - LOG_ERROR("Failed to subscribe to catalog: %d", catalog); - return; - } - - LOG_INFO("Connecting to MoQ broadcast: %s @ %s", ctx->broadcast, ctx->url); -} - -static void hang_source_disconnect(struct hang_source *ctx) -{ - if (ctx->video_track >= 0) { - moq_consume_video_track_close(ctx->video_track); - ctx->video_track = -1; - } - - if (ctx->catalog_handle >= 0) { - moq_consume_catalog_close(ctx->catalog_handle); - ctx->catalog_handle = -1; - } - - if (ctx->consume >= 0) { - moq_consume_close(ctx->consume); - ctx->consume = -1; - } - - if (ctx->session >= 0) { - moq_session_close(ctx->session); - ctx->session = -1; - } - - if (ctx->origin >= 0) { - moq_origin_close(ctx->origin); - ctx->origin = -1; - } - - hang_source_destroy_decoder(ctx); - ctx->got_keyframe = false; -} - -static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) -{ - hang_source_destroy_decoder(ctx); - - // Find H.264 decoder - const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if (!codec) { - LOG_ERROR("H.264 codec not found"); - return false; - } - - // Create codec context - ctx->codec_ctx = avcodec_alloc_context3(codec); - if (!ctx->codec_ctx) { - LOG_ERROR("Failed to allocate codec context"); - return false; - } - - // Set codec parameters from config - if (config->coded_width && *config->coded_width > 0) { - ctx->codec_ctx->width = *config->coded_width; - ctx->frame.width = *config->coded_width; - } - if (config->coded_height && *config->coded_height > 0) { - ctx->codec_ctx->height = *config->coded_height; - ctx->frame.height = *config->coded_height; - } - - // Use codec description as extradata (contains SPS/PPS) - if (config->description && config->description_len > 0) { - ctx->codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); - if (ctx->codec_ctx->extradata) { - memcpy(ctx->codec_ctx->extradata, config->description, config->description_len); - ctx->codec_ctx->extradata_size = config->description_len; - } - } - - // Open codec - if (avcodec_open2(ctx->codec_ctx, codec, NULL) < 0) { - LOG_ERROR("Failed to open codec"); - hang_source_destroy_decoder(ctx); - return false; - } - - // Allocate frame buffer (RGBA for OBS) - ctx->frame.linesize[0] = ctx->frame.width * 4; - size_t buffer_size = ctx->frame.width * ctx->frame.height * 4; - ctx->frame_buffer = (uint8_t *)bmalloc(buffer_size); - - // Create scaling context for YUV420P -> RGBA conversion - ctx->sws_ctx = sws_getContext( - ctx->frame.width, ctx->frame.height, AV_PIX_FMT_YUV420P, - ctx->frame.width, ctx->frame.height, AV_PIX_FMT_RGBA, - SWS_BILINEAR, NULL, NULL, NULL - ); - - if (!ctx->sws_ctx) { - LOG_ERROR("Failed to create scaling context"); - hang_source_destroy_decoder(ctx); - return false; - } - - ctx->frame.format = VIDEO_FORMAT_RGBA; - ctx->frame.timestamp = 0; - - LOG_INFO("Decoder initialized: %dx%d", ctx->frame.width, ctx->frame.height); - return true; -} - -static void hang_source_destroy_decoder(struct hang_source *ctx) -{ - if (ctx->sws_ctx) { - sws_freeContext(ctx->sws_ctx); - ctx->sws_ctx = NULL; - } - - if (ctx->codec_ctx) { - avcodec_free_context(&ctx->codec_ctx); - ctx->codec_ctx = NULL; - } - - if (ctx->frame_buffer) { - bfree(ctx->frame_buffer); - ctx->frame_buffer = NULL; - } -} - -static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) -{ - if (!ctx->codec_ctx) - return; - - // Get frame data - struct moq_frame frame_data; - if (moq_consume_frame_chunk(frame_id, 0, &frame_data) < 0) { - LOG_ERROR("Failed to get frame data"); - moq_consume_frame_close(frame_id); - return; - } - - // Skip non-keyframes until we get the first one - if (!ctx->got_keyframe && !frame_data.keyframe) { - moq_consume_frame_close(frame_id); - return; - } - - // Create AVPacket from frame data - AVPacket *packet = av_packet_alloc(); - if (!packet) { - moq_consume_frame_close(frame_id); - return; - } - - packet->data = (uint8_t *)frame_data.payload; - packet->size = frame_data.payload_size; - packet->pts = frame_data.timestamp_us / 1000; // Convert to milliseconds - packet->dts = packet->pts; - - // Send packet to decoder - int ret = avcodec_send_packet(ctx->codec_ctx, packet); - av_packet_free(&packet); - - if (ret < 0) { - if (ret != AVERROR(EAGAIN)) { - char errbuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(ret, errbuf, sizeof(errbuf)); - LOG_ERROR("Error sending packet to decoder: %s", errbuf); - } - moq_consume_frame_close(frame_id); - return; - } - - // Receive decoded frames - AVFrame *frame = av_frame_alloc(); - if (!frame) { - moq_consume_frame_close(frame_id); - return; - } - - ret = avcodec_receive_frame(ctx->codec_ctx, frame); - if (ret < 0) { - if (ret != AVERROR(EAGAIN)) { - char errbuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(ret, errbuf, sizeof(errbuf)); - LOG_ERROR("Error receiving frame from decoder: %s", errbuf); - } - av_frame_free(&frame); - moq_consume_frame_close(frame_id); - return; - } - - // Mark that we've received a keyframe - if (frame->key_frame) - ctx->got_keyframe = true; - - // Convert YUV420P to RGBA - uint8_t *dst_data[4] = {ctx->frame_buffer, NULL, NULL, NULL}; - int dst_linesize[4] = {ctx->frame.width * 4, 0, 0, 0}; - - sws_scale(ctx->sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, - 0, ctx->frame.height, dst_data, dst_linesize); - - // Update OBS frame timestamp and output - ctx->frame.timestamp = frame_data.timestamp_us; - obs_source_output_video(ctx->source, &ctx->frame); - - av_frame_free(&frame); - moq_consume_frame_close(frame_id); -} - -// Registration function -void register_hang_source() -{ - struct obs_source_info info = {}; - info.id = "hang_source"; - info.type = OBS_SOURCE_TYPE_INPUT; - info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_DO_NOT_DUPLICATE; - info.get_name = [](void *) -> const char * { - return "Hang Source (MoQ)"; - }; - info.create = hang_source_create; - info.destroy = hang_source_destroy; - info.update = hang_source_update; - info.get_defaults = hang_source_get_defaults; - info.get_properties = hang_source_properties; - info.video_tick = hang_source_tick; - info.video_render = hang_source_render; - - obs_register_source(&info); -} From 6a4911ec9ac89b7d04671f3c9a859ea5ab19906a Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 12:48:55 -0600 Subject: [PATCH 07/12] hang-source: Fix thread safety and dynamic resolution handling Thread safety improvements: - Replace volatile with std::atomic for shutting_down and generation fields - Use proper atomic operations (.load()/.store()) for generation counter - Copy url/broadcast while holding mutex before logging in video_tick to prevent use-after-free race condition Memory safety fixes: - Use av_mallocz instead of av_malloc for codec extradata to ensure padding bytes are zero-initialized - Detect mid-stream resolution changes and reinitialize scaler context and frame buffer to prevent out-of-bounds memory access Documentation: - Add detailed comment explaining the 100ms callback drain delay limitation and suggest reference counting as a more robust solution --- src/hang-source.cpp | 103 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index 92af819..9ae09de 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -4,6 +4,8 @@ #include #include +#include + extern "C" { #include #include @@ -31,10 +33,10 @@ struct hang_source { bool reconnect_pending; // True if we need to reconnect after debounce // Shutdown flag - set when destroy begins, callbacks should exit early - volatile bool shutting_down; + std::atomic shutting_down; // Session handles (all negative = invalid) - volatile uint32_t generation; // Increments on reconnect + std::atomic generation; // Increments on reconnect bool reconnect_in_progress; // True while reconnect is happening int32_t origin; int32_t session; @@ -135,8 +137,20 @@ static void hang_source_destroy(void *data) hang_source_disconnect_locked(ctx); pthread_mutex_unlock(&ctx->mutex); - // Give MoQ callbacks time to drain - they check shutting_down and exit early - // This prevents use-after-free when async callbacks fire after ctx is freed + // Give MoQ callbacks time to drain - they check shutting_down and exit early. + // This prevents use-after-free when async callbacks fire after ctx is freed. + // + // LIMITATION: This 100ms sleep is a timing-based workaround, not a synchronization + // guarantee. If a callback is mid-execution when shutting_down is set AND takes + // longer than 100ms to complete (after the mutex unlock), there is still a + // potential race condition. In practice, our callbacks are fast (< 1ms typically) + // and this delay provides sufficient margin. However, a more robust solution + // would use reference counting: + // - Increment refcount when entering a callback + // - Decrement when exiting + // - Wait for refcount to reach zero before freeing ctx + // This could be implemented using std::shared_ptr or a manual atomic refcount + // with a condition variable for waiting. os_sleep_ms(100); bfree(ctx->url); @@ -267,10 +281,16 @@ static void hang_source_video_tick(void *data, float seconds) return; } + // Copy url and broadcast while holding mutex to avoid race condition in LOG_INFO + char *url_for_log = bstrdup(ctx->url); + char *broadcast_for_log = bstrdup(ctx->broadcast); + pthread_mutex_unlock(&ctx->mutex); // Valid settings - reconnect - LOG_INFO("Debounce complete, reconnecting to %s / %s", ctx->url, ctx->broadcast); + LOG_INFO("Debounce complete, reconnecting to %s / %s", url_for_log, broadcast_for_log); + bfree(url_for_log); + bfree(broadcast_for_log); hang_source_reconnect(ctx); } @@ -441,9 +461,9 @@ static void hang_source_reconnect(struct hang_source *ctx) } ctx->reconnect_in_progress = true; - uint32_t new_gen = ctx->generation + 1; - LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation, new_gen); - ctx->generation = new_gen; + uint32_t new_gen = ctx->generation.load() + 1; + LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation.load(), new_gen); + ctx->generation.store(new_gen); hang_source_disconnect_locked(ctx); // Copy URL while holding mutex for thread safety @@ -656,7 +676,7 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v // Use codec description as extradata (contains SPS/PPS) if (config->description && config->description_len > 0) { - new_codec_ctx->extradata = (uint8_t *)av_malloc(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); + new_codec_ctx->extradata = (uint8_t *)av_mallocz(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); if (new_codec_ctx->extradata) { memcpy(new_codec_ctx->extradata, config->description, config->description_len); new_codec_ctx->extradata_size = config->description_len; @@ -875,6 +895,71 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Successfully decoded a frame - reset error counter ctx->consecutive_decode_errors = 0; + // Validate decoded frame dimensions against ctx->frame and reinitialize if they differ + // This prevents OOB reads/writes when the stream resolution changes mid-stream + if (frame->width != (int)ctx->frame.width || frame->height != (int)ctx->frame.height) { + LOG_INFO("Decoded frame dimensions changed: %dx%d -> %dx%d, reinitializing scaler", + ctx->frame.width, ctx->frame.height, frame->width, frame->height); + + // Validate that dimensions are positive and reasonable + if (frame->width <= 0 || frame->height <= 0 || + frame->width > 16384 || frame->height > 16384) { + LOG_ERROR("Invalid decoded frame dimensions: %dx%d", frame->width, frame->height); + av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + + // Free old sws context + if (ctx->sws_ctx) { + sws_freeContext(ctx->sws_ctx); + ctx->sws_ctx = NULL; + } + + // Create new scaling context with the new dimensions + struct SwsContext *new_sws_ctx = sws_getContext( + frame->width, frame->height, AV_PIX_FMT_YUV420P, + frame->width, frame->height, AV_PIX_FMT_RGBA, + SWS_BILINEAR, NULL, NULL, NULL + ); + if (!new_sws_ctx) { + LOG_ERROR("Failed to create scaling context for %dx%d", frame->width, frame->height); + av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + + // Reallocate frame buffer for new dimensions (width * height * 4 for RGBA) + size_t new_buffer_size = (size_t)frame->width * (size_t)frame->height * 4; + uint8_t *new_frame_buffer = (uint8_t *)bmalloc(new_buffer_size); + if (!new_frame_buffer) { + LOG_ERROR("Failed to allocate frame buffer for %dx%d (%zu bytes)", + frame->width, frame->height, new_buffer_size); + sws_freeContext(new_sws_ctx); + av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + + // Free old frame buffer + if (ctx->frame_buffer) { + bfree(ctx->frame_buffer); + } + + // Install new state + ctx->sws_ctx = new_sws_ctx; + ctx->frame_buffer = new_frame_buffer; + ctx->frame.width = frame->width; + ctx->frame.height = frame->height; + ctx->frame.linesize[0] = frame->width * 4; + ctx->frame.data[0] = new_frame_buffer; + + LOG_INFO("Scaler reinitialized for %dx%d", frame->width, frame->height); + } + // Convert YUV420P to RGBA uint8_t *dst_data[4] = {ctx->frame_buffer, NULL, NULL, NULL}; int dst_linesize[4] = {static_cast(ctx->frame.width * 4), 0, 0, 0}; From 9756763283cb8a7566e60c42d6a60ec3a6567a69 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Thu, 18 Dec 2025 13:04:17 -0600 Subject: [PATCH 08/12] hang-source: Remove hardcoded codec and format assumptions Replace static H.264/YUV420P/1080p assumptions with dynamic detection from moq_video_config and decoded frame properties. Codec detection: - Add codec_string_to_id() to map catalog codec strings to FFmpeg IDs - Support H.264 (h264/avc/avc1), HEVC (hevc/h265/hev1/hvc1), VP9 (vp9/vp09), AV1 (av1/av01), and VP8 - Read codec from config->codec/codec_len instead of hardcoding H.264 Pixel format handling: - Add current_pix_fmt field to track actual decoded pixel format - Query frame->format after decode instead of assuming YUV420P - Recreate swscale context when pixel format changes - Supports any format: YUV420P, YUV420P10LE, YUV444P, NV12, etc. Resolution handling: - Remove FRAME_WIDTH/FRAME_HEIGHT (1920x1080) constants - Initialize frame dimensions to 0, set dynamically from stream - Try to get dimensions from codec context after avcodec_open2() if not available in moq_video_config - Existing mid-stream resolution change handling preserved Deferred initialization: - Defer sws_ctx and frame_buffer allocation to first decoded frame - Allows proper format detection before committing to scaler config - Reduces wasted allocations when config dimensions are unavailable Logging improvements: - Include av_get_pix_fmt_name() for readable format names - Log codec string, dimensions, and pixel format on initialization - Log format/dimension changes when scaler is reinitialized --- src/hang-source.cpp | 178 ++++++++++++++++++++++++++++++++------------ 1 file changed, 131 insertions(+), 47 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index 9ae09de..fbceff1 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -9,6 +9,7 @@ extern "C" { #include #include +#include #include #include "moq.h" } @@ -16,8 +17,47 @@ extern "C" { #include "hang-source.h" #include "logger.h" -#define FRAME_WIDTH 1920 -#define FRAME_HEIGHT 1080 +// Map codec string from moq_video_config to FFmpeg codec ID +static AVCodecID codec_string_to_id(const char *codec, size_t len) +{ + if (!codec || len == 0) { + return AV_CODEC_ID_NONE; + } + + // H.264/AVC + if ((len >= 4 && strncasecmp(codec, "h264", 4) == 0) || + (len >= 4 && strncasecmp(codec, "avc1", 4) == 0) || + (len >= 3 && strncasecmp(codec, "avc", 3) == 0)) { + return AV_CODEC_ID_H264; + } + + // HEVC/H.265 + if ((len >= 4 && strncasecmp(codec, "hevc", 4) == 0) || + (len >= 4 && strncasecmp(codec, "h265", 4) == 0) || + (len >= 4 && strncasecmp(codec, "hev1", 4) == 0) || + (len >= 4 && strncasecmp(codec, "hvc1", 4) == 0)) { + return AV_CODEC_ID_HEVC; + } + + // VP9 + if ((len >= 3 && strncasecmp(codec, "vp9", 3) == 0) || + (len >= 4 && strncasecmp(codec, "vp09", 4) == 0)) { + return AV_CODEC_ID_VP9; + } + + // AV1 + if ((len >= 3 && strncasecmp(codec, "av1", 3) == 0) || + (len >= 4 && strncasecmp(codec, "av01", 4) == 0)) { + return AV_CODEC_ID_AV1; + } + + // VP8 + if (len >= 3 && strncasecmp(codec, "vp8", 3) == 0) { + return AV_CODEC_ID_VP8; + } + + return AV_CODEC_ID_NONE; +} struct hang_source { obs_source_t *source; @@ -46,6 +86,8 @@ struct hang_source { // Decoder state AVCodecContext *codec_ctx; + AVCodecID current_codec_id; // Currently configured codec + enum AVPixelFormat current_pix_fmt; // Current pixel format for sws_ctx struct SwsContext *sws_ctx; bool got_keyframe; uint32_t frames_waiting_for_keyframe; // Count of skipped frames while waiting @@ -107,6 +149,8 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) // Initialize decoder state ctx->codec_ctx = NULL; + ctx->current_codec_id = AV_CODEC_ID_NONE; + ctx->current_pix_fmt = AV_PIX_FMT_NONE; ctx->sws_ctx = NULL; ctx->got_keyframe = false; ctx->frames_waiting_for_keyframe = 0; @@ -116,11 +160,11 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) // Initialize threading pthread_mutex_init(&ctx->mutex, NULL); - // Initialize OBS frame structure - ctx->frame.width = FRAME_WIDTH; - ctx->frame.height = FRAME_HEIGHT; + // Initialize OBS frame structure - dimensions will be set dynamically from stream + ctx->frame.width = 0; + ctx->frame.height = 0; ctx->frame.format = VIDEO_FORMAT_RGBA; - ctx->frame.linesize[0] = FRAME_WIDTH * 4; + ctx->frame.linesize[0] = 0; hang_source_update(ctx, settings); @@ -647,10 +691,23 @@ static void hang_source_blank_video(struct hang_source *ctx) static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) { - // Find H.264 decoder (can be done outside mutex) - const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + // Map codec string to FFmpeg codec ID dynamically + AVCodecID codec_id = codec_string_to_id(config->codec, config->codec_len); + if (codec_id == AV_CODEC_ID_NONE) { + // Log the codec string for debugging (may not be null-terminated) + char codec_str[64] = {0}; + size_t copy_len = config->codec_len < sizeof(codec_str) - 1 ? config->codec_len : sizeof(codec_str) - 1; + if (config->codec && copy_len > 0) { + memcpy(codec_str, config->codec, copy_len); + } + LOG_ERROR("Unknown or unsupported codec: '%s'", codec_str); + return false; + } + + // Find decoder for the codec + const AVCodec *codec = avcodec_find_decoder(codec_id); if (!codec) { - LOG_ERROR("H.264 codec not found"); + LOG_ERROR("Decoder not found for codec ID: %d", codec_id); return false; } @@ -661,10 +718,10 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v return false; } - uint32_t width = FRAME_WIDTH; - uint32_t height = FRAME_HEIGHT; + // Get dimensions from config - required for buffer allocation + uint32_t width = 0; + uint32_t height = 0; - // Set codec parameters from config if (config->coded_width && *config->coded_width > 0) { new_codec_ctx->width = *config->coded_width; width = *config->coded_width; @@ -674,7 +731,7 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v height = *config->coded_height; } - // Use codec description as extradata (contains SPS/PPS) + // Use codec description as extradata (contains SPS/PPS for H.264, VPS/SPS/PPS for HEVC, etc.) if (config->description && config->description_len > 0) { new_codec_ctx->extradata = (uint8_t *)av_mallocz(config->description_len + AV_INPUT_BUFFER_PADDING_SIZE); if (new_codec_ctx->extradata) { @@ -690,27 +747,13 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v return false; } - // Allocate frame buffer (RGBA for OBS) - size_t buffer_size = width * height * 4; - uint8_t *new_frame_buffer = (uint8_t *)bmalloc(buffer_size); - if (!new_frame_buffer) { - LOG_ERROR("Failed to allocate frame buffer"); - avcodec_free_context(&new_codec_ctx); - return false; + // If dimensions weren't in config, try to get them from the opened codec context + // (may have been parsed from extradata) + if (width == 0 && new_codec_ctx->width > 0) { + width = new_codec_ctx->width; } - - // Create scaling context for YUV420P -> RGBA conversion - struct SwsContext *new_sws_ctx = sws_getContext( - width, height, AV_PIX_FMT_YUV420P, - width, height, AV_PIX_FMT_RGBA, - SWS_BILINEAR, NULL, NULL, NULL - ); - - if (!new_sws_ctx) { - LOG_ERROR("Failed to create scaling context"); - bfree(new_frame_buffer); - avcodec_free_context(&new_codec_ctx); - return false; + if (height == 0 && new_codec_ctx->height > 0) { + height = new_codec_ctx->height; } // Now take the mutex and swap in the new decoder state @@ -728,13 +771,17 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v } // Install new decoder state + // Note: sws_ctx, frame_buffer, and frame dimensions will be initialized + // dynamically on first decoded frame when we know the actual pixel format ctx->codec_ctx = new_codec_ctx; - ctx->sws_ctx = new_sws_ctx; - ctx->frame_buffer = new_frame_buffer; + ctx->current_codec_id = codec_id; + ctx->current_pix_fmt = AV_PIX_FMT_NONE; // Will be set on first frame + ctx->sws_ctx = NULL; // Will be created on first frame with actual pixel format + ctx->frame_buffer = NULL; // Will be allocated on first frame with actual dimensions ctx->frame.width = width; ctx->frame.height = height; ctx->frame.linesize[0] = width * 4; - ctx->frame.data[0] = new_frame_buffer; + ctx->frame.data[0] = NULL; ctx->frame.format = VIDEO_FORMAT_RGBA; ctx->frame.timestamp = 0; ctx->got_keyframe = false; @@ -743,7 +790,14 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v pthread_mutex_unlock(&ctx->mutex); - LOG_INFO("Decoder initialized: %dx%d", width, height); + // Log codec name for debugging + char codec_str[64] = {0}; + size_t copy_len = config->codec_len < sizeof(codec_str) - 1 ? config->codec_len : sizeof(codec_str) - 1; + if (config->codec && copy_len > 0) { + memcpy(codec_str, config->codec, copy_len); + } + LOG_INFO("Decoder initialized: codec=%s, dimensions=%ux%u (may be refined on first frame)", + codec_str, width, height); return true; } @@ -765,6 +819,10 @@ static void hang_source_destroy_decoder_locked(struct hang_source *ctx) ctx->frame_buffer = NULL; ctx->frame.data[0] = NULL; } + + // Reset dynamic format tracking + ctx->current_codec_id = AV_CODEC_ID_NONE; + ctx->current_pix_fmt = AV_PIX_FMT_NONE; } static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) @@ -779,7 +837,8 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) } // Check if decoder is still valid (may have been destroyed during reconnect) - if (!ctx->codec_ctx || !ctx->sws_ctx || !ctx->frame_buffer) { + // Note: sws_ctx and frame_buffer may be NULL on first frame - they're created dynamically + if (!ctx->codec_ctx) { pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; @@ -895,11 +954,22 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Successfully decoded a frame - reset error counter ctx->consecutive_decode_errors = 0; - // Validate decoded frame dimensions against ctx->frame and reinitialize if they differ - // This prevents OOB reads/writes when the stream resolution changes mid-stream - if (frame->width != (int)ctx->frame.width || frame->height != (int)ctx->frame.height) { - LOG_INFO("Decoded frame dimensions changed: %dx%d -> %dx%d, reinitializing scaler", - ctx->frame.width, ctx->frame.height, frame->width, frame->height); + // Check if we need to (re)initialize the scaler - either first frame, dimension change, or pixel format change + enum AVPixelFormat decoded_pix_fmt = (enum AVPixelFormat)frame->format; + bool dimensions_changed = (frame->width != (int)ctx->frame.width || frame->height != (int)ctx->frame.height); + bool pix_fmt_changed = (decoded_pix_fmt != ctx->current_pix_fmt); + bool need_reinit = (!ctx->sws_ctx || !ctx->frame_buffer || dimensions_changed || pix_fmt_changed); + + if (need_reinit) { + if (dimensions_changed) { + LOG_INFO("Decoded frame dimensions changed: %ux%u -> %dx%d", + ctx->frame.width, ctx->frame.height, frame->width, frame->height); + } + if (pix_fmt_changed) { + LOG_INFO("Decoded frame pixel format changed: %d -> %d (%s)", + ctx->current_pix_fmt, decoded_pix_fmt, + av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); + } // Validate that dimensions are positive and reasonable if (frame->width <= 0 || frame->height <= 0 || @@ -911,20 +981,31 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) return; } + // Validate pixel format is supported by swscale + if (decoded_pix_fmt == AV_PIX_FMT_NONE) { + LOG_ERROR("Invalid decoded frame pixel format: %d", decoded_pix_fmt); + av_frame_free(&frame); + pthread_mutex_unlock(&ctx->mutex); + moq_consume_frame_close(frame_id); + return; + } + // Free old sws context if (ctx->sws_ctx) { sws_freeContext(ctx->sws_ctx); ctx->sws_ctx = NULL; } - // Create new scaling context with the new dimensions + // Create new scaling context with the actual pixel format from the decoded frame struct SwsContext *new_sws_ctx = sws_getContext( - frame->width, frame->height, AV_PIX_FMT_YUV420P, + frame->width, frame->height, decoded_pix_fmt, frame->width, frame->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL ); if (!new_sws_ctx) { - LOG_ERROR("Failed to create scaling context for %dx%d", frame->width, frame->height); + LOG_ERROR("Failed to create scaling context for %dx%d pix_fmt=%d (%s)", + frame->width, frame->height, decoded_pix_fmt, + av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); av_frame_free(&frame); pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); @@ -951,13 +1032,16 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Install new state ctx->sws_ctx = new_sws_ctx; + ctx->current_pix_fmt = decoded_pix_fmt; ctx->frame_buffer = new_frame_buffer; ctx->frame.width = frame->width; ctx->frame.height = frame->height; ctx->frame.linesize[0] = frame->width * 4; ctx->frame.data[0] = new_frame_buffer; - LOG_INFO("Scaler reinitialized for %dx%d", frame->width, frame->height); + LOG_INFO("Scaler initialized for %dx%d pix_fmt=%s", + frame->width, frame->height, + av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); } // Convert YUV420P to RGBA From b5fccc70afe959249bc3e768d371e7db09527fc9 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Fri, 19 Dec 2025 04:52:52 -0600 Subject: [PATCH 09/12] updated defaults to localhost --- src/hang-source.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index fbceff1..4770d65 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -246,8 +246,8 @@ static void hang_source_update(void *data, obs_data_t *settings) static void hang_source_get_defaults(obs_data_t *settings) { - obs_data_set_default_string(settings, "url", "https://attention.us-central-2.ooda.video:4443"); - obs_data_set_default_string(settings, "broadcast", "flyover-ranch/cam_192_168_42_190"); + obs_data_set_default_string(settings, "url", "http://localhost:4443"); + obs_data_set_default_string(settings, "broadcast", "obs/test"); } static obs_properties_t *hang_source_properties(void *data) From 84fa11617d8e65deca4a93c5af276356a0e3105c Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Sat, 20 Dec 2025 07:08:50 -0600 Subject: [PATCH 10/12] Simplify hang_source: remove debounce mechanism and reduce mutex contention Major refactoring to simplify the hang_source plugin architecture: Removed debounce mechanism: - Removed pending_url, pending_broadcast, settings_changed_time, reconnect_pending fields - Removed DEBOUNCE_DELAY_MS constant and video_tick polling function - Settings now apply immediately when user clicks OK (not on every keystroke) Simplified settings flow: - hang_source_update() now detects if settings actually changed - Auto-reconnects when settings change to valid values - Auto-disconnects and blanks video when settings become invalid - No more pending vs active settings distinction Reduced mutex contention: - Added fast-path shutting_down.load() checks before acquiring mutex in callbacks - on_session_status, on_catalog, on_video_frame, hang_source_decode_frame all check the atomic flag first for early exit without lock acquisition Result: 62 fewer lines of code, cleaner architecture, same functionality. --- src/hang-source.cpp | 192 +++++++++++++++----------------------------- 1 file changed, 65 insertions(+), 127 deletions(-) diff --git a/src/hang-source.cpp b/src/hang-source.cpp index 4770d65..27eee6a 100644 --- a/src/hang-source.cpp +++ b/src/hang-source.cpp @@ -66,12 +66,6 @@ struct hang_source { char *url; char *broadcast; - // Pending settings - what user has typed but not yet applied - char *pending_url; - char *pending_broadcast; - uint64_t settings_changed_time; // Timestamp when settings last changed - bool reconnect_pending; // True if we need to reconnect after debounce - // Shutdown flag - set when destroy begins, callbacks should exit early std::atomic shutting_down; @@ -101,13 +95,9 @@ struct hang_source { pthread_mutex_t mutex; }; -// Debounce delay in milliseconds (500ms = user stops typing for half a second) -#define DEBOUNCE_DELAY_MS 500 - // Forward declarations static void hang_source_update(void *data, obs_data_t *settings); static void hang_source_destroy(void *data); -static void hang_source_video_tick(void *data, float seconds); static obs_properties_t *hang_source_properties(void *data); static void hang_source_get_defaults(obs_data_t *settings); @@ -129,12 +119,6 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); ctx->source = source; - // Initialize pending settings - ctx->pending_url = NULL; - ctx->pending_broadcast = NULL; - ctx->settings_changed_time = 0; - ctx->reconnect_pending = false; - // Initialize shutdown flag ctx->shutting_down = false; @@ -166,6 +150,8 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) ctx->frame.format = VIDEO_FORMAT_RGBA; ctx->frame.linesize[0] = 0; + // Load settings from OBS - this will auto-connect if settings are valid + // (hang_source_update detects settings changed from NULL and reconnects) hang_source_update(ctx, settings); return ctx; @@ -199,8 +185,6 @@ static void hang_source_destroy(void *data) bfree(ctx->url); bfree(ctx->broadcast); - bfree(ctx->pending_url); - bfree(ctx->pending_broadcast); // Note: frame_buffer is already freed by hang_source_disconnect_locked pthread_mutex_destroy(&ctx->mutex); @@ -217,31 +201,39 @@ static void hang_source_update(void *data, obs_data_t *settings) pthread_mutex_lock(&ctx->mutex); - // Check if pending settings have changed - bool pending_changed = false; - - if (!ctx->pending_url || strcmp(ctx->pending_url, url) != 0) { - bfree(ctx->pending_url); - ctx->pending_url = bstrdup(url); - pending_changed = true; - } + // Check if settings actually changed + bool url_changed = (!ctx->url && url && strlen(url) > 0) || + (ctx->url && !url) || + (ctx->url && url && strcmp(ctx->url, url) != 0); + bool broadcast_changed = (!ctx->broadcast && broadcast && strlen(broadcast) > 0) || + (ctx->broadcast && !broadcast) || + (ctx->broadcast && broadcast && strcmp(ctx->broadcast, broadcast) != 0); + bool settings_changed = url_changed || broadcast_changed; - if (!ctx->pending_broadcast || strcmp(ctx->pending_broadcast, broadcast) != 0) { - bfree(ctx->pending_broadcast); - ctx->pending_broadcast = bstrdup(broadcast); - pending_changed = true; - } + // Store the new settings + bfree(ctx->url); + ctx->url = bstrdup(url); + bfree(ctx->broadcast); + ctx->broadcast = bstrdup(broadcast); - if (pending_changed) { - // Record the time of this change and mark reconnect as pending - // The actual reconnect will happen in video_tick after debounce delay - ctx->settings_changed_time = os_gettime_ns(); - ctx->reconnect_pending = true; - LOG_DEBUG("Settings changed, scheduling reconnect after debounce (url=%s, broadcast=%s)", - url ? url : "(null)", broadcast ? broadcast : "(null)"); - } + // Check if new settings are valid for connection + bool valid = ctx->url && ctx->broadcast && + strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0; pthread_mutex_unlock(&ctx->mutex); + + // If settings changed and are valid, reconnect + if (settings_changed && valid) { + LOG_INFO("Settings changed, reconnecting (url=%s, broadcast=%s)", + url ? url : "(null)", broadcast ? broadcast : "(null)"); + hang_source_reconnect(ctx); + } else if (settings_changed && !valid) { + LOG_INFO("Settings changed but invalid - disconnecting"); + pthread_mutex_lock(&ctx->mutex); + hang_source_disconnect_locked(ctx); + pthread_mutex_unlock(&ctx->mutex); + hang_source_blank_video(ctx); + } } static void hang_source_get_defaults(obs_data_t *settings) @@ -262,82 +254,6 @@ static obs_properties_t *hang_source_properties(void *data) return props; } -// video_tick handles debounced reconnection - waits for user to stop typing -static void hang_source_video_tick(void *data, float seconds) -{ - UNUSED_PARAMETER(seconds); - struct hang_source *ctx = (struct hang_source *)data; - - pthread_mutex_lock(&ctx->mutex); - - // Don't process during shutdown - if (ctx->shutting_down) { - pthread_mutex_unlock(&ctx->mutex); - return; - } - - if (!ctx->reconnect_pending) { - pthread_mutex_unlock(&ctx->mutex); - return; - } - - // Check if enough time has passed since last settings change (debounce) - uint64_t now = os_gettime_ns(); - uint64_t elapsed_ms = (now - ctx->settings_changed_time) / 1000000; - - if (elapsed_ms < DEBOUNCE_DELAY_MS) { - pthread_mutex_unlock(&ctx->mutex); - return; - } - - // Debounce period elapsed - time to apply the pending settings - ctx->reconnect_pending = false; - - // Check if pending settings differ from current active settings - bool url_changed = (!ctx->url && ctx->pending_url) || - (ctx->url && !ctx->pending_url) || - (ctx->url && ctx->pending_url && strcmp(ctx->url, ctx->pending_url) != 0); - bool broadcast_changed = (!ctx->broadcast && ctx->pending_broadcast) || - (ctx->broadcast && !ctx->pending_broadcast) || - (ctx->broadcast && ctx->pending_broadcast && strcmp(ctx->broadcast, ctx->pending_broadcast) != 0); - - if (!url_changed && !broadcast_changed) { - // No actual change from current active connection - pthread_mutex_unlock(&ctx->mutex); - return; - } - - // Apply pending settings as the new active settings - bfree(ctx->url); - ctx->url = ctx->pending_url ? bstrdup(ctx->pending_url) : NULL; - bfree(ctx->broadcast); - ctx->broadcast = ctx->pending_broadcast ? bstrdup(ctx->pending_broadcast) : NULL; - - // Check if the new settings are valid - bool valid = ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0; - - if (!valid) { - // Invalid settings - disconnect and blank video - LOG_INFO("Invalid URL or broadcast - disconnecting and blanking video"); - hang_source_disconnect_locked(ctx); - pthread_mutex_unlock(&ctx->mutex); - hang_source_blank_video(ctx); - return; - } - - // Copy url and broadcast while holding mutex to avoid race condition in LOG_INFO - char *url_for_log = bstrdup(ctx->url); - char *broadcast_for_log = bstrdup(ctx->broadcast); - - pthread_mutex_unlock(&ctx->mutex); - - // Valid settings - reconnect - LOG_INFO("Debounce complete, reconnecting to %s / %s", url_for_log, broadcast_for_log); - bfree(url_for_log); - bfree(broadcast_for_log); - hang_source_reconnect(ctx); -} - // Forward declaration for use in callback static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen); @@ -346,10 +262,15 @@ static void on_session_status(void *user_data, int32_t code) { struct hang_source *ctx = (struct hang_source *)user_data; - // Check if we're shutting down - exit early to avoid use-after-free - pthread_mutex_lock(&ctx->mutex); - if (ctx->shutting_down) { + // Fast path: check atomic flag before taking lock + if (ctx->shutting_down.load()) { LOG_DEBUG("Ignoring session status callback - shutting down"); + return; + } + + pthread_mutex_lock(&ctx->mutex); + // Double-check after acquiring lock (may have changed) + if (ctx->shutting_down.load()) { pthread_mutex_unlock(&ctx->mutex); return; } @@ -391,11 +312,18 @@ static void on_catalog(void *user_data, int32_t catalog) LOG_INFO("Catalog callback received: %d", catalog); + // Fast path: check atomic flag before taking lock + if (ctx->shutting_down.load()) { + LOG_DEBUG("Ignoring catalog callback - shutting down"); + if (catalog >= 0) + moq_consume_catalog_close(catalog); + return; + } + pthread_mutex_lock(&ctx->mutex); - // Check if we're shutting down - exit early to avoid use-after-free - if (ctx->shutting_down) { - LOG_DEBUG("Ignoring catalog callback - shutting down"); + // Double-check after acquiring lock (may have changed) + if (ctx->shutting_down.load()) { pthread_mutex_unlock(&ctx->mutex); if (catalog >= 0) moq_consume_catalog_close(catalog); @@ -470,12 +398,18 @@ static void on_video_frame(void *user_data, int32_t frame_id) return; } + // Fast path: check atomic flag before taking lock + if (ctx->shutting_down.load()) { + moq_consume_frame_close(frame_id); + return; + } + // Check if this callback is still valid using generation (not video_track) // Note: We can't check video_track here because frames may arrive before // the track handle is stored in on_catalog (race condition) pthread_mutex_lock(&ctx->mutex); - // Check if we're shutting down - exit early to avoid use-after-free - if (ctx->shutting_down) { + // Double-check after acquiring lock (may have changed) + if (ctx->shutting_down.load()) { pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; @@ -827,10 +761,16 @@ static void hang_source_destroy_decoder_locked(struct hang_source *ctx) static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) { + // Fast path: check atomic flag before taking lock + if (ctx->shutting_down.load()) { + moq_consume_frame_close(frame_id); + return; + } + pthread_mutex_lock(&ctx->mutex); - // Check if we're shutting down - exit early to avoid use-after-free - if (ctx->shutting_down) { + // Double-check after acquiring lock (may have changed) + if (ctx->shutting_down.load()) { pthread_mutex_unlock(&ctx->mutex); moq_consume_frame_close(frame_id); return; @@ -1075,8 +1015,6 @@ void register_hang_source() info.update = hang_source_update; info.get_defaults = hang_source_get_defaults; info.get_properties = hang_source_properties; - // video_tick is needed for debounced reconnection (blur simulation) - info.video_tick = hang_source_video_tick; obs_register_source(&info); } From fdd5257fa147b63de43f21f8b0e3bd5553feac46 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Wed, 24 Dec 2025 04:29:23 -0600 Subject: [PATCH 11/12] =?UTF-8?q?Rebranding:=20Hang=20Source=20=E2=9E=A1?= =?UTF-8?q?=EF=B8=8F=20=20MoQ=20Source=20hang=5Fsource=20=E2=9E=A1?= =?UTF-8?q?=EF=B8=8F=20=20moq=5Fsource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (filenames and variables too) --- CMakeLists.txt | 2 +- src/hang-source.h | 3 - src/{hang-source.cpp => moq-source.cpp} | 152 ++++++++++++------------ src/moq-source.h | 3 + src/obs-moq.cpp | 4 +- 5 files changed, 82 insertions(+), 82 deletions(-) delete mode 100644 src/hang-source.h rename src/{hang-source.cpp => moq-source.cpp} (89%) create mode 100644 src/moq-source.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cf2332d..240e0c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,7 +61,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${FFMPEG_LIBRARIES}) target_sources( ${CMAKE_PROJECT_NAME} PRIVATE src/obs-moq.cpp src/moq-output.h src/moq-service.h src/moq-output.cpp src/moq-service.cpp - src/hang-source.cpp src/hang-source.h + src/moq-source.cpp src/moq-source.h ) set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) diff --git a/src/hang-source.h b/src/hang-source.h deleted file mode 100644 index 30d13e4..0000000 --- a/src/hang-source.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void register_hang_source(); diff --git a/src/hang-source.cpp b/src/moq-source.cpp similarity index 89% rename from src/hang-source.cpp rename to src/moq-source.cpp index 27eee6a..0fd3f3a 100644 --- a/src/hang-source.cpp +++ b/src/moq-source.cpp @@ -14,7 +14,7 @@ extern "C" { #include "moq.h" } -#include "hang-source.h" +#include "moq-source.h" #include "logger.h" // Map codec string from moq_video_config to FFmpeg codec ID @@ -59,7 +59,7 @@ static AVCodecID codec_string_to_id(const char *codec, size_t len) return AV_CODEC_ID_NONE; } -struct hang_source { +struct moq_source { obs_source_t *source; // Settings - current active connection settings @@ -96,10 +96,10 @@ struct hang_source { }; // Forward declarations -static void hang_source_update(void *data, obs_data_t *settings); -static void hang_source_destroy(void *data); -static obs_properties_t *hang_source_properties(void *data); -static void hang_source_get_defaults(obs_data_t *settings); +static void moq_source_update(void *data, obs_data_t *settings); +static void moq_source_destroy(void *data); +static obs_properties_t *moq_source_properties(void *data); +static void moq_source_get_defaults(obs_data_t *settings); // MoQ callbacks static void on_session_status(void *user_data, int32_t code); @@ -107,16 +107,16 @@ static void on_catalog(void *user_data, int32_t catalog); static void on_video_frame(void *user_data, int32_t frame_id); // Helper functions -static void hang_source_reconnect(struct hang_source *ctx); -static void hang_source_disconnect_locked(struct hang_source *ctx); -static void hang_source_blank_video(struct hang_source *ctx); -static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config); -static void hang_source_destroy_decoder_locked(struct hang_source *ctx); -static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id); - -static void *hang_source_create(obs_data_t *settings, obs_source_t *source) +static void moq_source_reconnect(struct moq_source *ctx); +static void moq_source_disconnect_locked(struct moq_source *ctx); +static void moq_source_blank_video(struct moq_source *ctx); +static bool moq_source_init_decoder(struct moq_source *ctx, const struct moq_video_config *config); +static void moq_source_destroy_decoder_locked(struct moq_source *ctx); +static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id); + +static void *moq_source_create(obs_data_t *settings, obs_source_t *source) { - struct hang_source *ctx = (struct hang_source *)bzalloc(sizeof(struct hang_source)); + struct moq_source *ctx = (struct moq_source *)bzalloc(sizeof(struct moq_source)); ctx->source = source; // Initialize shutdown flag @@ -151,20 +151,20 @@ static void *hang_source_create(obs_data_t *settings, obs_source_t *source) ctx->frame.linesize[0] = 0; // Load settings from OBS - this will auto-connect if settings are valid - // (hang_source_update detects settings changed from NULL and reconnects) - hang_source_update(ctx, settings); + // (moq_source_update detects settings changed from NULL and reconnects) + moq_source_update(ctx, settings); return ctx; } -static void hang_source_destroy(void *data) +static void moq_source_destroy(void *data) { - struct hang_source *ctx = (struct hang_source *)data; + struct moq_source *ctx = (struct moq_source *)data; // Set shutdown flag first - callbacks will check this and exit early pthread_mutex_lock(&ctx->mutex); ctx->shutting_down = true; - hang_source_disconnect_locked(ctx); + moq_source_disconnect_locked(ctx); pthread_mutex_unlock(&ctx->mutex); // Give MoQ callbacks time to drain - they check shutting_down and exit early. @@ -185,16 +185,16 @@ static void hang_source_destroy(void *data) bfree(ctx->url); bfree(ctx->broadcast); - // Note: frame_buffer is already freed by hang_source_disconnect_locked + // Note: frame_buffer is already freed by moq_source_disconnect_locked pthread_mutex_destroy(&ctx->mutex); bfree(ctx); } -static void hang_source_update(void *data, obs_data_t *settings) +static void moq_source_update(void *data, obs_data_t *settings) { - struct hang_source *ctx = (struct hang_source *)data; + struct moq_source *ctx = (struct moq_source *)data; const char *url = obs_data_get_string(settings, "url"); const char *broadcast = obs_data_get_string(settings, "broadcast"); @@ -217,32 +217,32 @@ static void hang_source_update(void *data, obs_data_t *settings) ctx->broadcast = bstrdup(broadcast); // Check if new settings are valid for connection - bool valid = ctx->url && ctx->broadcast && + bool valid = ctx->url && ctx->broadcast && strlen(ctx->url) > 0 && strlen(ctx->broadcast) > 0; pthread_mutex_unlock(&ctx->mutex); // If settings changed and are valid, reconnect if (settings_changed && valid) { - LOG_INFO("Settings changed, reconnecting (url=%s, broadcast=%s)", + LOG_INFO("Settings changed, reconnecting (url=%s, broadcast=%s)", url ? url : "(null)", broadcast ? broadcast : "(null)"); - hang_source_reconnect(ctx); + moq_source_reconnect(ctx); } else if (settings_changed && !valid) { LOG_INFO("Settings changed but invalid - disconnecting"); pthread_mutex_lock(&ctx->mutex); - hang_source_disconnect_locked(ctx); + moq_source_disconnect_locked(ctx); pthread_mutex_unlock(&ctx->mutex); - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); } } -static void hang_source_get_defaults(obs_data_t *settings) +static void moq_source_get_defaults(obs_data_t *settings) { obs_data_set_default_string(settings, "url", "http://localhost:4443"); obs_data_set_default_string(settings, "broadcast", "obs/test"); } -static obs_properties_t *hang_source_properties(void *data) +static obs_properties_t *moq_source_properties(void *data) { UNUSED_PARAMETER(data); @@ -255,12 +255,12 @@ static obs_properties_t *hang_source_properties(void *data) } // Forward declaration for use in callback -static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen); +static void moq_source_start_consume(struct moq_source *ctx, uint32_t expected_gen); // MoQ callback implementations static void on_session_status(void *user_data, int32_t code) { - struct hang_source *ctx = (struct hang_source *)user_data; + struct moq_source *ctx = (struct moq_source *)user_data; // Fast path: check atomic flag before taking lock if (ctx->shutting_down.load()) { @@ -280,16 +280,16 @@ static void on_session_status(void *user_data, int32_t code) return; } uint32_t current_gen = ctx->generation; - + if (code == 0) { pthread_mutex_unlock(&ctx->mutex); LOG_INFO("MoQ session connected successfully (generation %u)", current_gen); // Now that we're connected, start consuming the broadcast - hang_source_start_consume(ctx, current_gen); + moq_source_start_consume(ctx, current_gen); } else { // Connection failed - clean up the session and origin immediately LOG_ERROR("MoQ session failed with code: %d (generation %u)", code, current_gen); - + // Clean up failed session/origin to prevent further callbacks if (ctx->session >= 0) { moq_session_close(ctx->session); @@ -300,15 +300,15 @@ static void on_session_status(void *user_data, int32_t code) ctx->origin = -1; } pthread_mutex_unlock(&ctx->mutex); - + // Blank the video to show error state - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); } } static void on_catalog(void *user_data, int32_t catalog) { - struct hang_source *ctx = (struct hang_source *)user_data; + struct moq_source *ctx = (struct moq_source *)user_data; LOG_INFO("Catalog callback received: %d", catalog); @@ -345,7 +345,7 @@ static void on_catalog(void *user_data, int32_t catalog) if (catalog < 0) { LOG_ERROR("Failed to get catalog: %d", catalog); // Catalog failed (likely invalid broadcast) - blank video - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); return; } @@ -358,7 +358,7 @@ static void on_catalog(void *user_data, int32_t catalog) } // Initialize decoder with the video config (takes mutex internally) - if (!hang_source_init_decoder(ctx, &video_config)) { + if (!moq_source_init_decoder(ctx, &video_config)) { LOG_ERROR("Failed to initialize decoder"); moq_consume_catalog_close(catalog); return; @@ -391,7 +391,7 @@ static void on_catalog(void *user_data, int32_t catalog) static void on_video_frame(void *user_data, int32_t frame_id) { - struct hang_source *ctx = (struct hang_source *)user_data; + struct moq_source *ctx = (struct moq_source *)user_data; if (frame_id < 0) { LOG_ERROR("Video frame callback with error: %d", frame_id); @@ -422,34 +422,34 @@ static void on_video_frame(void *user_data, int32_t frame_id) } pthread_mutex_unlock(&ctx->mutex); - hang_source_decode_frame(ctx, frame_id); + moq_source_decode_frame(ctx, frame_id); } // Helper function implementations -static void hang_source_reconnect(struct hang_source *ctx) +static void moq_source_reconnect(struct moq_source *ctx) { // Increment generation to invalidate old callbacks pthread_mutex_lock(&ctx->mutex); - + // Check if reconnect is already in progress if (ctx->reconnect_in_progress) { LOG_DEBUG("Reconnect already in progress, skipping"); pthread_mutex_unlock(&ctx->mutex); return; } - + ctx->reconnect_in_progress = true; uint32_t new_gen = ctx->generation.load() + 1; LOG_INFO("Reconnecting (generation %u -> %u)", ctx->generation.load(), new_gen); ctx->generation.store(new_gen); - hang_source_disconnect_locked(ctx); + moq_source_disconnect_locked(ctx); // Copy URL while holding mutex for thread safety char *url_copy = bstrdup(ctx->url); pthread_mutex_unlock(&ctx->mutex); // Blank video while reconnecting to avoid showing stale frames - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); // Small delay to allow MoQ library to fully clean up previous connection os_sleep_ms(50); @@ -503,7 +503,7 @@ static void hang_source_reconnect(struct hang_source *ctx) } // Called after session is connected successfully -static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected_gen) +static void moq_source_start_consume(struct moq_source *ctx, uint32_t expected_gen) { // Check if origin is still valid and generation matches pthread_mutex_lock(&ctx->mutex); @@ -535,7 +535,7 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected } } pthread_mutex_unlock(&ctx->mutex); - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); return; } @@ -573,7 +573,7 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected } } pthread_mutex_unlock(&ctx->mutex); - hang_source_blank_video(ctx); + moq_source_blank_video(ctx); return; } @@ -582,7 +582,7 @@ static void hang_source_start_consume(struct hang_source *ctx, uint32_t expected } // NOTE: Caller must hold ctx->mutex when calling this function -static void hang_source_disconnect_locked(struct hang_source *ctx) +static void moq_source_disconnect_locked(struct moq_source *ctx) { if (ctx->video_track >= 0) { moq_consume_video_close(ctx->video_track); @@ -609,21 +609,21 @@ static void hang_source_disconnect_locked(struct hang_source *ctx) ctx->origin = -1; } - hang_source_destroy_decoder_locked(ctx); + moq_source_destroy_decoder_locked(ctx); ctx->got_keyframe = false; ctx->frames_waiting_for_keyframe = 0; ctx->consecutive_decode_errors = 0; } // Blanks the video preview by outputting a NULL frame -static void hang_source_blank_video(struct hang_source *ctx) +static void moq_source_blank_video(struct moq_source *ctx) { // Passing NULL to obs_source_output_video clears the current frame obs_source_output_video(ctx->source, NULL); LOG_DEBUG("Video preview blanked"); } -static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_video_config *config) +static bool moq_source_init_decoder(struct moq_source *ctx, const struct moq_video_config *config) { // Map codec string to FFmpeg codec ID dynamically AVCodecID codec_id = codec_string_to_id(config->codec, config->codec_len); @@ -730,13 +730,13 @@ static bool hang_source_init_decoder(struct hang_source *ctx, const struct moq_v if (config->codec && copy_len > 0) { memcpy(codec_str, config->codec, copy_len); } - LOG_INFO("Decoder initialized: codec=%s, dimensions=%ux%u (may be refined on first frame)", + LOG_INFO("Decoder initialized: codec=%s, dimensions=%ux%u (may be refined on first frame)", codec_str, width, height); return true; } // NOTE: Caller must hold ctx->mutex when calling this function -static void hang_source_destroy_decoder_locked(struct hang_source *ctx) +static void moq_source_destroy_decoder_locked(struct moq_source *ctx) { if (ctx->sws_ctx) { sws_freeContext(ctx->sws_ctx); @@ -759,7 +759,7 @@ static void hang_source_destroy_decoder_locked(struct hang_source *ctx) ctx->current_pix_fmt = AV_PIX_FMT_NONE; } -static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) +static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id) { // Fast path: check atomic flag before taking lock if (ctx->shutting_down.load()) { @@ -796,9 +796,9 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Skip non-keyframes until we get the first one if (!ctx->got_keyframe && !frame_data.keyframe) { ctx->frames_waiting_for_keyframe++; - if (ctx->frames_waiting_for_keyframe == 1 || + if (ctx->frames_waiting_for_keyframe == 1 || (ctx->frames_waiting_for_keyframe % 30) == 0) { - LOG_INFO("Waiting for keyframe... (skipped %u frames so far)", + LOG_INFO("Waiting for keyframe... (skipped %u frames so far)", ctx->frames_waiting_for_keyframe); } pthread_mutex_unlock(&ctx->mutex); @@ -809,7 +809,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) // Mark that we've received a keyframe from the stream if (frame_data.keyframe) { if (!ctx->got_keyframe) { - LOG_INFO("Got keyframe after waiting for %u frames, payload_size=%zu", + LOG_INFO("Got keyframe after waiting for %u frames, payload_size=%zu", ctx->frames_waiting_for_keyframe, frame_data.payload_size); // Flush decoder to ensure clean state when starting from keyframe avcodec_flush_buffers(ctx->codec_ctx); @@ -841,10 +841,10 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) ctx->consecutive_decode_errors++; char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); - + // If too many consecutive errors, flush decoder and wait for next keyframe if (ctx->consecutive_decode_errors >= 5) { - LOG_WARNING("Too many send errors (%u), flushing decoder and waiting for keyframe", + LOG_WARNING("Too many send errors (%u), flushing decoder and waiting for keyframe", ctx->consecutive_decode_errors); avcodec_flush_buffers(ctx->codec_ctx); ctx->got_keyframe = false; @@ -872,10 +872,10 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) ctx->consecutive_decode_errors++; char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); - + // If too many consecutive errors, flush decoder and wait for next keyframe if (ctx->consecutive_decode_errors >= 5) { - LOG_WARNING("Too many decode errors (%u), flushing decoder and waiting for keyframe", + LOG_WARNING("Too many decode errors (%u), flushing decoder and waiting for keyframe", ctx->consecutive_decode_errors); avcodec_flush_buffers(ctx->codec_ctx); ctx->got_keyframe = false; @@ -890,7 +890,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) moq_consume_frame_close(frame_id); return; } - + // Successfully decoded a frame - reset error counter ctx->consecutive_decode_errors = 0; @@ -907,7 +907,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) } if (pix_fmt_changed) { LOG_INFO("Decoded frame pixel format changed: %d -> %d (%s)", - ctx->current_pix_fmt, decoded_pix_fmt, + ctx->current_pix_fmt, decoded_pix_fmt, av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); } @@ -943,7 +943,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) SWS_BILINEAR, NULL, NULL, NULL ); if (!new_sws_ctx) { - LOG_ERROR("Failed to create scaling context for %dx%d pix_fmt=%d (%s)", + LOG_ERROR("Failed to create scaling context for %dx%d pix_fmt=%d (%s)", frame->width, frame->height, decoded_pix_fmt, av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); av_frame_free(&frame); @@ -979,7 +979,7 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) ctx->frame.linesize[0] = frame->width * 4; ctx->frame.data[0] = new_frame_buffer; - LOG_INFO("Scaler initialized for %dx%d pix_fmt=%s", + LOG_INFO("Scaler initialized for %dx%d pix_fmt=%s", frame->width, frame->height, av_get_pix_fmt_name(decoded_pix_fmt) ? av_get_pix_fmt_name(decoded_pix_fmt) : "unknown"); } @@ -1001,20 +1001,20 @@ static void hang_source_decode_frame(struct hang_source *ctx, int32_t frame_id) } // Registration function -void register_hang_source() +void register_moq_source() { struct obs_source_info info = {}; - info.id = "hang_source"; + info.id = "moq_source"; info.type = OBS_SOURCE_TYPE_INPUT; info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_DO_NOT_DUPLICATE; info.get_name = [](void *) -> const char * { - return "Hang Source (MoQ)"; + return "Moq Source (MoQ)"; }; - info.create = hang_source_create; - info.destroy = hang_source_destroy; - info.update = hang_source_update; - info.get_defaults = hang_source_get_defaults; - info.get_properties = hang_source_properties; + info.create = moq_source_create; + info.destroy = moq_source_destroy; + info.update = moq_source_update; + info.get_defaults = moq_source_get_defaults; + info.get_properties = moq_source_properties; obs_register_source(&info); } diff --git a/src/moq-source.h b/src/moq-source.h new file mode 100644 index 0000000..5396bb6 --- /dev/null +++ b/src/moq-source.h @@ -0,0 +1,3 @@ +#pragma once + +void register_moq_source(); diff --git a/src/obs-moq.cpp b/src/obs-moq.cpp index f4ed809..c877e40 100644 --- a/src/obs-moq.cpp +++ b/src/obs-moq.cpp @@ -20,7 +20,7 @@ with this program. If not, see #include "moq-output.h" #include "moq-service.h" -#include "hang-source.h" +#include "moq-source.h" extern "C" { #include "moq.h" @@ -41,7 +41,7 @@ bool obs_module_load(void) register_moq_output(); register_moq_service(); - register_hang_source(); + register_moq_source(); return true; } From f4f128a57a1d7179fa5fd4a691bf09188c3968c9 Mon Sep 17 00:00:00 2001 From: Dave Gullo Date: Wed, 24 Dec 2025 04:38:58 -0600 Subject: [PATCH 12/12] removed redundant check, match all "avc" and "h264" to AV_CODEC_ID_H264 --- src/moq-source.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/moq-source.cpp b/src/moq-source.cpp index 0fd3f3a..c073ae9 100644 --- a/src/moq-source.cpp +++ b/src/moq-source.cpp @@ -26,7 +26,6 @@ static AVCodecID codec_string_to_id(const char *codec, size_t len) // H.264/AVC if ((len >= 4 && strncasecmp(codec, "h264", 4) == 0) || - (len >= 4 && strncasecmp(codec, "avc1", 4) == 0) || (len >= 3 && strncasecmp(codec, "avc", 3) == 0)) { return AV_CODEC_ID_H264; }