From dcb238eb81768be4021f01dc7102c6c73821d0c0 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Mon, 6 Apr 2020 21:12:41 +0200 Subject: [PATCH] Add support for V4L2 output. This is video-only (no audio) and unsynchronized. It's intended mainly for a way to pipe Nageru output into a videoconferencing application, in these Covid-19 days. --- meson.build | 3 +- nageru/flags.cpp | 5 +++ nageru/flags.h | 1 + nageru/quicksync_encoder.cpp | 11 +++++ nageru/quicksync_encoder_impl.h | 2 + nageru/v4l_output.cpp | 77 +++++++++++++++++++++++++++++++++ nageru/v4l_output.h | 37 ++++++++++++++++ 7 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 nageru/v4l_output.cpp create mode 100644 nageru/v4l_output.h diff --git a/meson.build b/meson.build index 8e54985..79fb198 100644 --- a/meson.build +++ b/meson.build @@ -202,7 +202,8 @@ nageru_srcs += ['nageru/chroma_subsampler.cpp', 'nageru/v210_converter.cpp', 'na # Streaming and encoding objects (largely the set that is shared between Nageru and Kaeru). stream_srcs = ['nageru/quicksync_encoder.cpp', 'nageru/x264_encoder.cpp', 'nageru/x264_dynamic.cpp', 'nageru/x264_speed_control.cpp', 'nageru/video_encoder.cpp', 'nageru/audio_encoder.cpp', 'nageru/ffmpeg_util.cpp', 'nageru/ffmpeg_capture.cpp', - 'nageru/print_latency.cpp', 'nageru/basic_stats.cpp', 'nageru/ref_counted_frame.cpp'] + 'nageru/print_latency.cpp', 'nageru/basic_stats.cpp', 'nageru/ref_counted_frame.cpp', + 'nageru/v4l_output.cpp'] stream = static_library('stream', stream_srcs, dependencies: nageru_deps, include_directories: nageru_include_dirs) nageru_link_with += stream diff --git a/nageru/flags.cpp b/nageru/flags.cpp index ed52dc8..887cd96 100644 --- a/nageru/flags.cpp +++ b/nageru/flags.cpp @@ -19,6 +19,7 @@ enum LongOption { OPTION_MIDI_MAPPING, OPTION_DEFAULT_HDMI_INPUT, OPTION_FAKE_CARDS_AUDIO, + OPTION_V4L_OUTPUT, OPTION_HTTP_UNCOMPRESSED_VIDEO, OPTION_HTTP_X264_VIDEO, OPTION_RECORD_X264_VIDEO, @@ -237,6 +238,7 @@ void parse_flags(Program program, int argc, char * const argv[]) { "midi-mapping", required_argument, 0, OPTION_MIDI_MAPPING }, { "default-hdmi-input", no_argument, 0, OPTION_DEFAULT_HDMI_INPUT }, { "fake-cards-audio", no_argument, 0, OPTION_FAKE_CARDS_AUDIO }, + { "v4l-output", required_argument, 0, OPTION_V4L_OUTPUT }, { "http-uncompressed-video", no_argument, 0, OPTION_HTTP_UNCOMPRESSED_VIDEO }, { "http-x264-video", no_argument, 0, OPTION_HTTP_X264_VIDEO }, { "record-x264-video", no_argument, 0, OPTION_RECORD_X264_VIDEO }, @@ -352,6 +354,9 @@ void parse_flags(Program program, int argc, char * const argv[]) case OPTION_FAKE_CARDS_AUDIO: global_flags.fake_cards_audio = true; break; + case OPTION_V4L_OUTPUT: + global_flags.v4l_output_device = optarg; + break; case OPTION_HTTP_UNCOMPRESSED_VIDEO: global_flags.uncompressed_video_to_http = true; break; diff --git a/nageru/flags.h b/nageru/flags.h index caad05a..5758d88 100644 --- a/nageru/flags.h +++ b/nageru/flags.h @@ -41,6 +41,7 @@ struct Flags { int x264_vbv_max_bitrate = -1; // In kilobits. 0 = no limit, -1 = same as (CBR). int x264_vbv_buffer_size = -1; // In kilobits. 0 = one-frame VBV, -1 = same as (one-second VBV). std::vector x264_extra_param; // In “key[,value]” format. + std::string v4l_output_device; // Empty if none. bool enable_alsa_output = true; std::map default_stream_mapping; bool multichannel_mapping_mode = false; // Implicitly true if input_mapping_filename is nonempty. diff --git a/nageru/quicksync_encoder.cpp b/nageru/quicksync_encoder.cpp index 9dc603b..70bf800 100644 --- a/nageru/quicksync_encoder.cpp +++ b/nageru/quicksync_encoder.cpp @@ -719,6 +719,9 @@ void QuickSyncEncoderImpl::enable_zerocopy_if_possible() } else if (global_flags.x264_video_to_http) { fprintf(stderr, "Disabling zerocopy H.264 encoding due to --http-x264-video.\n"); use_zerocopy = false; + } else if (!global_flags.v4l_output_device.empty()) { + fprintf(stderr, "Disabling zerocopy H.264 encoding due to --v4l-output.\n"); + use_zerocopy = false; } else { use_zerocopy = true; } @@ -1547,6 +1550,10 @@ QuickSyncEncoderImpl::QuickSyncEncoderImpl(const std::string &filename, Resource memset(&slice_param, 0, sizeof(slice_param)); } + if (!global_flags.v4l_output_device.empty()) { + v4l_output.reset(new V4LOutput(global_flags.v4l_output_device.c_str(), width, height)); + } + call_once(quick_sync_metrics_inited, [](){ mixer_latency_histogram.init("mixer"); qs_latency_histogram.init("quick_sync"); @@ -1990,6 +1997,10 @@ void QuickSyncEncoderImpl::pass_frame(QuickSyncEncoderImpl::PendingFrame frame, } else if (global_flags.x264_video_to_http || global_flags.x264_video_to_disk) { x264_encoder->add_frame(pts, duration, frame.ycbcr_coefficients, data, received_ts); } + + if (v4l_output != nullptr) { + v4l_output->send_frame(data); + } } void QuickSyncEncoderImpl::encode_frame(QuickSyncEncoderImpl::PendingFrame frame, int encoding_frame_num, int display_frame_num, int gop_start_display_frame_num, diff --git a/nageru/quicksync_encoder_impl.h b/nageru/quicksync_encoder_impl.h index 5e215e5..0800c59 100644 --- a/nageru/quicksync_encoder_impl.h +++ b/nageru/quicksync_encoder_impl.h @@ -21,6 +21,7 @@ #include "print_latency.h" #include "shared/ref_counted_gl_sync.h" #include "va_display_with_cleanup.h" +#include "v4l_output.h" #define SURFACE_NUM 16 /* 16 surfaces for source YUV */ #define MAX_NUM_REF1 16 // Seemingly a hardware-fixed value, not related to SURFACE_NUM @@ -171,6 +172,7 @@ private: std::unique_ptr file_audio_encoder; X264Encoder *x264_encoder; // nullptr if not using x264. + std::unique_ptr v4l_output; // nullptr if not using V4L2 output. Mux* stream_mux = nullptr; // To HTTP. std::unique_ptr file_mux; // To local disk. diff --git a/nageru/v4l_output.cpp b/nageru/v4l_output.cpp new file mode 100644 index 0000000..f6c4e85 --- /dev/null +++ b/nageru/v4l_output.cpp @@ -0,0 +1,77 @@ +#include "v4l_output.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shared/memcpy_interleaved.h" + +V4LOutput::V4LOutput(const char *device_path, unsigned width, unsigned height) + : width(width), height(height), + image_size_bytes(width * height + (width / 2) * (height / 2) * 2), + yuv420_buf(new uint8_t[image_size_bytes]) +{ + video_fd = open(device_path, O_WRONLY); + if (video_fd == -1) { + perror(device_path); + exit(1); + } + + v4l2_format fmt; + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; + fmt.fmt.pix.width = width; + fmt.fmt.pix.height = width; + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420; + fmt.fmt.pix.field = V4L2_FIELD_NONE; + fmt.fmt.pix.bytesperline = 0; + fmt.fmt.pix.sizeimage = image_size_bytes; + fmt.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; + int err = ioctl(video_fd, VIDIOC_S_FMT, &fmt); + if (err == -1) { + perror("ioctl(VIDIOC_S_FMT)"); + exit(1); + } +} + +V4LOutput::~V4LOutput() +{ + close(video_fd); +} + +void V4LOutput::send_frame(const uint8_t *data) +{ + // Seemingly NV12 isn't a very common format among V4L2 consumers, + // so we convert from our usual NV12 to YUV420. We get an unneeded + // memcpy() of the luma data, but hopefully, we'll manage. + const size_t luma_size = width * height; + const size_t chroma_size = (width / 2) * (height / 2); + memcpy(yuv420_buf.get(), data, luma_size); + memcpy_interleaved( + yuv420_buf.get() + luma_size, + yuv420_buf.get() + luma_size + chroma_size, + data + luma_size, 2 * chroma_size); + + const uint8_t *ptr = yuv420_buf.get(); + size_t bytes_left = image_size_bytes; + while (bytes_left > 0) { + int err = write(video_fd, ptr, bytes_left); + if (err == -1) { + perror("V4L write"); + exit(1); + } + if (err == 0) { + fprintf(stderr, "WARNING: Short V4L write() (only wrote %zu of %zu bytes), skipping rest of frame.\n", + image_size_bytes - bytes_left, image_size_bytes); + return; + } + assert(err > 0); + bytes_left -= err; + ptr += err; + } +} diff --git a/nageru/v4l_output.h b/nageru/v4l_output.h new file mode 100644 index 0000000..b7686f6 --- /dev/null +++ b/nageru/v4l_output.h @@ -0,0 +1,37 @@ +#ifndef _V4L_OUTPUT_H +#define _V4L_OUTPUT_H 1 + +// Video-only V4L2 output. The intended use-case is output into +// v4l2loopback to get into videoconferencing or the likes: +// +// sudo apt install v4l2loopback-dkms +// sudo modprobe v4l2loopback video_nr=2 card_label='Nageru loopback' exclusive_caps=1 +// nageru --v4l2-output /dev/video2 +// +// Start Nageru before any readers. +// +// Unlike DecklinkOutput, this output does not own the master clock; +// it is entirely unsynchronized, and runs off of the normal master clock. +// It comes in addition to any other output, and is not GUI-controlled. + +#include +#include + +#include + +class V4LOutput { +public: + V4LOutput(const char *device_path, unsigned width, unsigned height); + ~V4LOutput(); + + // Expects NV12 data. + void send_frame(const uint8_t *data); + +private: + const unsigned width, height; + const size_t image_size_bytes; + std::unique_ptr yuv420_buf; + int video_fd; +}; + +#endif // !defined(_V4L_OUTPUT_H) -- 2.39.2