From 32a8fa9837d46a944350e2f83705cee9ea542d7e Mon Sep 17 00:00:00 2001 From: itten Date: Fri, 3 Jul 2026 18:48:11 +0300 Subject: [PATCH] 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 } + } + ] +}