]> git.sesse.net Git - nageru/commitdiff
Add support for V4L2 output.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 6 Apr 2020 19:12:41 +0000 (21:12 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 6 Apr 2020 19:12:41 +0000 (21:12 +0200)
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
nageru/flags.cpp
nageru/flags.h
nageru/quicksync_encoder.cpp
nageru/quicksync_encoder_impl.h
nageru/v4l_output.cpp [new file with mode: 0644]
nageru/v4l_output.h [new file with mode: 0644]

index 8e549855b020413e1dde39034b9310b68d5fcc05..79fb198a8fabcfcec122700c65d9712625262f6b 100644 (file)
@@ -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
 
index ed52dc8933fba1f36a1a05c75f8ef85fae7d0691..887cd9603278ad478cac9a2babf3201bbb0b01cb 100644 (file)
@@ -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;
index caad05a5088e4402081cbc14d2cad3414a5412c8..5758d884c01620765b1d0df770246d18ef1134f7 100644 (file)
@@ -41,6 +41,7 @@ struct Flags {
        int x264_vbv_max_bitrate = -1;  // In kilobits. 0 = no limit, -1 = same as <x264_bitrate> (CBR).
        int x264_vbv_buffer_size = -1;  // In kilobits. 0 = one-frame VBV, -1 = same as <x264_bitrate> (one-second VBV).
        std::vector<std::string> x264_extra_param;  // In “key[,value]” format.
+       std::string v4l_output_device;  // Empty if none.
        bool enable_alsa_output = true;
        std::map<int, int> default_stream_mapping;
        bool multichannel_mapping_mode = false;  // Implicitly true if input_mapping_filename is nonempty.
index 9dc603b5ede7a0c1c2f6bbefcf59db713bd92408..70bf8002073d64aba40f2aa6021c3e19ddf9a352 100644 (file)
@@ -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,
index 5e215e5b691bfa3f81ff317ce58404415c8d4c3d..0800c5956b758466234560fba3b24ec79fd37585 100644 (file)
@@ -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<AudioEncoder> file_audio_encoder;
 
        X264Encoder *x264_encoder;  // nullptr if not using x264.
+       std::unique_ptr<V4LOutput> v4l_output;  // nullptr if not using V4L2 output.
 
        Mux* stream_mux = nullptr;  // To HTTP.
        std::unique_ptr<Mux> file_mux;  // To local disk.
diff --git a/nageru/v4l_output.cpp b/nageru/v4l_output.cpp
new file mode 100644 (file)
index 0000000..f6c4e85
--- /dev/null
@@ -0,0 +1,77 @@
+#include "v4l_output.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <linux/videodev2.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#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 (file)
index 0000000..b7686f6
--- /dev/null
@@ -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 <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+
+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<uint8_t[]> yuv420_buf;
+       int video_fd;
+};
+
+#endif  // !defined(_V4L_OUTPUT_H)