# 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
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,
{ "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 },
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;
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.
} 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;
}
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");
} 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,
#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
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.
--- /dev/null
+#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;
+ }
+}
--- /dev/null
+#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)