diff --git a/CMakeLists.txt b/CMakeLists.txt index 61d8ed0..967131f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,18 +73,40 @@ 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 + libswresample +) + +# 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/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/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/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/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/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..0e98ef6 --- /dev/null +++ b/nodes/videoin/main.cpp @@ -0,0 +1,181 @@ +#include +#include +#include +#include "V210.hpp" +#include "FlowDef.hpp" +#include "NodeBase.hpp" +#include "VideoReader.hpp" + +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("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; + } + + 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; + 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); + } + + mxlFlowWriter audio_writer = nullptr; + 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); + + bool created = false; + 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}; + const mxlRational audio_rate = {sample_rate, 1}; + uint64_t audio_index = 0; + if (has_audio) { + audio_index = mxlGetCurrentIndex(&audio_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{}; + 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.read_next( + 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 (has_video && vst == MXL_STATUS_OK) { + grain.flags = 0; + grain.validSlices = grain.totalSlices; + mxlFlowWriterCommitGrain(video_writer, &grain); + } + 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 = mxlGetCurrentIndex(&video_rate); + } + } else if (frame_kind == dmf::VideoReader::FrameKind::Audio) { + 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 — 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) + + 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; + if (has_video) { + const uint64_t current = mxlGetCurrentIndex(&video_rate); + if (current > video_index) video_index = current; + } + } + } + + log("stopped at video_index=%llu", static_cast(video_index)); + if (has_video) mxlReleaseFlowWriter(instance(), video_writer); + if (has_audio) mxlReleaseFlowWriter(instance(), audio_writer); + } +}; + +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..d7814b2 --- /dev/null +++ b/shared/VideoReader.hpp @@ -0,0 +1,278 @@ +#pragma once + +extern "C" { + #include + #include + #include + #include + #include + #include + #include +} + +#include +#include +#include +#include "Signal.hpp" +#include "V210.hpp" + +namespace dmf { + +class VideoReader { +public: + struct VideoInfo { + 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; + }; + + enum class FrameKind { None, Video, Audio }; + + VideoInfo video_info{}; + AudioInfo audio_info{}; + bool has_audio = false; + bool has_video = false; + + explicit VideoReader(const std::string& filename) { + open_file(filename); + get_source_info(); + if (has_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); + av_freep(&p10_data[0]); + av_frame_free(&video_frame); + av_frame_free(&audio_frame); + av_packet_free(&packet); + } + + // 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 (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 + } + sws_scale( + sws_video_ctx, + video_frame->data, + video_frame->linesize, + 0, + video_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, + video_info.width, + video_info.height, + p10_linesizes[0], + p10_linesizes[1], + p10_linesizes[2], + mxl_stride + ); + av_frame_unref(video_frame); + return FrameKind::Video; + } + + 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 + } + + 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 + 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); + 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); + } + continue; + } + + 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 FrameKind::None; + } + +private: + int video_stream_index = -1; + int audio_stream_index = -1; + + 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; + + 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); + + if (avformat_find_stream_info(format_context, nullptr) < 0) { + avformat_close_input(&format_context); + throw std::runtime_error("Could not find stream info"); + } + + 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); + has_video = true; + } else if (type == AVMEDIA_TYPE_AUDIO && audio_stream_index == -1) { + audio_stream_index = static_cast(i); + has_audio = true; + } + } + + if (!has_video && !has_audio) { + avformat_close_input(&format_context); + throw std::runtime_error("No audio/video stream found in: " + filename); + } + } + + void get_source_info() { + 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"); + + 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 { + has_video = false; + } + + // 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_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_video_ctx) + throw std::runtime_error("Failed to create SwsContext"); + + if (av_image_alloc(p10_data, p10_linesizes, + 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/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; diff --git a/video-ndi.json b/video-ndi.json new file mode 100644 index 0000000..c2b8d0b --- /dev/null +++ b/video-ndi.json @@ -0,0 +1,18 @@ +{ + "nodes": [ + { "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": 24, "fps_den": 1 } + }, + { + "from": "videoin", "from_port": "audio_flow_id", + "to": "ndiout", "to_port": "audio_flow_id", + "format": { "kind": "audio", "sample_rate": 48000, "channels": 6, "bit_depth": 32 } + } + ] +}