From 2c43f356d12e66f5635ff2f317a6e3e87f57aca4 Mon Sep 17 00:00:00 2001 From: itten Date: Fri, 3 Jul 2026 14:01:13 +0300 Subject: [PATCH 01/12] it's alive --- CMakeLists.txt | 21 ++++ nodes/ndiin/main.cpp | 3 + nodes/videoin/CMakeLists.txt | 8 ++ nodes/videoin/main.cpp | 206 +++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 nodes/videoin/CMakeLists.txt create mode 100644 nodes/videoin/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 61d8ed0..5a30194 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,18 +73,39 @@ FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz) FetchContent_MakeAvailable(json) +# ── FFmpeg ────────────────────────────────────────────────────────────────── +find_package(PkgConfig REQUIRED) + +# Check for FFmpeg components +pkg_check_modules(FFMPEG REQUIRED + libavformat + libavcodec + libswscale + libavutil +) + +# Create an interface library for FFmpeg +add_library(ffmpeg INTERFACE) +target_include_directories(ffmpeg INTERFACE ${FFMPEG_INCLUDE_DIRS}) +target_link_libraries(ffmpeg INTERFACE ${FFMPEG_LIBRARIES}) + # ── Shared utilities (Signal.hpp, NodeBase.hpp, FlowDef.hpp, V210.hpp) ─────── add_library(dmf-shared INTERFACE) target_include_directories(dmf-shared INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/shared) target_link_libraries(dmf-shared INTERFACE mxl nlohmann_json::nlohmann_json) +# ── Basic nodes ────────────────────────────────────────────────────────────── add_subdirectory(nodes/testpattern) add_subdirectory(nodes/fakesink) +# ── NDI nodes ──────────────────────────────────────────────────────────────── set(NDI_SDK_DIR "" CACHE PATH "Path to NDI SDK root") if(NDI_SDK_DIR) add_subdirectory(nodes/ndiout) add_subdirectory(nodes/ndiin) endif() +add_subdirectory(nodes/videoin) + +# ── Core server ────────────────────────────────────────────────────────────── add_subdirectory(studio-manager) diff --git a/nodes/ndiin/main.cpp b/nodes/ndiin/main.cpp index fe08d65..07abb34 100644 --- a/nodes/ndiin/main.cpp +++ b/nodes/ndiin/main.cpp @@ -145,6 +145,9 @@ class NDIInNode : public dmf::NodeBase { } grain.validSlices = grain.totalSlices; mxlFlowWriterCommitGrain(video_writer, &grain); + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; } else { log("video OpenGrain failed (%s) at index=%llu", dmf::mxl_status_str(st), video_index); } diff --git a/nodes/videoin/CMakeLists.txt b/nodes/videoin/CMakeLists.txt new file mode 100644 index 0000000..52fadb5 --- /dev/null +++ b/nodes/videoin/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(dmf-node-videoin main.cpp) +target_compile_features(dmf-node-videoin PRIVATE cxx_std_20) +target_link_libraries(dmf-node-videoin + PRIVATE + dmf-shared + ffmpeg +) +install(TARGETS dmf-node-videoin RUNTIME DESTINATION bin) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp new file mode 100644 index 0000000..e57fe87 --- /dev/null +++ b/nodes/videoin/main.cpp @@ -0,0 +1,206 @@ +extern "C" { + #include + #include + #include + #include + #include + #include +} + +#include +#include +#include +#include +#include +#include "V210.hpp" +#include "FlowDef.hpp" +#include "NodeBase.hpp" + +int main(int argc, char* argv[]) { + // Open video file + AVFormatContext* formatContext = nullptr; + const char* filename = (argc > 1) ? argv[1] : "/home/itten/test-vid/0.ts"; + + if (avformat_open_input(&formatContext, filename, nullptr, nullptr) != 0) { + std::cerr << "Could not open file: " << filename << std::endl; + return 1; + } + + // Find stream info + if (avformat_find_stream_info(formatContext, nullptr) < 0) { + std::cerr << "Could not find stream info" << std::endl; + avformat_close_input(&formatContext); + return 1; + } + + // Find video stream + int videoStreamIndex = -1; + for (unsigned int i = 0; i < formatContext->nb_streams; i++) { + if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + videoStreamIndex = i; + break; + } + } + + if (videoStreamIndex == -1) { + std::cerr << "No video stream found" << std::endl; + avformat_close_input(&formatContext); + return 1; + } + + // Get codec parameters + AVCodecParameters* codecParams = formatContext->streams[videoStreamIndex]->codecpar; + const AVCodec* codec = avcodec_find_decoder(codecParams->codec_id); + + if (!codec) { + std::cerr << "Unsupported codec" << std::endl; + avformat_close_input(&formatContext); + return 1; + } + + // Open codec + AVCodecContext* codecContext = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codecContext, codecParams); + + if (avcodec_open2(codecContext, codec, nullptr) < 0) { + std::cerr << "Could not open codec" << std::endl; + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + return 1; + } + + std::cout << "Video info:" << std::endl; + std::cout << " Width: " << codecContext->width << std::endl; + std::cout << " Height: " << codecContext->height << std::endl; + std::cout << " Pixel format: " << av_get_pix_fmt_name(codecContext->pix_fmt) << std::endl; + std::cout << " Frame rate: " << codecContext->framerate.num << "/" << codecContext->framerate.den << std::endl; + + // Create Sws context to convert to UYVY422 + struct SwsContext *sws_ctx = sws_getContext( + codecContext->width, codecContext->height, codecContext->pix_fmt, + codecContext->width, codecContext->height, AV_PIX_FMT_UYVY422, + NULL, NULL, NULL, NULL + ); + + if (!sws_ctx) { + std::cerr << "Failed to create SwsContext" << std::endl; + } + + // Allocate UYVY buffer + int uyvy_buf_size = av_image_get_buffer_size( + AV_PIX_FMT_UYVY422, + codecContext->width, + codecContext->height, + 32 + ); + if (uyvy_buf_size < 0) { + std::cerr << "Failed to calculate UYVY buffer size" << std::endl; + } + + uint8_t* uyvy_buffer = (uint8_t*)av_malloc(uyvy_buf_size); + if (!uyvy_buffer) { + std::cerr << "Failed to allocate UYVY buffer" << std::endl; + } + + int uyvy_line_size = av_image_get_linesize(AV_PIX_FMT_UYVY422, codecContext->width, 0); + if (uyvy_line_size < 0) { + std::cerr << "Failed to get UYVY line size" << std::endl; + } + + uint8_t* uyvy_data[4] = {uyvy_buffer, nullptr, nullptr, nullptr}; + int uyvy_line_sizes[4] = {uyvy_line_size, 0, 0, 0}; + + // Allocate v210 buffer + int blocks_per_row = (codecContext->width + 5) / 6; + int v210_bytes_per_row = blocks_per_row * 16; + int v210_stride = ((v210_bytes_per_row + 63) / 64) * 64; + int v210_buffer_size = v210_stride * codecContext->height; + + uint8_t* v210_buffer = (uint8_t*)av_malloc(v210_buffer_size); + if (!v210_buffer) { + std::cerr << "Failed to allocate V210 buffer" << std::endl; + } + + // MXL prep + int fps_num = 25, fps_den = 1; + mxlInstance mxl_instance = mxlCreateInstance("/tmp/videotest-domain", nullptr); + std::string video_flow_def = ""; + mxlFlowWriter video_writer = nullptr; + mxlFlowConfigInfo video_config = {}; + std::string flow_uuid = "5fbec3b1-1b0f-417d-9059-8b94a47197ed"; + video_flow_def = dmf::make_video_flow_def( + flow_uuid, + "libav video flow", + codecContext->width, + codecContext->height, + fps_num, + fps_den + ); + bool is_flow_created = false; + mxlStatus vst = + mxlCreateFlowWriter(mxl_instance, video_flow_def.c_str(), "", &video_writer, &video_config, &is_flow_created); + if (vst != MXL_STATUS_OK) { + std::cerr << "MXL flow writer is not created. Reason: " << dmf::mxl_status_str(vst) << std::endl; + } + + mxlRational video_rate = {fps_num, fps_den}; + uint64_t video_index = mxlGetCurrentIndex(&video_rate); + + // Allocate packets and frames + AVPacket* packet = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + + // Read and decode frames (example - just count them) + int frameCount = 0; + while (av_read_frame(formatContext, packet) >= 0) { + if (packet->stream_index == videoStreamIndex) { + if (avcodec_send_packet(codecContext, packet) == 0) { + while (avcodec_receive_frame(codecContext, frame) == 0) { + frameCount++; + // mxl part + mxlGrainInfo grain{}; + uint8_t* buf = nullptr; + vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); + if (vst == MXL_STATUS_OK) { + // source pix_fmt -> UYVY422 pix_fmt + sws_scale(sws_ctx, frame->data, frame->linesize, 0, codecContext->height, uyvy_data, uyvy_line_sizes); + // UYVY -> V210 + dmf::v210::UYVYtoV210( + uyvy_buffer, + buf, + codecContext->width, + codecContext->height, + uyvy_line_size, + v210_stride + ); + grain.flags = 0; + grain.validSlices = grain.totalSlices; + mxlFlowWriterCommitGrain(video_writer, &grain); + } + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; + av_frame_unref(frame); + } + } + } + av_packet_unref(packet); + } + + std::cout << "Total frames: " << frameCount << std::endl; + + // Cleanup MXL + mxlReleaseFlowWriter(mxl_instance, video_writer); + + // Cleanup context + sws_freeContext(sws_ctx); + + // Cleanup common + av_frame_free(&frame); + av_packet_free(&packet); + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + + + return 0; +} \ No newline at end of file -- 2.52.0 From 32a8fa9837d46a944350e2f83705cee9ea542d7e Mon Sep 17 00:00:00 2001 From: itten Date: Fri, 3 Jul 2026 18:48:11 +0300 Subject: [PATCH 02/12] refactored videoin to NodeBase --- nodes/videoin/main.cpp | 241 ++++++++++------------------------------- shared/V210.hpp | 35 ++++++ shared/VideoReader.hpp | 223 ++++++++++++++++++++++++++++++++++++++ video-ndi.json | 13 +++ 4 files changed, 330 insertions(+), 182 deletions(-) create mode 100644 shared/VideoReader.hpp create mode 100644 video-ndi.json diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index e57fe87..733f6a9 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -7,200 +7,77 @@ extern "C" { #include } -#include #include -#include #include #include #include "V210.hpp" #include "FlowDef.hpp" #include "NodeBase.hpp" +#include "VideoReader.hpp" -int main(int argc, char* argv[]) { - // Open video file - AVFormatContext* formatContext = nullptr; - const char* filename = (argc > 1) ? argv[1] : "/home/itten/test-vid/0.ts"; - - if (avformat_open_input(&formatContext, filename, nullptr, nullptr) != 0) { - std::cerr << "Could not open file: " << filename << std::endl; - return 1; - } - - // Find stream info - if (avformat_find_stream_info(formatContext, nullptr) < 0) { - std::cerr << "Could not find stream info" << std::endl; - avformat_close_input(&formatContext); - return 1; - } - - // Find video stream - int videoStreamIndex = -1; - for (unsigned int i = 0; i < formatContext->nb_streams; i++) { - if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - videoStreamIndex = i; - break; +#include + + +class VideoInNode : public dmf::NodeBase { + void run() override { + std::string filename = "/home/itten/test-vid/0.ts"; + log("VideoIn Node started with file: %s", filename.c_str()); + dmf::VideoReader video_reader(filename); + + const auto video_flow_info = config().at("video_flow_id"); + const auto video_flow_id = video_flow_info.at("id").get(); + const int width = video_flow_info.value("width", video_reader.source_info.width); + const int height = video_flow_info.value("height", video_reader.source_info.height); + const int fps_num = video_flow_info.value("fps_num", video_reader.source_info.fps_num); + const int fps_den = video_flow_info.value("fps_den", video_reader.source_info.fps_den); + + mxlFlowWriter video_writer = nullptr; + mxlFlowConfigInfo video_config = {}; + std::string video_flow_def = dmf::make_video_flow_def( + video_flow_id, + node_id(), + width, + height, + fps_num, + fps_den + ); + bool created = false; + mxlStatus vst = mxlCreateFlowWriter( + instance(), + video_flow_def.c_str(), + "", + &video_writer, + &video_config, + &created + ); + if (vst != MXL_STATUS_OK) { + log("MXL flow writer is not created. Reason: %s", dmf::mxl_status_str(vst)); } - } - - if (videoStreamIndex == -1) { - std::cerr << "No video stream found" << std::endl; - avformat_close_input(&formatContext); - return 1; - } - - // Get codec parameters - AVCodecParameters* codecParams = formatContext->streams[videoStreamIndex]->codecpar; - const AVCodec* codec = avcodec_find_decoder(codecParams->codec_id); - - if (!codec) { - std::cerr << "Unsupported codec" << std::endl; - avformat_close_input(&formatContext); - return 1; - } - - // Open codec - AVCodecContext* codecContext = avcodec_alloc_context3(codec); - avcodec_parameters_to_context(codecContext, codecParams); - - if (avcodec_open2(codecContext, codec, nullptr) < 0) { - std::cerr << "Could not open codec" << std::endl; - avcodec_free_context(&codecContext); - avformat_close_input(&formatContext); - return 1; - } - - std::cout << "Video info:" << std::endl; - std::cout << " Width: " << codecContext->width << std::endl; - std::cout << " Height: " << codecContext->height << std::endl; - std::cout << " Pixel format: " << av_get_pix_fmt_name(codecContext->pix_fmt) << std::endl; - std::cout << " Frame rate: " << codecContext->framerate.num << "/" << codecContext->framerate.den << std::endl; - // Create Sws context to convert to UYVY422 - struct SwsContext *sws_ctx = sws_getContext( - codecContext->width, codecContext->height, codecContext->pix_fmt, - codecContext->width, codecContext->height, AV_PIX_FMT_UYVY422, - NULL, NULL, NULL, NULL - ); + mxlRational video_rate = {fps_num, fps_den}; + uint64_t video_index = mxlGetCurrentIndex(&video_rate); - if (!sws_ctx) { - std::cerr << "Failed to create SwsContext" << std::endl; - } - - // Allocate UYVY buffer - int uyvy_buf_size = av_image_get_buffer_size( - AV_PIX_FMT_UYVY422, - codecContext->width, - codecContext->height, - 32 - ); - if (uyvy_buf_size < 0) { - std::cerr << "Failed to calculate UYVY buffer size" << std::endl; - } - - uint8_t* uyvy_buffer = (uint8_t*)av_malloc(uyvy_buf_size); - if (!uyvy_buffer) { - std::cerr << "Failed to allocate UYVY buffer" << std::endl; - } - - int uyvy_line_size = av_image_get_linesize(AV_PIX_FMT_UYVY422, codecContext->width, 0); - if (uyvy_line_size < 0) { - std::cerr << "Failed to get UYVY line size" << std::endl; - } - - uint8_t* uyvy_data[4] = {uyvy_buffer, nullptr, nullptr, nullptr}; - int uyvy_line_sizes[4] = {uyvy_line_size, 0, 0, 0}; - - // Allocate v210 buffer - int blocks_per_row = (codecContext->width + 5) / 6; - int v210_bytes_per_row = blocks_per_row * 16; - int v210_stride = ((v210_bytes_per_row + 63) / 64) * 64; - int v210_buffer_size = v210_stride * codecContext->height; - - uint8_t* v210_buffer = (uint8_t*)av_malloc(v210_buffer_size); - if (!v210_buffer) { - std::cerr << "Failed to allocate V210 buffer" << std::endl; - } - - // MXL prep - int fps_num = 25, fps_den = 1; - mxlInstance mxl_instance = mxlCreateInstance("/tmp/videotest-domain", nullptr); - std::string video_flow_def = ""; - mxlFlowWriter video_writer = nullptr; - mxlFlowConfigInfo video_config = {}; - std::string flow_uuid = "5fbec3b1-1b0f-417d-9059-8b94a47197ed"; - video_flow_def = dmf::make_video_flow_def( - flow_uuid, - "libav video flow", - codecContext->width, - codecContext->height, - fps_num, - fps_den - ); - bool is_flow_created = false; - mxlStatus vst = - mxlCreateFlowWriter(mxl_instance, video_flow_def.c_str(), "", &video_writer, &video_config, &is_flow_created); - if (vst != MXL_STATUS_OK) { - std::cerr << "MXL flow writer is not created. Reason: " << dmf::mxl_status_str(vst) << std::endl; - } - - mxlRational video_rate = {fps_num, fps_den}; - uint64_t video_index = mxlGetCurrentIndex(&video_rate); - - // Allocate packets and frames - AVPacket* packet = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - - // Read and decode frames (example - just count them) - int frameCount = 0; - while (av_read_frame(formatContext, packet) >= 0) { - if (packet->stream_index == videoStreamIndex) { - if (avcodec_send_packet(codecContext, packet) == 0) { - while (avcodec_receive_frame(codecContext, frame) == 0) { - frameCount++; - // mxl part - mxlGrainInfo grain{}; - uint8_t* buf = nullptr; - vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); - if (vst == MXL_STATUS_OK) { - // source pix_fmt -> UYVY422 pix_fmt - sws_scale(sws_ctx, frame->data, frame->linesize, 0, codecContext->height, uyvy_data, uyvy_line_sizes); - // UYVY -> V210 - dmf::v210::UYVYtoV210( - uyvy_buffer, - buf, - codecContext->width, - codecContext->height, - uyvy_line_size, - v210_stride - ); - grain.flags = 0; - grain.validSlices = grain.totalSlices; - mxlFlowWriterCommitGrain(video_writer, &grain); - } - const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); - if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); - video_index++; - av_frame_unref(frame); - } + while (dmf::g_running.load(std::memory_order_relaxed)) { + uint8_t* buf = nullptr; + mxlGrainInfo grain{}; + vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); + if (vst == MXL_STATUS_OK) { + if (!video_reader.get_next_frame(buf, nullptr)) continue; + grain.flags = 0; + grain.validSlices = grain.totalSlices; + mxlFlowWriterCommitGrain(video_writer, &grain); } + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; } - av_packet_unref(packet); + + mxlReleaseFlowWriter(instance(), video_writer); } - - std::cout << "Total frames: " << frameCount << std::endl; - - // Cleanup MXL - mxlReleaseFlowWriter(mxl_instance, video_writer); +}; - // Cleanup context - sws_freeContext(sws_ctx); - // Cleanup common - av_frame_free(&frame); - av_packet_free(&packet); - avcodec_free_context(&codecContext); - avformat_close_input(&formatContext); - - - return 0; -} \ No newline at end of file +int main() { + VideoInNode node; + return node.execute(); +} diff --git a/shared/V210.hpp b/shared/V210.hpp index 4dc48c5..461a105 100644 --- a/shared/V210.hpp +++ b/shared/V210.hpp @@ -170,4 +170,39 @@ inline void UYVYtoV210(uint8_t* src_buf, uint8_t* dst_buf, int width, int height } } +inline void YUV422P10toV210(const uint16_t* y, const uint16_t* u, const uint16_t* v, + uint8_t* dst, int width, int height, + int y_stride, int u_stride, int v_stride, // bytes between rows + uint32_t dst_stride) + { + for (int row = 0; row < height; row++) { + const uint16_t* y_row = reinterpret_cast( + reinterpret_cast(y) + row * y_stride + ); + const uint16_t* u_row = reinterpret_cast( + reinterpret_cast(u) + row * u_stride + ); + const uint16_t* v_row = reinterpret_cast( + reinterpret_cast(v) + row * v_stride + ); + + uint8_t* dst_row = dst + static_cast(row) * dst_stride; + const int blocks = width / 6; + + for (int b = 0; b < blocks; b++) { + int x = b * 6; + const uint16_t cb0 = u_row[x/2], cb1 = u_row[x/2+1], cb2 = u_row[x/2+2]; + const uint16_t cr0 = v_row[x/2], cr1 = v_row[x/2+1], cr2 = v_row[x/2+2]; + const uint16_t y0 = y_row[x], y1 = y_row[x+1], y2 = y_row[x+2]; + const uint16_t y3 = y_row[x+3], y4 = y_row[x+4], y5 = y_row[x+5]; + + auto* w = reinterpret_cast(dst_row + b * 16); + w[0] = (cb0 & 0x3FFu) | ((y0 & 0x3FFu) << 10) | ((cr0 & 0x3FFu) << 20); + w[1] = (y1 & 0x3FFu) | ((cb1 & 0x3FFu) << 10) | ((y2 & 0x3FFu) << 20); + w[2] = (cr1 & 0x3FFu) | ((y3 & 0x3FFu) << 10) | ((cb2 & 0x3FFu) << 20); + w[3] = (y4 & 0x3FFu) | ((cr2 & 0x3FFu) << 10) | ((y5 & 0x3FFu) << 20); + } + } + } + } // namespace dmf::v210 diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp new file mode 100644 index 0000000..c6338a1 --- /dev/null +++ b/shared/VideoReader.hpp @@ -0,0 +1,223 @@ +#pragma once + +extern "C" { + #include + #include + #include + #include + #include + #include +} + +#include +#include +#include "Signal.hpp" +#include "V210.hpp" + +namespace dmf { +class VideoReader { + public: + struct SourceInfo { + int width = 0; + int height = 0; + int fps_num = 0; + int fps_den = 0; + int stride = 0; + AVPixelFormat pix_fmt{}; + }; + + struct AudioInfo { + int sample_rate = 0; + int channels = 0; + int samples = 0; + int channel_stride = 0; // floats between channel planes + }; + + struct SourceInfo source_info{}; + bool has_audio = false; + bool have_video = false; + + VideoReader(std::string filename) { + if (!open_file(filename)) { + return; + } + get_source_info(); + allocate_conversion_buffers(); + } + + ~VideoReader() { + avcodec_free_context(&codec_context); + avformat_close_input(&format_context); + sws_freeContext(sws_ctx); + av_frame_unref(frame); + av_packet_unref(packet); + } + + bool get_next_frame(uint8_t* video_buf, uint8_t* audiobuf) { + while (dmf::g_running.load(std::memory_order_relaxed)) { + // Try to get a buffered frame from previous packet first + if (avcodec_receive_frame(codec_context, frame) == 0) { + sws_scale( + sws_ctx, + frame->data, + frame->linesize, + 0, + source_info.height, + p10_data, + p10_linesizes + ); + dmf::v210::YUV422P10toV210( + reinterpret_cast(p10_data[0]), + reinterpret_cast(p10_data[1]), + reinterpret_cast(p10_data[2]), + video_buf, + source_info.width, + source_info.height, + p10_linesizes[0], + p10_linesizes[1], + p10_linesizes[2], + v210_stride + ); + av_frame_unref(frame); + return true; + } + // No buffered frame — read next packet + av_packet_unref(packet); + if (av_read_frame(format_context, packet) < 0) { + // EOF — seek back to start and keep going + avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(codec_context); + av_packet_unref(packet); + continue; + } + if (packet->stream_index != video_stream_index) continue; + avcodec_send_packet(codec_context, packet); + } + } + + private: + AVFormatContext* format_context = nullptr; + AVCodecContext* codec_context = nullptr; + AVPacket* packet = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + int video_stream_index = -1; + int audio_stream_index = -1; + + // conversion data + struct SwsContext *sws_ctx{}; + uint8_t* p10_buffer = nullptr; + int p10_linesizes[4] = {0, 0, 0, 0}; + uint8_t* p10_data[4] = {nullptr, nullptr, nullptr, nullptr}; + uint8_t* v210_buffer = nullptr; + int v210_stride = 0; + + bool open_file(std::string filename) { + if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) { + throw std::runtime_error("Could not open file: " + filename); + return false; + } + + // Find stream info + if (avformat_find_stream_info(format_context, nullptr) < 0) { + avformat_close_input(&format_context); + throw std::runtime_error("Could not find stream info"); + return false; + } + + // Find streams + for (unsigned int i = 0; i < format_context->nb_streams; i++) { + AVMediaType data_type = format_context->streams[i]->codecpar->codec_type; + if (data_type == AVMEDIA_TYPE_VIDEO) { + video_stream_index = i; + have_video = true; + } else if (data_type == AVMEDIA_TYPE_AUDIO && audio_stream_index != -1) { + // TODO: show list of available audio tracks and allow user to pick + // or handle multiple audio streams + audio_stream_index = i; + has_audio = true; + } + } + + if (video_stream_index == -1 && audio_stream_index == -1) { + avformat_close_input(&format_context); + throw std::runtime_error("No audio/video stream found"); + return false; + } + + return true; + } + + void get_source_info() { + // Get codec parameters + AVCodecParameters* codec_params = format_context->streams[video_stream_index]->codecpar; + const AVCodec* codec = avcodec_find_decoder(codec_params->codec_id); + + if (!codec) { + avformat_close_input(&format_context); + throw std::runtime_error("Unsupported codec"); + } + + // Open codec + codec_context = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codec_context, codec_params); + + if (avcodec_open2(codec_context, codec, nullptr) < 0) { + avcodec_free_context(&codec_context); + avformat_close_input(&format_context); + throw std::runtime_error("Could not open codec"); + } + + AVRational fps = codec_context->framerate; + if (fps.num == 0 || fps.den == 0) { + fps = format_context->streams[video_stream_index]->avg_frame_rate; + } + + source_info.width = codec_context->width; + source_info.height = codec_context->height; + source_info.fps_num = fps.num; + source_info.fps_den = fps.den; + source_info.pix_fmt = codec_context->pix_fmt; + } + + void allocate_conversion_buffers() + { + // Create Sws context to convert to planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), LE + sws_ctx = sws_getContext( + source_info.width, source_info.height, source_info.pix_fmt, + source_info.width, source_info.height, AV_PIX_FMT_YUV422P10LE, + 0, NULL, NULL, NULL + ); + + if (!sws_ctx) { + throw std::runtime_error("Failed to create SwsContext"); + return; + } + + // Allocate P10 image + int p10_buf_size = av_image_get_buffer_size( + AV_PIX_FMT_YUV422P10LE, + source_info.width, source_info.height, + 64 + ); + p10_buffer = (uint8_t*)av_malloc(p10_buf_size); + av_image_alloc( + p10_data, p10_linesizes, + source_info.width, source_info.height, + AV_PIX_FMT_YUV422P10LE, 64 + ); + + // Allocate v210 buffer + int blocks_per_row = (codec_context->width + 5) / 6; + int v210_bytes_per_row = blocks_per_row * 16; + v210_stride = ((v210_bytes_per_row + 63) / 64) * 64; + int v210_buffer_size = v210_stride * source_info.height; + + v210_buffer = (uint8_t*)av_malloc(v210_buffer_size); + if (!v210_buffer) { + throw std::runtime_error("Failed to allocate V210 buffer"); + return; + } + } + +}; +} \ No newline at end of file diff --git a/video-ndi.json b/video-ndi.json new file mode 100644 index 0000000..fd395d9 --- /dev/null +++ b/video-ndi.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { "id": "videoin", "type": "videoin", "params": {} }, + { "id": "ndiout", "type": "ndiout", "params": {} } + ], + "edges": [ + { + "from": "videoin", "from_port": "video_flow_id", + "to": "ndiout", "to_port": "flow_id", + "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 25, "fps_den": 1 } + } + ] +} -- 2.52.0 From 0b5b113d4ea6980841d1dece0d5fd8d779cf40b4 Mon Sep 17 00:00:00 2001 From: itten Date: Fri, 3 Jul 2026 18:51:10 +0300 Subject: [PATCH 03/12] fixed ndiout flow_id -> video_flow_id --- nodes/ndiout/main.cpp | 4 ++-- video-ndi.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nodes/ndiout/main.cpp b/nodes/ndiout/main.cpp index 5699e55..acc3acf 100644 --- a/nodes/ndiout/main.cpp +++ b/nodes/ndiout/main.cpp @@ -34,7 +34,7 @@ struct NDIContext { class NDIOutNode : public dmf::NodeBase { void run() override { // --- video flow (optional) --- - bool has_video = config().contains("flow_id"); + bool has_video = config().contains("video_flow_id"); int width = 1920; int height = 1080; @@ -45,7 +45,7 @@ class NDIOutNode : public dmf::NodeBase { uint32_t video_stride = 0; if (has_video) { - const auto flow_info = config().at("flow_id"); + const auto flow_info = config().at("video_flow_id"); flow_id = flow_info.at("id").get(); width = flow_info.value("width", 1920); height = flow_info.value("height", 1080); diff --git a/video-ndi.json b/video-ndi.json index fd395d9..c908c44 100644 --- a/video-ndi.json +++ b/video-ndi.json @@ -6,7 +6,7 @@ "edges": [ { "from": "videoin", "from_port": "video_flow_id", - "to": "ndiout", "to_port": "flow_id", + "to": "ndiout", "to_port": "video_flow_id", "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 25, "fps_den": 1 } } ] -- 2.52.0 From bb7d1bb5a3ffcca0b7428c909ddbe4d828b3c2e8 Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Fri, 3 Jul 2026 19:11:18 +0300 Subject: [PATCH 04/12] =?UTF-8?q?fix:=20VideoReader=20bugs=20=E2=80=94=20s?= =?UTF-8?q?tride,=20memory,=20audio=20detection,=20UB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass MXL grain stride (from sliceSizes[0]) into get_next_frame so YUV422P10toV210 writes with the correct line width instead of a self-computed value that may not match the MXL buffer. Fix audio stream detection: condition was inverted (!= -1 → == -1), so the first audio stream was never picked up. Add return false at end of get_next_frame to fix UB when g_running goes false and the loop exits without returning. Replace av_frame_unref/av_packet_unref with av_frame_free/av_packet_free in destructor — unref only releases data, not the struct itself. Add av_freep(&p10_data[0]) in destructor to free av_image_alloc memory. Remove unused p10_buffer and v210_buffer allocations. Read filename from config("file") instead of hardcoded path. Add early return if mxlCreateFlowWriter fails. Co-Authored-By: Claude Sonnet 4.6 --- nodes/videoin/main.cpp | 14 +++++++------- shared/VideoReader.hpp | 34 ++++++++-------------------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index 733f6a9..a54f9b5 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -15,12 +15,10 @@ extern "C" { #include "NodeBase.hpp" #include "VideoReader.hpp" -#include - - class VideoInNode : public dmf::NodeBase { - void run() override { - std::string filename = "/home/itten/test-vid/0.ts"; + void run() override { + const std::string filename = config().value("file", std::string{}); + if (filename.empty()) { log("config missing 'file'"); return; } log("VideoIn Node started with file: %s", filename.c_str()); dmf::VideoReader video_reader(filename); @@ -51,8 +49,10 @@ class VideoInNode : public dmf::NodeBase { &created ); if (vst != MXL_STATUS_OK) { - log("MXL flow writer is not created. Reason: %s", dmf::mxl_status_str(vst)); + log("mxlCreateFlowWriter failed (%s)", dmf::mxl_status_str(vst)); + return; } + const uint32_t video_stride = video_config.discrete.sliceSizes[0]; mxlRational video_rate = {fps_num, fps_den}; uint64_t video_index = mxlGetCurrentIndex(&video_rate); @@ -62,7 +62,7 @@ class VideoInNode : public dmf::NodeBase { mxlGrainInfo grain{}; vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); if (vst == MXL_STATUS_OK) { - if (!video_reader.get_next_frame(buf, nullptr)) continue; + if (!video_reader.get_next_frame(buf, video_stride, nullptr)) continue; grain.flags = 0; grain.validSlices = grain.totalSlices; mxlFlowWriterCommitGrain(video_writer, &grain); diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index c6338a1..445b1b0 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -49,11 +49,12 @@ class VideoReader { avcodec_free_context(&codec_context); avformat_close_input(&format_context); sws_freeContext(sws_ctx); - av_frame_unref(frame); - av_packet_unref(packet); + av_freep(&p10_data[0]); + av_frame_free(&frame); + av_packet_free(&packet); } - bool get_next_frame(uint8_t* video_buf, uint8_t* audiobuf) { + bool get_next_frame(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audiobuf) { while (dmf::g_running.load(std::memory_order_relaxed)) { // Try to get a buffered frame from previous packet first if (avcodec_receive_frame(codec_context, frame) == 0) { @@ -76,7 +77,7 @@ class VideoReader { p10_linesizes[0], p10_linesizes[1], p10_linesizes[2], - v210_stride + mxl_stride ); av_frame_unref(frame); return true; @@ -93,6 +94,7 @@ class VideoReader { if (packet->stream_index != video_stream_index) continue; avcodec_send_packet(codec_context, packet); } + return false; } private: @@ -105,11 +107,8 @@ class VideoReader { // conversion data struct SwsContext *sws_ctx{}; - uint8_t* p10_buffer = nullptr; int p10_linesizes[4] = {0, 0, 0, 0}; uint8_t* p10_data[4] = {nullptr, nullptr, nullptr, nullptr}; - uint8_t* v210_buffer = nullptr; - int v210_stride = 0; bool open_file(std::string filename) { if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) { @@ -130,7 +129,7 @@ class VideoReader { if (data_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; have_video = true; - } else if (data_type == AVMEDIA_TYPE_AUDIO && audio_stream_index != -1) { + } else if (data_type == AVMEDIA_TYPE_AUDIO && audio_stream_index == -1) { // TODO: show list of available audio tracks and allow user to pick // or handle multiple audio streams audio_stream_index = i; @@ -193,30 +192,13 @@ class VideoReader { return; } - // Allocate P10 image - int p10_buf_size = av_image_get_buffer_size( - AV_PIX_FMT_YUV422P10LE, - source_info.width, source_info.height, - 64 - ); - p10_buffer = (uint8_t*)av_malloc(p10_buf_size); + // Allocate P10 image (freed in destructor via av_freep(&p10_data[0])) av_image_alloc( p10_data, p10_linesizes, source_info.width, source_info.height, AV_PIX_FMT_YUV422P10LE, 64 ); - // Allocate v210 buffer - int blocks_per_row = (codec_context->width + 5) / 6; - int v210_bytes_per_row = blocks_per_row * 16; - v210_stride = ((v210_bytes_per_row + 63) / 64) * 64; - int v210_buffer_size = v210_stride * source_info.height; - - v210_buffer = (uint8_t*)av_malloc(v210_buffer_size); - if (!v210_buffer) { - throw std::runtime_error("Failed to allocate V210 buffer"); - return; - } } }; -- 2.52.0 From 96475acf62f23d08480b16f560fe074cc65e0f40 Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Fri, 3 Jul 2026 19:13:19 +0300 Subject: [PATCH 05/12] refactor: VideoReader and videoin cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VideoReader: - Guard get_source_info/allocate_conversion_buffers behind have_video check — prevents crash on audio-only files - Remove unused audiobuf parameter from get_next_frame - Remove dead return statements after throw - Remove unused SourceInfo::stride field and AudioInfo struct - Pass const std::string& instead of by value in constructor/open_file - Remove redundant struct keyword on SourceInfo source_info{} - Fix video_stream_index never guarded against -1 in open_file - Check av_image_alloc and avcodec_parameters_to_context return values - Remove extra av_packet_unref after seek (was harmless but confusing) - Use SWS_BILINEAR for sws_getContext flags instead of 0 - Replace NULL with nullptr in sws_getContext - Remove unused #include videoin main.cpp: - Early return if have_video is false after open - Inline make_video_flow_def call (remove intermediate variable) - Add grain count log line matching other nodes - Change continue to break when get_next_frame returns false - Make video_rate const - Remove double blank line before main() - Remove trailing spaces Co-Authored-By: Claude Sonnet 4.6 --- nodes/videoin/main.cpp | 51 +++---- shared/VideoReader.hpp | 306 +++++++++++++++++++---------------------- 2 files changed, 161 insertions(+), 196 deletions(-) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index a54f9b5..d39893c 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -4,7 +4,6 @@ extern "C" { #include #include #include - #include } #include @@ -19,51 +18,45 @@ class VideoInNode : public dmf::NodeBase { void run() override { const std::string filename = config().value("file", std::string{}); if (filename.empty()) { log("config missing 'file'"); return; } - log("VideoIn Node started with file: %s", filename.c_str()); + log("file: %s", filename.c_str()); + dmf::VideoReader video_reader(filename); + if (!video_reader.have_video) { log("no video stream found"); return; } const auto video_flow_info = config().at("video_flow_id"); const auto video_flow_id = video_flow_info.at("id").get(); - const int width = video_flow_info.value("width", video_reader.source_info.width); - const int height = video_flow_info.value("height", video_reader.source_info.height); - const int fps_num = video_flow_info.value("fps_num", video_reader.source_info.fps_num); - const int fps_den = video_flow_info.value("fps_den", video_reader.source_info.fps_den); + const int width = video_flow_info.value("width", video_reader.source_info.width); + const int height = video_flow_info.value("height", video_reader.source_info.height); + const int fps_num = video_flow_info.value("fps_num", video_reader.source_info.fps_num); + const int fps_den = video_flow_info.value("fps_den", video_reader.source_info.fps_den); - mxlFlowWriter video_writer = nullptr; - mxlFlowConfigInfo video_config = {}; - std::string video_flow_def = dmf::make_video_flow_def( - video_flow_id, - node_id(), - width, - height, - fps_num, - fps_den - ); + log("video flow=%s %dx%d @ %d/%d fps", video_flow_id.c_str(), width, height, fps_num, fps_den); + + mxlFlowWriter video_writer = nullptr; + mxlFlowConfigInfo video_cfg = {}; bool created = false; mxlStatus vst = mxlCreateFlowWriter( instance(), - video_flow_def.c_str(), - "", - &video_writer, - &video_config, - &created - ); + dmf::make_video_flow_def(video_flow_id, node_id(), width, height, fps_num, fps_den).c_str(), + "", &video_writer, &video_cfg, &created); if (vst != MXL_STATUS_OK) { log("mxlCreateFlowWriter failed (%s)", dmf::mxl_status_str(vst)); return; } - const uint32_t video_stride = video_config.discrete.sliceSizes[0]; + const uint32_t video_stride = video_cfg.discrete.sliceSizes[0]; + log("video stride=%u B/line grain=%u B ring=%u grains", + video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); - mxlRational video_rate = {fps_num, fps_den}; + const mxlRational video_rate = {fps_num, fps_den}; uint64_t video_index = mxlGetCurrentIndex(&video_rate); while (dmf::g_running.load(std::memory_order_relaxed)) { - uint8_t* buf = nullptr; + uint8_t* buf = nullptr; mxlGrainInfo grain{}; vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); if (vst == MXL_STATUS_OK) { - if (!video_reader.get_next_frame(buf, video_stride, nullptr)) continue; - grain.flags = 0; + if (!video_reader.get_next_frame(buf, video_stride)) break; + grain.flags = 0; grain.validSlices = grain.totalSlices; mxlFlowWriterCommitGrain(video_writer, &grain); } @@ -72,12 +65,12 @@ class VideoInNode : public dmf::NodeBase { video_index++; } + log("stopped at video_index=%llu", video_index); mxlReleaseFlowWriter(instance(), video_writer); } }; - -int main() { +int main() { VideoInNode node; return node.execute(); } diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index 445b1b0..65f7925 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -4,9 +4,8 @@ extern "C" { #include #include #include - #include #include - #include + #include } #include @@ -15,191 +14,164 @@ extern "C" { #include "V210.hpp" namespace dmf { + class VideoReader { - public: - struct SourceInfo { - int width = 0; - int height = 0; - int fps_num = 0; - int fps_den = 0; - int stride = 0; - AVPixelFormat pix_fmt{}; - }; +public: + struct SourceInfo { + int width = 0; + int height = 0; + int fps_num = 0; + int fps_den = 0; + AVPixelFormat pix_fmt{}; + }; - struct AudioInfo { - int sample_rate = 0; - int channels = 0; - int samples = 0; - int channel_stride = 0; // floats between channel planes - }; + SourceInfo source_info{}; + bool has_audio = false; + bool have_video = false; - struct SourceInfo source_info{}; - bool has_audio = false; - bool have_video = false; - - VideoReader(std::string filename) { - if (!open_file(filename)) { - return; - } + explicit VideoReader(const std::string& filename) { + if (!open_file(filename)) + return; + if (have_video) { get_source_info(); allocate_conversion_buffers(); } + } - ~VideoReader() { - avcodec_free_context(&codec_context); + ~VideoReader() { + avcodec_free_context(&codec_context); + avformat_close_input(&format_context); + sws_freeContext(sws_ctx); + av_freep(&p10_data[0]); + av_frame_free(&frame); + av_packet_free(&packet); + } + + // Returns true when a frame was decoded and written into video_buf. + // Returns false when g_running goes false. + bool get_next_frame(uint8_t* video_buf, uint32_t mxl_stride) { + while (dmf::g_running.load(std::memory_order_relaxed)) { + // Drain any frames buffered in the decoder first + if (avcodec_receive_frame(codec_context, frame) == 0) { + sws_scale( + sws_ctx, + frame->data, + frame->linesize, + 0, + source_info.height, + p10_data, + p10_linesizes + ); + dmf::v210::YUV422P10toV210( + reinterpret_cast(p10_data[0]), + reinterpret_cast(p10_data[1]), + reinterpret_cast(p10_data[2]), + video_buf, + source_info.width, + source_info.height, + p10_linesizes[0], + p10_linesizes[1], + p10_linesizes[2], + mxl_stride + ); + av_frame_unref(frame); + return true; + } + + // No buffered frame — read next packet + av_packet_unref(packet); + if (av_read_frame(format_context, packet) < 0) { + // EOF — loop back to start + avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(codec_context); + continue; + } + if (packet->stream_index != video_stream_index) continue; + avcodec_send_packet(codec_context, packet); + } + return false; + } + +private: + AVFormatContext* format_context = nullptr; + AVCodecContext* codec_context = nullptr; + AVPacket* packet = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + int video_stream_index = -1; + int audio_stream_index = -1; + + SwsContext* sws_ctx = nullptr; + int p10_linesizes[4] = {0, 0, 0, 0}; + uint8_t* p10_data[4] = {nullptr, nullptr, nullptr, nullptr}; + + bool open_file(const std::string& filename) { + if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) + throw std::runtime_error("Could not open file: " + filename); + + if (avformat_find_stream_info(format_context, nullptr) < 0) { avformat_close_input(&format_context); - sws_freeContext(sws_ctx); - av_freep(&p10_data[0]); - av_frame_free(&frame); - av_packet_free(&packet); + throw std::runtime_error("Could not find stream info"); } - bool get_next_frame(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audiobuf) { - while (dmf::g_running.load(std::memory_order_relaxed)) { - // Try to get a buffered frame from previous packet first - if (avcodec_receive_frame(codec_context, frame) == 0) { - sws_scale( - sws_ctx, - frame->data, - frame->linesize, - 0, - source_info.height, - p10_data, - p10_linesizes - ); - dmf::v210::YUV422P10toV210( - reinterpret_cast(p10_data[0]), - reinterpret_cast(p10_data[1]), - reinterpret_cast(p10_data[2]), - video_buf, - source_info.width, - source_info.height, - p10_linesizes[0], - p10_linesizes[1], - p10_linesizes[2], - mxl_stride - ); - av_frame_unref(frame); - return true; - } - // No buffered frame — read next packet - av_packet_unref(packet); - if (av_read_frame(format_context, packet) < 0) { - // EOF — seek back to start and keep going - avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); - avcodec_flush_buffers(codec_context); - av_packet_unref(packet); - continue; - } - if (packet->stream_index != video_stream_index) continue; - avcodec_send_packet(codec_context, packet); + for (unsigned int i = 0; i < format_context->nb_streams; ++i) { + const AVMediaType type = format_context->streams[i]->codecpar->codec_type; + if (type == AVMEDIA_TYPE_VIDEO && video_stream_index == -1) { + video_stream_index = static_cast(i); + have_video = true; + } else if (type == AVMEDIA_TYPE_AUDIO && audio_stream_index == -1) { + audio_stream_index = static_cast(i); + has_audio = true; } - return false; } - private: - AVFormatContext* format_context = nullptr; - AVCodecContext* codec_context = nullptr; - AVPacket* packet = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - int video_stream_index = -1; - int audio_stream_index = -1; - - // conversion data - struct SwsContext *sws_ctx{}; - int p10_linesizes[4] = {0, 0, 0, 0}; - uint8_t* p10_data[4] = {nullptr, nullptr, nullptr, nullptr}; - - bool open_file(std::string filename) { - if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) { - throw std::runtime_error("Could not open file: " + filename); - return false; - } - - // Find stream info - if (avformat_find_stream_info(format_context, nullptr) < 0) { - avformat_close_input(&format_context); - throw std::runtime_error("Could not find stream info"); - return false; - } - - // Find streams - for (unsigned int i = 0; i < format_context->nb_streams; i++) { - AVMediaType data_type = format_context->streams[i]->codecpar->codec_type; - if (data_type == AVMEDIA_TYPE_VIDEO) { - video_stream_index = i; - have_video = true; - } else if (data_type == AVMEDIA_TYPE_AUDIO && audio_stream_index == -1) { - // TODO: show list of available audio tracks and allow user to pick - // or handle multiple audio streams - audio_stream_index = i; - has_audio = true; - } - } - - if (video_stream_index == -1 && audio_stream_index == -1) { - avformat_close_input(&format_context); - throw std::runtime_error("No audio/video stream found"); - return false; - } - - return true; + if (video_stream_index == -1 && audio_stream_index == -1) { + avformat_close_input(&format_context); + throw std::runtime_error("No audio/video stream found in: " + filename); } - void get_source_info() { - // Get codec parameters - AVCodecParameters* codec_params = format_context->streams[video_stream_index]->codecpar; - const AVCodec* codec = avcodec_find_decoder(codec_params->codec_id); - - if (!codec) { - avformat_close_input(&format_context); - throw std::runtime_error("Unsupported codec"); - } - - // Open codec - codec_context = avcodec_alloc_context3(codec); - avcodec_parameters_to_context(codec_context, codec_params); - - if (avcodec_open2(codec_context, codec, nullptr) < 0) { - avcodec_free_context(&codec_context); - avformat_close_input(&format_context); - throw std::runtime_error("Could not open codec"); - } + return true; + } - AVRational fps = codec_context->framerate; - if (fps.num == 0 || fps.den == 0) { - fps = format_context->streams[video_stream_index]->avg_frame_rate; - } + void get_source_info() { + AVCodecParameters* codec_params = format_context->streams[video_stream_index]->codecpar; + const AVCodec* codec = avcodec_find_decoder(codec_params->codec_id); + if (!codec) + throw std::runtime_error("Unsupported codec"); - source_info.width = codec_context->width; - source_info.height = codec_context->height; - source_info.fps_num = fps.num; - source_info.fps_den = fps.den; - source_info.pix_fmt = codec_context->pix_fmt; + codec_context = avcodec_alloc_context3(codec); + if (avcodec_parameters_to_context(codec_context, codec_params) < 0) + throw std::runtime_error("Could not copy codec parameters"); + + if (avcodec_open2(codec_context, codec, nullptr) < 0) { + avcodec_free_context(&codec_context); + throw std::runtime_error("Could not open codec"); } - void allocate_conversion_buffers() - { - // Create Sws context to convert to planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), LE - sws_ctx = sws_getContext( - source_info.width, source_info.height, source_info.pix_fmt, - source_info.width, source_info.height, AV_PIX_FMT_YUV422P10LE, - 0, NULL, NULL, NULL - ); + AVRational fps = codec_context->framerate; + if (fps.num == 0 || fps.den == 0) + fps = format_context->streams[video_stream_index]->avg_frame_rate; - if (!sws_ctx) { - throw std::runtime_error("Failed to create SwsContext"); - return; - } + source_info.width = codec_context->width; + source_info.height = codec_context->height; + source_info.fps_num = fps.num; + source_info.fps_den = fps.den; + source_info.pix_fmt = codec_context->pix_fmt; + } - // Allocate P10 image (freed in destructor via av_freep(&p10_data[0])) - av_image_alloc( - p10_data, p10_linesizes, + void allocate_conversion_buffers() { + sws_ctx = sws_getContext( + source_info.width, source_info.height, source_info.pix_fmt, + source_info.width, source_info.height, AV_PIX_FMT_YUV422P10LE, + SWS_BILINEAR, nullptr, nullptr, nullptr + ); + if (!sws_ctx) + throw std::runtime_error("Failed to create SwsContext"); + + if (av_image_alloc(p10_data, p10_linesizes, source_info.width, source_info.height, - AV_PIX_FMT_YUV422P10LE, 64 - ); - - } - + AV_PIX_FMT_YUV422P10LE, 64) < 0) + throw std::runtime_error("Failed to allocate YUV422P10 buffer"); + } }; -} \ No newline at end of file + +} // namespace dmf -- 2.52.0 From b2534efb0f3f107f20df6e2f3a350689aaebb6bd Mon Sep 17 00:00:00 2001 From: itten Date: Sat, 4 Jul 2026 03:24:20 +0300 Subject: [PATCH 06/12] audio works, holy shit --- CMakeLists.txt | 1 + nodes/videoin/main.cpp | 109 +++++++++++++++++++--- shared/VideoReader.hpp | 202 +++++++++++++++++++++++++++++++---------- video-ndi.json | 9 +- 4 files changed, 258 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a30194..967131f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ pkg_check_modules(FFMPEG REQUIRED libavcodec libswscale libavutil + libswresample ) # Create an interface library for FFmpeg diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index d39893c..5549bbf 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -25,10 +25,10 @@ class VideoInNode : public dmf::NodeBase { const auto video_flow_info = config().at("video_flow_id"); const auto video_flow_id = video_flow_info.at("id").get(); - const int width = video_flow_info.value("width", video_reader.source_info.width); - const int height = video_flow_info.value("height", video_reader.source_info.height); - const int fps_num = video_flow_info.value("fps_num", video_reader.source_info.fps_num); - const int fps_den = video_flow_info.value("fps_den", video_reader.source_info.fps_den); + const int width = video_flow_info.value("width", video_reader.video_info.width); + const int height = video_flow_info.value("height", video_reader.video_info.height); + const int fps_num = video_flow_info.value("fps_num", video_reader.video_info.fps_num); + const int fps_den = video_flow_info.value("fps_den", video_reader.video_info.fps_den); log("video flow=%s %dx%d @ %d/%d fps", video_flow_id.c_str(), width, height, fps_num, fps_den); @@ -47,26 +47,107 @@ class VideoInNode : public dmf::NodeBase { log("video stride=%u B/line grain=%u B ring=%u grains", video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); + mxlFlowWriter audio_writer{}; + mxlFlowConfigInfo audio_cfg{}; + int sample_rate = video_reader.audio_info.sample_rate; + int channels = video_reader.audio_info.channels; + int bit_depth = 32; + bool has_audio = config().contains("audio_flow_id") && video_reader.has_audio; + + int max_audio_samples = 0; + if (has_audio) { + const auto audio_flow_info = config().at("audio_flow_id"); + const auto audio_flow_id = audio_flow_info.at("id").get(); + + log("audio flow=%s %d Hz %dch %d-bit", audio_flow_id.c_str(), sample_rate, channels, bit_depth); + + mxlStatus ast = mxlCreateFlowWriter( + instance(), + dmf::make_audio_flow_def(audio_flow_id, node_id(), sample_rate, channels, bit_depth, + fps_num, fps_den).c_str(), + "", &audio_writer, &audio_cfg, &created); + if (ast != MXL_STATUS_OK) { + log("audio mxlCreateFlowWriter failed (%s) — continuing without audio", dmf::mxl_status_str(ast)); + has_audio = false; + } else { + log("audio channels=%u buffer=%u samples", + audio_cfg.continuous.channelCount, audio_cfg.continuous.bufferLength); + } + + size_t max_write = 0; + mxlFlowWriterGetMaxWriteLengthSamples(audio_writer, &max_write); + max_audio_samples = static_cast(max_write); + } + std::vector audio_temp(max_audio_samples * channels * sizeof(float)); + const mxlRational video_rate = {fps_num, fps_den}; + uint64_t audio_index = 0; + if (has_audio) { + const mxlRational audio_rate = {sample_rate, 1}; + audio_index = mxlGetCurrentIndex(&audio_rate); + } uint64_t video_index = mxlGetCurrentIndex(&video_rate); while (dmf::g_running.load(std::memory_order_relaxed)) { - uint8_t* buf = nullptr; + uint8_t* video_buf = nullptr; mxlGrainInfo grain{}; - vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &buf); - if (vst == MXL_STATUS_OK) { - if (!video_reader.get_next_frame(buf, video_stride)) break; - grain.flags = 0; - grain.validSlices = grain.totalSlices; - mxlFlowWriterCommitGrain(video_writer, &grain); + vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &video_buf); + + int out_samples_written = 0; + dmf::VideoReader::FrameKind frame_kind = video_reader.get_next_frame( + video_buf, + video_stride, + has_audio ? audio_temp.data() : nullptr, + max_audio_samples, + out_samples_written + ); + + if (frame_kind == dmf::VideoReader::FrameKind::None) break; + + if (frame_kind == dmf::VideoReader::FrameKind::Video) { + if (vst == MXL_STATUS_OK) { + grain.flags = 0; + grain.validSlices = grain.totalSlices; + mxlFlowWriterCommitGrain(video_writer, &grain); + } + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; + } else if (frame_kind == dmf::VideoReader::FrameKind::Audio) { + mxlFlowWriterCancelGrain(video_writer); + mxlMutableWrappedMultiBufferSlice slice{}; + mxlStatus ast = mxlFlowWriterOpenSamples(audio_writer, audio_index, out_samples_written, &slice); + if (ast != MXL_STATUS_OK) { + log("audio OpenSamples failed (%s) index=%llu", dmf::mxl_status_str(ast), audio_index); + continue; + } + // copy per channel from audio_temp into slice + for (int ch = 0; ch < channels; ch++) { + uint8_t* dst0 = static_cast(slice.base.fragments[0].pointer) + + ch * slice.stride; + uint8_t* src = audio_temp.data() + ch * max_audio_samples * sizeof(float); + + size_t frag0_bytes = slice.base.fragments[0].size; + size_t total_bytes = out_samples_written * sizeof(float); + + if (total_bytes <= frag0_bytes) { + std::memcpy(dst0, src, total_bytes); + } else { + // Wrapped — copy first fragment, then second + std::memcpy(dst0, src, frag0_bytes); + uint8_t* dst1 = static_cast(slice.base.fragments[1].pointer) + + ch * slice.stride; + std::memcpy(dst1, src + frag0_bytes, total_bytes - frag0_bytes); + } + } + mxlFlowWriterCommitSamples(audio_writer); + audio_index += out_samples_written; } - const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); - if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); - video_index++; } log("stopped at video_index=%llu", video_index); mxlReleaseFlowWriter(instance(), video_writer); + mxlReleaseFlowWriter(instance(), audio_writer); } }; diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index 65f7925..1862bea 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -6,18 +6,23 @@ extern "C" { #include #include #include + #include + #include } #include #include +#include #include "Signal.hpp" #include "V210.hpp" +#include + namespace dmf { class VideoReader { public: - struct SourceInfo { + struct VideoInfo { int width = 0; int height = 0; int fps_num = 0; @@ -25,7 +30,17 @@ public: AVPixelFormat pix_fmt{}; }; - SourceInfo source_info{}; + struct AudioInfo { + int sample_rate = 0; + int channels = 0; + int samples = 0; + int channel_stride = 0; // floats between channel planes (NDI planar layout) + }; + + enum class FrameKind { None, Video, Audio }; + + VideoInfo video_info{}; + AudioInfo audio_info{}; bool has_audio = false; bool have_video = false; @@ -34,31 +49,42 @@ public: return; if (have_video) { get_source_info(); - allocate_conversion_buffers(); + if (have_video) { + allocate_video_conversion_buffers(); + } + if (has_audio) { + allocate_audio_conversion_buffers(); + } } } ~VideoReader() { - avcodec_free_context(&codec_context); + avcodec_free_context(&video_codec_context); avformat_close_input(&format_context); - sws_freeContext(sws_ctx); + sws_freeContext(sws_video_ctx); + swr_free(&swr_audio_ctx); av_freep(&p10_data[0]); - av_frame_free(&frame); + av_frame_free(&video_frame); + av_frame_free(&audio_frame); av_packet_free(&packet); } // Returns true when a frame was decoded and written into video_buf. // Returns false when g_running goes false. - bool get_next_frame(uint8_t* video_buf, uint32_t mxl_stride) { + FrameKind get_next_frame(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audio_buf, int max_audio_samples, int& out_samples_written) { while (dmf::g_running.load(std::memory_order_relaxed)) { // Drain any frames buffered in the decoder first - if (avcodec_receive_frame(codec_context, frame) == 0) { + if (avcodec_receive_frame(video_codec_context, video_frame) == 0) { + if (!video_buf) { + av_frame_unref(video_frame); + continue; // nowhere to write — discard frame + } sws_scale( - sws_ctx, - frame->data, - frame->linesize, + sws_video_ctx, + video_frame->data, + video_frame->linesize, 0, - source_info.height, + video_info.height, p10_data, p10_linesizes ); @@ -67,15 +93,48 @@ public: reinterpret_cast(p10_data[1]), reinterpret_cast(p10_data[2]), video_buf, - source_info.width, - source_info.height, + video_info.width, + video_info.height, p10_linesizes[0], p10_linesizes[1], p10_linesizes[2], mxl_stride ); - av_frame_unref(frame); - return true; + av_frame_unref(video_frame); + return FrameKind::Video; + } + + if (avcodec_receive_frame(audio_codec_context, audio_frame) == 0) { + if (!audio_buf) { + av_frame_unref(audio_frame); + continue; // nowhere to write — discard frame + } + + int dst_nb_samples = av_rescale_rnd( + swr_get_delay(swr_audio_ctx, audio_codec_context->sample_rate) + audio_frame->nb_samples, + audio_codec_context->sample_rate, audio_codec_context->sample_rate, AV_ROUND_UP + ); + + // Guard against buffer overflows + if (dst_nb_samples > max_audio_samples) { + dst_nb_samples = max_audio_samples; + } + + std::vector dst(audio_info.channels); + for (int ch = 0; ch < audio_info.channels; ch++) { + dst[ch] = audio_buf + ch * max_audio_samples * sizeof(float); + } + + // Convert/Resample the audio layout and sample format + int converted_samples = swr_convert( + swr_audio_ctx, + dst.data(), dst_nb_samples, + (const uint8_t**)audio_frame->data, audio_frame->nb_samples + ); + out_samples_written = converted_samples; + + av_frame_unref(audio_frame); + return FrameKind::Audio; } // No buffered frame — read next packet @@ -83,27 +142,38 @@ public: if (av_read_frame(format_context, packet) < 0) { // EOF — loop back to start avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); - avcodec_flush_buffers(codec_context); + avcodec_flush_buffers(video_codec_context); + swr_close(swr_audio_ctx); + swr_init(swr_audio_ctx); continue; } - if (packet->stream_index != video_stream_index) continue; - avcodec_send_packet(codec_context, packet); + + if (packet->stream_index == video_stream_index) { + avcodec_send_packet(video_codec_context, packet); + } else if (packet->stream_index == audio_stream_index) { + avcodec_send_packet(audio_codec_context, packet); + } } - return false; + return FrameKind::None; } private: - AVFormatContext* format_context = nullptr; - AVCodecContext* codec_context = nullptr; - AVPacket* packet = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); int video_stream_index = -1; int audio_stream_index = -1; - SwsContext* sws_ctx = nullptr; + AVFormatContext* format_context = nullptr; + AVPacket* packet = av_packet_alloc(); + + AVCodecContext* video_codec_context = nullptr; + AVFrame* video_frame = av_frame_alloc(); + SwsContext* sws_video_ctx = nullptr; int p10_linesizes[4] = {0, 0, 0, 0}; uint8_t* p10_data[4] = {nullptr, nullptr, nullptr, nullptr}; + AVCodecContext* audio_codec_context = nullptr; + AVFrame* audio_frame = av_frame_alloc(); + SwrContext* swr_audio_ctx = nullptr; + bool open_file(const std::string& filename) { if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) throw std::runtime_error("Could not open file: " + filename); @@ -133,45 +203,83 @@ private: } void get_source_info() { - AVCodecParameters* codec_params = format_context->streams[video_stream_index]->codecpar; - const AVCodec* codec = avcodec_find_decoder(codec_params->codec_id); - if (!codec) - throw std::runtime_error("Unsupported codec"); + AVCodecParameters* video_codec_params = format_context->streams[video_stream_index]->codecpar; + const AVCodec* video_codec = avcodec_find_decoder(video_codec_params->codec_id); + if (!video_codec) + throw std::runtime_error("Unsupported video codec"); - codec_context = avcodec_alloc_context3(codec); - if (avcodec_parameters_to_context(codec_context, codec_params) < 0) - throw std::runtime_error("Could not copy codec parameters"); + video_codec_context = avcodec_alloc_context3(video_codec); + if (avcodec_parameters_to_context(video_codec_context, video_codec_params) < 0) + throw std::runtime_error("Could not copy video codec parameters"); - if (avcodec_open2(codec_context, codec, nullptr) < 0) { - avcodec_free_context(&codec_context); - throw std::runtime_error("Could not open codec"); + if (avcodec_open2(video_codec_context, video_codec, nullptr) < 0) { + avcodec_free_context(&video_codec_context); + throw std::runtime_error("Could not open video codec"); } - AVRational fps = codec_context->framerate; + AVRational fps = video_codec_context->framerate; if (fps.num == 0 || fps.den == 0) fps = format_context->streams[video_stream_index]->avg_frame_rate; - source_info.width = codec_context->width; - source_info.height = codec_context->height; - source_info.fps_num = fps.num; - source_info.fps_den = fps.den; - source_info.pix_fmt = codec_context->pix_fmt; + video_info.width = video_codec_context->width; + video_info.height = video_codec_context->height; + video_info.fps_num = fps.num; + video_info.fps_den = fps.den; + video_info.pix_fmt = video_codec_context->pix_fmt; + + // audio part + if (audio_stream_index == -1) return; + AVCodecParameters* audio_codec_params = format_context->streams[audio_stream_index]->codecpar; + const AVCodec* audio_codec = avcodec_find_decoder(audio_codec_params->codec_id); + if (!audio_codec) + throw std::runtime_error("Unsupported audio codec"); + + audio_codec_context = avcodec_alloc_context3(audio_codec); + if (avcodec_parameters_to_context(audio_codec_context, audio_codec_params) < 0) + throw std::runtime_error("Could not copy audio codec parameters"); + + if (avcodec_open2(audio_codec_context, audio_codec, nullptr) < 0) { + avcodec_free_context(&audio_codec_context); + throw std::runtime_error("Could not open audio codec"); + } + audio_info.sample_rate = audio_codec_context->sample_rate; + audio_info.channels = audio_codec_context->ch_layout.nb_channels; } - void allocate_conversion_buffers() { - sws_ctx = sws_getContext( - source_info.width, source_info.height, source_info.pix_fmt, - source_info.width, source_info.height, AV_PIX_FMT_YUV422P10LE, + void allocate_video_conversion_buffers() { + sws_video_ctx = sws_getContext( + video_info.width, video_info.height, video_info.pix_fmt, + video_info.width, video_info.height, AV_PIX_FMT_YUV422P10LE, SWS_BILINEAR, nullptr, nullptr, nullptr ); - if (!sws_ctx) + if (!sws_video_ctx) throw std::runtime_error("Failed to create SwsContext"); if (av_image_alloc(p10_data, p10_linesizes, - source_info.width, source_info.height, + video_info.width, video_info.height, AV_PIX_FMT_YUV422P10LE, 64) < 0) throw std::runtime_error("Failed to allocate YUV422P10 buffer"); } + + void allocate_audio_conversion_buffers() { + swr_audio_ctx = swr_alloc(); + + // Set input options + av_opt_set_chlayout(swr_audio_ctx, "in_chlayout", &audio_codec_context->ch_layout, 0); + av_opt_set_int(swr_audio_ctx, "in_sample_rate", audio_info.sample_rate, 0); + av_opt_set_sample_fmt(swr_audio_ctx, "in_sample_fmt", audio_codec_context->sample_fmt, 0); + + // Set output options + av_opt_set_chlayout(swr_audio_ctx, "out_chlayout", &audio_codec_context->ch_layout, 0); + av_opt_set_int(swr_audio_ctx, "out_sample_rate", audio_info.sample_rate, 0); + av_opt_set_sample_fmt(swr_audio_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); + + // Initialize the context + if (swr_init(swr_audio_ctx) < 0) { + throw std::runtime_error("Failed to create SwrContext"); + } + + } }; } // namespace dmf diff --git a/video-ndi.json b/video-ndi.json index c908c44..1e354a5 100644 --- a/video-ndi.json +++ b/video-ndi.json @@ -1,13 +1,18 @@ { "nodes": [ - { "id": "videoin", "type": "videoin", "params": {} }, + { "id": "videoin", "type": "videoin", "params": {"file": "/home/itten/test-vid/2.ts"} }, { "id": "ndiout", "type": "ndiout", "params": {} } ], "edges": [ { "from": "videoin", "from_port": "video_flow_id", "to": "ndiout", "to_port": "video_flow_id", - "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 25, "fps_den": 1 } + "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 60, "fps_den": 1 } + }, + { + "from": "videoin", "from_port": "audio_flow_id", + "to": "ndiout", "to_port": "audio_flow_id", + "format": { "kind": "audio", "sample_rate": 44100, "channels": 2, "bit_depth": 32 } } ] } -- 2.52.0 From 166efc3c595d092720be168dea4538eb6b79427b Mon Sep 17 00:00:00 2001 From: itten Date: Sun, 5 Jul 2026 12:39:06 +0300 Subject: [PATCH 07/12] Some fixes. For a now we fully support only .ts, due to huge problem with h264/hevc audio bursts --- nodes/videoin/main.cpp | 114 +++++++++++++++++++++++++---------------- shared/VideoReader.hpp | 65 +++++++++++------------ video-ndi.json | 6 +-- 3 files changed, 106 insertions(+), 79 deletions(-) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index 5549bbf..315b485 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -21,31 +21,40 @@ class VideoInNode : public dmf::NodeBase { log("file: %s", filename.c_str()); dmf::VideoReader video_reader(filename); - if (!video_reader.have_video) { log("no video stream found"); return; } - - const auto video_flow_info = config().at("video_flow_id"); - const auto video_flow_id = video_flow_info.at("id").get(); - const int width = video_flow_info.value("width", video_reader.video_info.width); - const int height = video_flow_info.value("height", video_reader.video_info.height); - const int fps_num = video_flow_info.value("fps_num", video_reader.video_info.fps_num); - const int fps_den = video_flow_info.value("fps_den", video_reader.video_info.fps_den); - - log("video flow=%s %dx%d @ %d/%d fps", video_flow_id.c_str(), width, height, fps_num, fps_den); - - mxlFlowWriter video_writer = nullptr; - mxlFlowConfigInfo video_cfg = {}; - bool created = false; - mxlStatus vst = mxlCreateFlowWriter( - instance(), - dmf::make_video_flow_def(video_flow_id, node_id(), width, height, fps_num, fps_den).c_str(), - "", &video_writer, &video_cfg, &created); - if (vst != MXL_STATUS_OK) { - log("mxlCreateFlowWriter failed (%s)", dmf::mxl_status_str(vst)); - return; + if (!video_reader.have_video && !video_reader.has_audio) { + log("no video or audio stream found"); return; + } + + const bool has_video = config().contains("video_flow_id") && video_reader.have_video; + mxlFlowWriter video_writer = nullptr; + mxlFlowConfigInfo video_cfg{}; + uint32_t video_stride = 0; + int width = 0, height = 0, fps_num = 25, fps_den = 1; + std::string video_flow_id; + + if (has_video) { + const auto video_flow_info = config().at("video_flow_id"); + video_flow_id = video_flow_info.at("id").get(); + width = video_flow_info.value("width", video_reader.video_info.width); + height = video_flow_info.value("height", video_reader.video_info.height); + fps_num = video_flow_info.value("fps_num", video_reader.video_info.fps_num); + fps_den = video_flow_info.value("fps_den", video_reader.video_info.fps_den); + + log("video flow=%s %dx%d @ %d/%d fps", video_flow_id.c_str(), width, height, fps_num, fps_den); + + bool created = false; + mxlStatus vst = mxlCreateFlowWriter( + instance(), + dmf::make_video_flow_def(video_flow_id, node_id(), width, height, fps_num, fps_den).c_str(), + "", &video_writer, &video_cfg, &created); + if (vst != MXL_STATUS_OK) { + log("mxlCreateFlowWriter failed (%s)", dmf::mxl_status_str(vst)); + return; + } + video_stride = video_cfg.discrete.sliceSizes[0]; + log("video stride=%u B/line grain=%u B ring=%u grains", + video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); } - const uint32_t video_stride = video_cfg.discrete.sliceSizes[0]; - log("video stride=%u B/line grain=%u B ring=%u grains", - video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); mxlFlowWriter audio_writer{}; mxlFlowConfigInfo audio_cfg{}; @@ -53,7 +62,7 @@ class VideoInNode : public dmf::NodeBase { int channels = video_reader.audio_info.channels; int bit_depth = 32; bool has_audio = config().contains("audio_flow_id") && video_reader.has_audio; - + int max_audio_samples = 0; if (has_audio) { const auto audio_flow_info = config().at("audio_flow_id"); @@ -61,6 +70,7 @@ class VideoInNode : public dmf::NodeBase { log("audio flow=%s %d Hz %dch %d-bit", audio_flow_id.c_str(), sample_rate, channels, bit_depth); + bool created = false; mxlStatus ast = mxlCreateFlowWriter( instance(), dmf::make_audio_flow_def(audio_flow_id, node_id(), sample_rate, channels, bit_depth, @@ -81,55 +91,71 @@ class VideoInNode : public dmf::NodeBase { std::vector audio_temp(max_audio_samples * channels * sizeof(float)); const mxlRational video_rate = {fps_num, fps_den}; + const mxlRational audio_rate = {sample_rate, 1}; uint64_t audio_index = 0; if (has_audio) { - const mxlRational audio_rate = {sample_rate, 1}; audio_index = mxlGetCurrentIndex(&audio_rate); } - uint64_t video_index = mxlGetCurrentIndex(&video_rate); + uint64_t video_index = has_video ? mxlGetCurrentIndex(&video_rate) : 0; while (dmf::g_running.load(std::memory_order_relaxed)) { - uint8_t* video_buf = nullptr; - mxlGrainInfo grain{}; - vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &video_buf); + uint8_t* video_buf = nullptr; + mxlGrainInfo grain{}; + mxlStatus vst = MXL_ERR_UNSUPPORTED_OPERATION; + if (has_video) { + vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &video_buf); + } int out_samples_written = 0; dmf::VideoReader::FrameKind frame_kind = video_reader.get_next_frame( - video_buf, + has_video ? video_buf : nullptr, video_stride, has_audio ? audio_temp.data() : nullptr, max_audio_samples, out_samples_written ); - + if (frame_kind == dmf::VideoReader::FrameKind::None) break; if (frame_kind == dmf::VideoReader::FrameKind::Video) { - if (vst == MXL_STATUS_OK) { + if (has_video && vst == MXL_STATUS_OK) { grain.flags = 0; grain.validSlices = grain.totalSlices; mxlFlowWriterCommitGrain(video_writer, &grain); } - const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); - if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); - video_index++; + if (has_video) { + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; + } } else if (frame_kind == dmf::VideoReader::FrameKind::Audio) { - mxlFlowWriterCancelGrain(video_writer); + if (has_video && vst == MXL_STATUS_OK) mxlFlowWriterCancelGrain(video_writer); + // Wait for audio clock to catch up — prevents TOO_EARLY and sample loss + while (dmf::g_running.load(std::memory_order_relaxed)) { + const uint64_t audio_now = mxlGetCurrentIndex(&audio_rate); + if (audio_index + static_cast(out_samples_written) <= audio_now) break; + const uint64_t ns = mxlGetNsUntilIndex(audio_index + out_samples_written, &audio_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + else break; + } + if (!dmf::g_running.load(std::memory_order_relaxed)) break; + mxlMutableWrappedMultiBufferSlice slice{}; mxlStatus ast = mxlFlowWriterOpenSamples(audio_writer, audio_index, out_samples_written, &slice); if (ast != MXL_STATUS_OK) { - log("audio OpenSamples failed (%s) index=%llu", dmf::mxl_status_str(ast), audio_index); + log("audio OpenSamples failed (%s) index=%llu — skipping", dmf::mxl_status_str(ast), audio_index); + audio_index += out_samples_written; // advance even on failure — keeps alignment continue; } // copy per channel from audio_temp into slice for (int ch = 0; ch < channels; ch++) { - uint8_t* dst0 = static_cast(slice.base.fragments[0].pointer) + uint8_t* dst0 = static_cast(slice.base.fragments[0].pointer) + ch * slice.stride; uint8_t* src = audio_temp.data() + ch * max_audio_samples * sizeof(float); - + size_t frag0_bytes = slice.base.fragments[0].size; size_t total_bytes = out_samples_written * sizeof(float); - + if (total_bytes <= frag0_bytes) { std::memcpy(dst0, src, total_bytes); } else { @@ -145,9 +171,9 @@ class VideoInNode : public dmf::NodeBase { } } - log("stopped at video_index=%llu", video_index); - mxlReleaseFlowWriter(instance(), video_writer); - mxlReleaseFlowWriter(instance(), audio_writer); + log("stopped at video_index=%llu", static_cast(video_index)); + if (has_video) mxlReleaseFlowWriter(instance(), video_writer); + if (has_audio) mxlReleaseFlowWriter(instance(), audio_writer); } }; diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index 1862bea..3c620fc 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -47,19 +47,15 @@ public: explicit VideoReader(const std::string& filename) { if (!open_file(filename)) return; - if (have_video) { - get_source_info(); - if (have_video) { - allocate_video_conversion_buffers(); - } - if (has_audio) { - allocate_audio_conversion_buffers(); - } - } + if (!have_video && !has_audio) return; + get_source_info(); + if (have_video) allocate_video_conversion_buffers(); + if (has_audio) allocate_audio_conversion_buffers(); } ~VideoReader() { avcodec_free_context(&video_codec_context); + avcodec_free_context(&audio_codec_context); avformat_close_input(&format_context); sws_freeContext(sws_video_ctx); swr_free(&swr_audio_ctx); @@ -74,7 +70,7 @@ public: FrameKind get_next_frame(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audio_buf, int max_audio_samples, int& out_samples_written) { while (dmf::g_running.load(std::memory_order_relaxed)) { // Drain any frames buffered in the decoder first - if (avcodec_receive_frame(video_codec_context, video_frame) == 0) { + if (have_video && avcodec_receive_frame(video_codec_context, video_frame) == 0) { if (!video_buf) { av_frame_unref(video_frame); continue; // nowhere to write — discard frame @@ -104,7 +100,7 @@ public: return FrameKind::Video; } - if (avcodec_receive_frame(audio_codec_context, audio_frame) == 0) { + if (has_audio && avcodec_receive_frame(audio_codec_context, audio_frame) == 0) { if (!audio_buf) { av_frame_unref(audio_frame); continue; // nowhere to write — discard frame @@ -142,7 +138,8 @@ public: if (av_read_frame(format_context, packet) < 0) { // EOF — loop back to start avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); - avcodec_flush_buffers(video_codec_context); + if (have_video) avcodec_flush_buffers(video_codec_context); + if (has_audio) avcodec_flush_buffers(audio_codec_context); swr_close(swr_audio_ctx); swr_init(swr_audio_ctx); continue; @@ -203,30 +200,34 @@ private: } void get_source_info() { - AVCodecParameters* video_codec_params = format_context->streams[video_stream_index]->codecpar; - const AVCodec* video_codec = avcodec_find_decoder(video_codec_params->codec_id); - if (!video_codec) - throw std::runtime_error("Unsupported video codec"); + if (video_stream_index != -1) { + AVCodecParameters* video_codec_params = format_context->streams[video_stream_index]->codecpar; + const AVCodec* video_codec = avcodec_find_decoder(video_codec_params->codec_id); + if (!video_codec) + throw std::runtime_error("Unsupported video codec"); - video_codec_context = avcodec_alloc_context3(video_codec); - if (avcodec_parameters_to_context(video_codec_context, video_codec_params) < 0) - throw std::runtime_error("Could not copy video codec parameters"); + video_codec_context = avcodec_alloc_context3(video_codec); + if (avcodec_parameters_to_context(video_codec_context, video_codec_params) < 0) + throw std::runtime_error("Could not copy video codec parameters"); - if (avcodec_open2(video_codec_context, video_codec, nullptr) < 0) { - avcodec_free_context(&video_codec_context); - throw std::runtime_error("Could not open video codec"); + if (avcodec_open2(video_codec_context, video_codec, nullptr) < 0) { + avcodec_free_context(&video_codec_context); + throw std::runtime_error("Could not open video codec"); + } + + AVRational fps = video_codec_context->framerate; + if (fps.num == 0 || fps.den == 0) + fps = format_context->streams[video_stream_index]->avg_frame_rate; + + video_info.width = video_codec_context->width; + video_info.height = video_codec_context->height; + video_info.fps_num = fps.num; + video_info.fps_den = fps.den; + video_info.pix_fmt = video_codec_context->pix_fmt; + } else { + have_video = false; } - AVRational fps = video_codec_context->framerate; - if (fps.num == 0 || fps.den == 0) - fps = format_context->streams[video_stream_index]->avg_frame_rate; - - video_info.width = video_codec_context->width; - video_info.height = video_codec_context->height; - video_info.fps_num = fps.num; - video_info.fps_den = fps.den; - video_info.pix_fmt = video_codec_context->pix_fmt; - // audio part if (audio_stream_index == -1) return; AVCodecParameters* audio_codec_params = format_context->streams[audio_stream_index]->codecpar; diff --git a/video-ndi.json b/video-ndi.json index 1e354a5..c2b8d0b 100644 --- a/video-ndi.json +++ b/video-ndi.json @@ -1,18 +1,18 @@ { "nodes": [ - { "id": "videoin", "type": "videoin", "params": {"file": "/home/itten/test-vid/2.ts"} }, + { "id": "videoin", "type": "videoin", "params": {"file": "/home/itten/test-vid/1.ts"} }, { "id": "ndiout", "type": "ndiout", "params": {} } ], "edges": [ { "from": "videoin", "from_port": "video_flow_id", "to": "ndiout", "to_port": "video_flow_id", - "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 60, "fps_den": 1 } + "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 24, "fps_den": 1 } }, { "from": "videoin", "from_port": "audio_flow_id", "to": "ndiout", "to_port": "audio_flow_id", - "format": { "kind": "audio", "sample_rate": 44100, "channels": 2, "bit_depth": 32 } + "format": { "kind": "audio", "sample_rate": 48000, "channels": 6, "bit_depth": 32 } } ] } -- 2.52.0 From 0298cf9b489587c175a13b7ec362aab3e3605712 Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Sun, 5 Jul 2026 12:44:35 +0300 Subject: [PATCH 08/12] refactor: VideoReader and videoin post-audio review cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VideoReader: - Remove unused #include - Remove dead AudioInfo::samples and ::channel_stride fields - Rename have_video → has_video (consistent with has_audio) - Rename get_next_frame → read_next (returns audio too, not just frames) - Fix outdated comment on read_next - Remove trailing blank line in allocate_audio_conversion_buffers videoin: - Remove redundant FFmpeg includes (VideoReader.hpp provides them) - Fix bug: mxlFlowWriterGetMaxWriteLengthSamples called with invalid audio_writer when mxlCreateFlowWriter fails — moved inside else branch - Rename call site: get_next_frame → read_next - Rename have_video → has_video at call sites - Use = nullptr for audio_writer (consistent with video_writer) Co-Authored-By: Claude Sonnet 4.6 --- nodes/videoin/main.cpp | 23 +++++++---------------- shared/VideoReader.hpp | 28 +++++++++++----------------- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index 315b485..0361a42 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -1,11 +1,3 @@ -extern "C" { - #include - #include - #include - #include - #include -} - #include #include #include @@ -21,11 +13,11 @@ class VideoInNode : public dmf::NodeBase { log("file: %s", filename.c_str()); dmf::VideoReader video_reader(filename); - if (!video_reader.have_video && !video_reader.has_audio) { + if (!video_reader.has_video && !video_reader.has_audio) { log("no video or audio stream found"); return; } - const bool has_video = config().contains("video_flow_id") && video_reader.have_video; + const bool has_video = config().contains("video_flow_id") && video_reader.has_video; mxlFlowWriter video_writer = nullptr; mxlFlowConfigInfo video_cfg{}; uint32_t video_stride = 0; @@ -56,7 +48,7 @@ class VideoInNode : public dmf::NodeBase { video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); } - mxlFlowWriter audio_writer{}; + mxlFlowWriter audio_writer = nullptr; mxlFlowConfigInfo audio_cfg{}; int sample_rate = video_reader.audio_info.sample_rate; int channels = video_reader.audio_info.channels; @@ -82,11 +74,10 @@ class VideoInNode : public dmf::NodeBase { } else { log("audio channels=%u buffer=%u samples", audio_cfg.continuous.channelCount, audio_cfg.continuous.bufferLength); + size_t max_write = 0; + mxlFlowWriterGetMaxWriteLengthSamples(audio_writer, &max_write); + max_audio_samples = static_cast(max_write); } - - size_t max_write = 0; - mxlFlowWriterGetMaxWriteLengthSamples(audio_writer, &max_write); - max_audio_samples = static_cast(max_write); } std::vector audio_temp(max_audio_samples * channels * sizeof(float)); @@ -107,7 +98,7 @@ class VideoInNode : public dmf::NodeBase { } int out_samples_written = 0; - dmf::VideoReader::FrameKind frame_kind = video_reader.get_next_frame( + dmf::VideoReader::FrameKind frame_kind = video_reader.read_next( has_video ? video_buf : nullptr, video_stride, has_audio ? audio_temp.data() : nullptr, diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index 3c620fc..2ae087d 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -16,8 +16,6 @@ extern "C" { #include "Signal.hpp" #include "V210.hpp" -#include - namespace dmf { class VideoReader { @@ -31,10 +29,8 @@ public: }; struct AudioInfo { - int sample_rate = 0; - int channels = 0; - int samples = 0; - int channel_stride = 0; // floats between channel planes (NDI planar layout) + int sample_rate = 0; + int channels = 0; }; enum class FrameKind { None, Video, Audio }; @@ -42,14 +38,14 @@ public: VideoInfo video_info{}; AudioInfo audio_info{}; bool has_audio = false; - bool have_video = false; + bool has_video = false; explicit VideoReader(const std::string& filename) { if (!open_file(filename)) return; - if (!have_video && !has_audio) return; + if (!has_video && !has_audio) return; get_source_info(); - if (have_video) allocate_video_conversion_buffers(); + if (has_video) allocate_video_conversion_buffers(); if (has_audio) allocate_audio_conversion_buffers(); } @@ -65,12 +61,11 @@ public: av_packet_free(&packet); } - // Returns true when a frame was decoded and written into video_buf. - // Returns false when g_running goes false. - FrameKind get_next_frame(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audio_buf, int max_audio_samples, int& out_samples_written) { + // Returns Video or Audio when a frame/packet was decoded, None when g_running goes false. + FrameKind read_next(uint8_t* video_buf, uint32_t mxl_stride, uint8_t* audio_buf, int max_audio_samples, int& out_samples_written) { while (dmf::g_running.load(std::memory_order_relaxed)) { // Drain any frames buffered in the decoder first - if (have_video && avcodec_receive_frame(video_codec_context, video_frame) == 0) { + if (has_video && avcodec_receive_frame(video_codec_context, video_frame) == 0) { if (!video_buf) { av_frame_unref(video_frame); continue; // nowhere to write — discard frame @@ -138,7 +133,7 @@ public: if (av_read_frame(format_context, packet) < 0) { // EOF — loop back to start avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); - if (have_video) avcodec_flush_buffers(video_codec_context); + if (has_video) avcodec_flush_buffers(video_codec_context); if (has_audio) avcodec_flush_buffers(audio_codec_context); swr_close(swr_audio_ctx); swr_init(swr_audio_ctx); @@ -184,7 +179,7 @@ private: const AVMediaType type = format_context->streams[i]->codecpar->codec_type; if (type == AVMEDIA_TYPE_VIDEO && video_stream_index == -1) { video_stream_index = static_cast(i); - have_video = true; + has_video = true; } else if (type == AVMEDIA_TYPE_AUDIO && audio_stream_index == -1) { audio_stream_index = static_cast(i); has_audio = true; @@ -225,7 +220,7 @@ private: video_info.fps_den = fps.den; video_info.pix_fmt = video_codec_context->pix_fmt; } else { - have_video = false; + has_video = false; } // audio part @@ -279,7 +274,6 @@ private: if (swr_init(swr_audio_ctx) < 0) { throw std::runtime_error("Failed to create SwrContext"); } - } }; -- 2.52.0 From fe6b7b10ed98f19ab1a430db58b7ebe36c0175ce Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Sun, 5 Jul 2026 12:46:00 +0300 Subject: [PATCH 09/12] fix: VideoReader crash on video-only file at EOF and dead bool return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard swr_close/swr_init at EOF inside if (has_audio) — calling swr_init(nullptr) on a video-only file crashed at first loop - Change open_file from bool to void — it never returned false, only threw, so the if (!open_file()) check in the constructor was dead code Co-Authored-By: Claude Sonnet 4.6 --- shared/VideoReader.hpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/shared/VideoReader.hpp b/shared/VideoReader.hpp index 2ae087d..d7814b2 100644 --- a/shared/VideoReader.hpp +++ b/shared/VideoReader.hpp @@ -41,9 +41,7 @@ public: bool has_video = false; explicit VideoReader(const std::string& filename) { - if (!open_file(filename)) - return; - if (!has_video && !has_audio) return; + open_file(filename); get_source_info(); if (has_video) allocate_video_conversion_buffers(); if (has_audio) allocate_audio_conversion_buffers(); @@ -134,9 +132,11 @@ public: // EOF — loop back to start avformat_seek_file(format_context, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD); if (has_video) avcodec_flush_buffers(video_codec_context); - if (has_audio) avcodec_flush_buffers(audio_codec_context); - swr_close(swr_audio_ctx); - swr_init(swr_audio_ctx); + if (has_audio) { + avcodec_flush_buffers(audio_codec_context); + swr_close(swr_audio_ctx); + swr_init(swr_audio_ctx); + } continue; } @@ -166,7 +166,7 @@ private: AVFrame* audio_frame = av_frame_alloc(); SwrContext* swr_audio_ctx = nullptr; - bool open_file(const std::string& filename) { + void open_file(const std::string& filename) { if (avformat_open_input(&format_context, filename.c_str(), nullptr, nullptr) != 0) throw std::runtime_error("Could not open file: " + filename); @@ -186,12 +186,10 @@ private: } } - if (video_stream_index == -1 && audio_stream_index == -1) { + if (!has_video && !has_audio) { avformat_close_input(&format_context); throw std::runtime_error("No audio/video stream found in: " + filename); } - - return true; } void get_source_info() { -- 2.52.0 From 704769064781acf61037203c500f9115deb6387d Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Sun, 5 Jul 2026 12:49:23 +0300 Subject: [PATCH 10/12] fix: resync video_index to MXL clock to prevent falling behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After each video frame sleep, use mxlGetCurrentIndex instead of a simple +1 increment — if audio processing ate into the next frame's time we now skip the stale index rather than writing a late grain. Same pattern ndiin already uses. Co-Authored-By: Claude Sonnet 4.6 --- nodes/videoin/main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index 0361a42..63f1d57 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -117,7 +117,7 @@ class VideoInNode : public dmf::NodeBase { if (has_video) { const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); - video_index++; + video_index = mxlGetCurrentIndex(&video_rate); } } else if (frame_kind == dmf::VideoReader::FrameKind::Audio) { if (has_video && vst == MXL_STATUS_OK) mxlFlowWriterCancelGrain(video_writer); @@ -159,6 +159,10 @@ class VideoInNode : public dmf::NodeBase { } mxlFlowWriterCommitSamples(audio_writer); audio_index += out_samples_written; + if (has_video) { + const uint64_t current = mxlGetCurrentIndex(&video_rate); + if (current > video_index) video_index = current; + } } } -- 2.52.0 From 1496cc2e1e2abb5314a9bd64bfd6c363b95394be Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Sun, 5 Jul 2026 12:52:48 +0300 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20rename=20video=20port=20flow?= =?UTF-8?q?=5Fid=20=E2=86=92=20video=5Fflow=5Fid=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All nodes now use video_flow_id/audio_flow_id consistently: - nodes/testpattern: flow_id → video_flow_id - nodes/fakesink: flow_id → video_flow_id - graph.json: from_port/to_port flow_id → video_flow_id - studio-manager: hardcoded build_graph edges + FlowGraph.hpp comment Co-Authored-By: Claude Sonnet 4.6 --- graph.json | 4 ++-- nodes/fakesink/main.cpp | 2 +- nodes/testpattern/main.cpp | 2 +- studio-manager/FlowGraph.hpp | 2 +- studio-manager/main.cpp | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/graph.json b/graph.json index 66daf64..ebd40da 100644 --- a/graph.json +++ b/graph.json @@ -5,8 +5,8 @@ ], "edges": [ { - "from": "testpattern", "from_port": "flow_id", - "to": "ndiout", "to_port": "flow_id", + "from": "testpattern", "from_port": "video_flow_id", + "to": "ndiout", "to_port": "video_flow_id", "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 25, "fps_den": 1 } }, { diff --git a/nodes/fakesink/main.cpp b/nodes/fakesink/main.cpp index 8cebc37..8210205 100644 --- a/nodes/fakesink/main.cpp +++ b/nodes/fakesink/main.cpp @@ -6,7 +6,7 @@ class FakeSinkNode : public dmf::NodeBase { void run() override { - const auto flow_info = config().at("flow_id"); + const auto flow_info = config().at("video_flow_id"); const auto flow_id = flow_info.at("id").get(); const int fps_num = flow_info.value("fps_num", 25); const int fps_den = flow_info.value("fps_den", 1); diff --git a/nodes/testpattern/main.cpp b/nodes/testpattern/main.cpp index 27e8974..84ca0fa 100644 --- a/nodes/testpattern/main.cpp +++ b/nodes/testpattern/main.cpp @@ -10,7 +10,7 @@ class TestPatternNode : public dmf::NodeBase { void run() override { // --- video flow --- - const auto flow_info = config().at("flow_id"); + const auto flow_info = config().at("video_flow_id"); const auto flow_id = flow_info.at("id").get(); const int width = flow_info.value("width", 1920); const int height = flow_info.value("height", 1080); diff --git a/studio-manager/FlowGraph.hpp b/studio-manager/FlowGraph.hpp index 398c693..8c0bb25 100644 --- a/studio-manager/FlowGraph.hpp +++ b/studio-manager/FlowGraph.hpp @@ -17,7 +17,7 @@ struct NodeDef { // // from_port / to_port are the NODE_CONFIG keys each end receives the flow descriptor under. // The injected value is a JSON object: { "id": "", ...format fields }. -// Nodes read the UUID as cfg["flow_id"]["id"] and format fields as cfg["flow_id"]["fps_num"] etc. +// Nodes read the UUID as cfg["video_flow_id"]["id"] and format fields as cfg["video_flow_id"]["fps_num"] etc. // // format examples: // video: { "kind":"video", "width":1920, "height":1080, "fps_num":25, "fps_den":1 } diff --git a/studio-manager/main.cpp b/studio-manager/main.cpp index e5007e7..a7591b9 100644 --- a/studio-manager/main.cpp +++ b/studio-manager/main.cpp @@ -106,11 +106,11 @@ static dmf::FlowGraph build_graph() { {"kind","audio"}, {"sample_rate",48000}, {"channels",2}, {"bit_depth",32} }; g.edges = { - // { tp_video_flow, "testpattern", "flow_id", "fakesink", "flow_id", video_fmt }, + // { tp_video_flow, "testpattern", "video_flow_id", "fakesink", "video_flow_id", video_fmt }, // { tp_audio_flow, "testpattern", "audio_flow_id", "", "", audio_fmt }, - // { ndi_video_flow, "ndiin", "video_flow_id", "ndiout", "flow_id", video_fmt }, + // { ndi_video_flow, "ndiin", "video_flow_id", "ndiout", "video_flow_id", video_fmt }, // { ndi_audio_flow, "ndiin", "audio_flow_id", "", "", audio_fmt }, - { tp_video_flow, "testpattern", "flow_id", "ndiout", "flow_id", video_fmt }, + { tp_video_flow, "testpattern", "video_flow_id", "ndiout", "video_flow_id", video_fmt }, { tp_audio_flow, "testpattern", "audio_flow_id", "ndiout", "audio_flow_id", audio_fmt }, }; return g; -- 2.52.0 From a16cc7c3b70688b821e41166200e36a61723c46d Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Sun, 5 Jul 2026 12:54:23 +0300 Subject: [PATCH 12/12] docs: document .ts-only audio pacing limitation in videoin Co-Authored-By: Claude Sonnet 4.6 --- nodes/videoin/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nodes/videoin/main.cpp b/nodes/videoin/main.cpp index 63f1d57..0e98ef6 100644 --- a/nodes/videoin/main.cpp +++ b/nodes/videoin/main.cpp @@ -12,6 +12,9 @@ class VideoInNode : public dmf::NodeBase { if (filename.empty()) { log("config missing 'file'"); return; } log("file: %s", filename.c_str()); + // Audio pacing assumes steady fixed-size chunks from the demuxer (e.g. 1024-sample AAC + // packets in MPEG-TS). Containers like MP4/MKV can deliver audio in large bursts, which + // would require a separate metering buffer to pace correctly. Stick to .ts for now. dmf::VideoReader video_reader(filename); if (!video_reader.has_video && !video_reader.has_audio) { log("no video or audio stream found"); return; -- 2.52.0