From 8bb8bb7cc9700befab35a8cc2c4b7a88f0638af9 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Thu, 21 Jul 2022 16:45:02 +0200 Subject: [PATCH] Support AV1 streaming over HTTP, via SVT-AV1. This is optional, and pretty rough currently. But it might be interesting for certain use cases, in particular around 1080p or 4K streaming. --- README | 7 + meson.build | 12 +- nageru/av1_encoder.cpp | 375 ++++++++++++++++++++++++++++++++ nageru/av1_encoder.h | 120 ++++++++++ nageru/defs.h | 6 + nageru/flags.cpp | 66 +++++- nageru/flags.h | 6 + nageru/quicksync_encoder.cpp | 13 +- nageru/quicksync_encoder_impl.h | 2 +- nageru/video_encoder.cpp | 24 +- nageru/video_encoder.h | 4 + shared/mux.cpp | 2 + shared/mux.h | 1 + 13 files changed, 624 insertions(+), 14 deletions(-) create mode 100644 nageru/av1_encoder.cpp create mode 100644 nageru/av1_encoder.h diff --git a/README b/README index a10fe94..a565b39 100644 --- a/README +++ b/README @@ -98,6 +98,10 @@ Nageru currently needs: If you build with libsrt, make sure it is not linked to OpenSSL, for license reasons. + - Optional: SVT-AV1, for encoding high-quality video suitable for streaming to + end users (higher quality than using x264, but not nearly as mature). + You will need at least version 1.0.0. + Futatabi also needs: @@ -125,6 +129,9 @@ Exceptions as of July 2022: meson obj -Dcef_dir=/usr/lib/x86_64-linux-gnu/cef -Dcef_build_type=system -Dcef_no_icudtl=true + - Debian's SVT-AV1 is too old, so you will need to compile it yourself + if you wish to use it for streaming. + The patches/ directory contains a patch that helps zita-resampler performance. It is meant for upstream, but was not in at the time Nageru was released. diff --git a/meson.build b/meson.build index 4f96702..a7ea6ae 100644 --- a/meson.build +++ b/meson.build @@ -27,6 +27,7 @@ qt5deps = dependency('qt5', modules: ['Core', 'Gui', 'Widgets', 'OpenGLExtension sdl2_imagedep = dependency('SDL2_image', required: false) sdl2dep = dependency('sdl2', required: false) srtdep = dependency('srt', required: false) +svtav1dep = dependency('SvtAv1Enc', required: false, version: '>=1.0.0') sqlite3dep = dependency('sqlite3') threaddep = dependency('threads') vadrmdep = dependency('libva-drm') @@ -63,6 +64,9 @@ if srtdep.found() # or gnutls libsrt, so we cannot check license compatibility here. add_project_arguments('-DHAVE_SRT=1', language: 'cpp') endif +if svtav1dep.found() + add_project_arguments('-DHAVE_AV1=1', language: 'cpp') +endif top_include = include_directories('.') @@ -73,7 +77,7 @@ subdir('shared') nageru_srcs = [] nageru_deps = [shareddep, qt5deps, libjpegdep, movitdep, protobufdep, vax11dep, vadrmdep, x11dep, libavformatdep, libswresampledep, libavcodecdep, libavutildep, - libswscaledep, libusbdep, luajitdep, dldep, x264dep, alsadep, zitaresamplerdep, + libswscaledep, libusbdep, luajitdep, dldep, x264dep, svtav1dep, alsadep, zitaresamplerdep, qcustomplotdep, threaddep, eigendep, srtdep, libdrmdep] nageru_include_dirs = [include_directories('nageru')] nageru_link_with = [] @@ -212,10 +216,14 @@ nageru_srcs += ['nageru/chroma_subsampler.cpp', 'nageru/v210_converter.cpp', 'na 'nageru/timecode_renderer.cpp', 'nageru/tweaked_inputs.cpp', 'nageru/mjpeg_encoder.cpp'] # 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', +stream_srcs = ['nageru/quicksync_encoder.cpp', 'nageru/video_encoder.cpp', + 'nageru/x264_encoder.cpp', 'nageru/x264_dynamic.cpp', 'nageru/x264_speed_control.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/v4l_output.cpp'] +if svtav1dep.found() + stream_srcs += 'nageru/av1_encoder.cpp' +endif stream = static_library('stream', stream_srcs, dependencies: nageru_deps, include_directories: nageru_include_dirs) nageru_link_with += stream diff --git a/nageru/av1_encoder.cpp b/nageru/av1_encoder.cpp new file mode 100644 index 0000000..a0dbdd1 --- /dev/null +++ b/nageru/av1_encoder.cpp @@ -0,0 +1,375 @@ +#include "av1_encoder.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "defs.h" +#include "flags.h" +#include "shared/metrics.h" +#include "shared/mux.h" +#include "print_latency.h" +#include "shared/timebase.h" +#include "shared/memcpy_interleaved.h" + +extern "C" { +#include +#include +} + +using namespace movit; +using namespace std; +using namespace std::chrono; +using namespace std::placeholders; + +namespace { + +// AV1Encoder can be restarted if --record-av1-video is set, so make these +// metrics global. +atomic metric_av1_queued_frames{0}; +atomic metric_av1_max_queued_frames{AV1_QUEUE_LENGTH}; +atomic metric_av1_dropped_frames{0}; +atomic metric_av1_output_frames_i{0}; +atomic metric_av1_output_frames_p{0}; +Histogram metric_av1_qp; +LatencyHistogram av1_latency_histogram; + +once_flag av1_metrics_inited; + +} // namespace + +AV1Encoder::AV1Encoder(const AVOutputFormat *oformat) + : wants_global_headers(oformat->flags & AVFMT_GLOBALHEADER) +{ + call_once(av1_metrics_inited, []{ + global_metrics.add("av1_queued_frames", {}, &metric_av1_queued_frames, Metrics::TYPE_GAUGE); + global_metrics.add("av1_max_queued_frames", {}, &metric_av1_max_queued_frames, Metrics::TYPE_GAUGE); + global_metrics.add("av1_dropped_frames", {}, &metric_av1_dropped_frames); + global_metrics.add("av1_output_frames", {{ "type", "i" }}, &metric_av1_output_frames_i); + global_metrics.add("av1_output_frames", {{ "type", "p" }}, &metric_av1_output_frames_p); + + metric_av1_qp.init_uniform(50); + global_metrics.add("av1_qp", {}, &metric_av1_qp); + av1_latency_histogram.init("av1"); + }); + + const size_t bytes_per_pixel = 1; // TODO: 10-bit support. + frame_pool.reset(new uint8_t[global_flags.width * global_flags.height * 2 * bytes_per_pixel * AV1_QUEUE_LENGTH]); + for (unsigned i = 0; i < AV1_QUEUE_LENGTH; ++i) { + free_frames.push(frame_pool.get() + i * (global_flags.width * global_flags.height * 2 * bytes_per_pixel)); + } + encoder_thread = thread(&AV1Encoder::encoder_thread_func, this); +} + +AV1Encoder::~AV1Encoder() +{ + should_quit = true; + queued_frames_nonempty.notify_all(); + encoder_thread.join(); +} + +void AV1Encoder::add_frame(int64_t pts, int64_t duration, YCbCrLumaCoefficients ycbcr_coefficients, const uint8_t *data, const ReceivedTimestamps &received_ts) +{ + assert(!should_quit); + + QueuedFrame qf; + qf.pts = pts; + qf.duration = duration; + qf.ycbcr_coefficients = ycbcr_coefficients; + qf.received_ts = received_ts; + + { + lock_guard lock(mu); + if (free_frames.empty()) { + fprintf(stderr, "WARNING: AV1 queue full, dropping frame with pts %" PRId64 "\n", pts); + ++metric_av1_dropped_frames; + return; + } + + qf.data = free_frames.front(); + free_frames.pop(); + } + + // Since we're copying anyway, we can unpack from NV12 to fully planar on the fly. + // SVT-AV1 makes its own copy, though, and it would have been nice to avoid the + // double-copy. + size_t bytes_per_pixel = 1; // TODO: 10-bit support. + size_t frame_size = global_flags.width * global_flags.height * bytes_per_pixel; + assert(global_flags.width % 2 == 0); + assert(global_flags.height % 2 == 0); + uint8_t *y = qf.data; + uint8_t *cb = y + frame_size; + uint8_t *cr = cb + frame_size / 4; + memcpy(y, data, frame_size); + memcpy_interleaved(cb, cr, data + frame_size, frame_size / 2); + + { + lock_guard lock(mu); + queued_frames.push(qf); + queued_frames_nonempty.notify_all(); + metric_av1_queued_frames = queued_frames.size(); + } +} + +void AV1Encoder::init_av1() +{ + EbSvtAv1EncConfiguration config; + EbErrorType ret = svt_av1_enc_init_handle(&encoder, nullptr, &config); + if (ret != EB_ErrorNone) { + fprintf(stderr, "Error initializing SVT-AV1 handle (error %08x)\n", ret); + exit(EXIT_FAILURE); + } + + config.enc_mode = global_flags.av1_preset; + config.intra_period_length = 63; // Approx. one second, conforms to the (n % 8) - 1 == 0 rule. + config.source_width = global_flags.width; + config.source_height = global_flags.height; + config.frame_rate_numerator = global_flags.av1_fps_num; + config.frame_rate_denominator = global_flags.av1_fps_den; + config.encoder_bit_depth = 8; // TODO: 10-bit support. + config.rate_control_mode = 2; // CBR. + config.pred_structure = 1; // PRED_LOW_DELAY_B (needed for CBR). + config.target_bit_rate = global_flags.av1_bitrate * 1000; + + // NOTE: These should be in sync with the ones in quicksync_encoder.cpp (sps_rbsp()). + config.color_primaries = EB_CICP_CP_BT_709; + config.transfer_characteristics = EB_CICP_TC_SRGB; + if (global_flags.ycbcr_rec709_coefficients) { + config.matrix_coefficients = EB_CICP_MC_BT_709; + } else { + config.matrix_coefficients = EB_CICP_MC_BT_601; + } + config.color_range = EB_CR_STUDIO_RANGE; +#if SVT_AV1_CHECK_VERSION(1, 0, 0) + config.chroma_sample_position = EB_CSP_VERTICAL; +#endif + + const vector &extra_param = global_flags.av1_extra_param; + for (const string &str : extra_param) { + const size_t pos = str.find(','); + if (pos == string::npos) { + if (svt_av1_enc_parse_parameter(&config, str.c_str(), nullptr) != EB_ErrorNone) { + fprintf(stderr, "ERROR: SVT-AV1 rejected parameter '%s' with no value\n", str.c_str()); + exit(EXIT_FAILURE); + } + } else { + const string key = str.substr(0, pos); + const string value = str.substr(pos + 1); + if (svt_av1_enc_parse_parameter(&config, key.c_str(), value.c_str()) != EB_ErrorNone) { + fprintf(stderr, "ERROR: SVT-AV1 rejected parameter '%s' set to '%s'\n", + key.c_str(), value.c_str()); + exit(EXIT_FAILURE); + } + } + } + + ret = svt_av1_enc_set_parameter(encoder, &config); + if (ret != EB_ErrorNone) { + fprintf(stderr, "Error configuring SVT-AV1 (error %08x)\n", ret); + exit(EXIT_FAILURE); + } + + ret = svt_av1_enc_init(encoder); + if (ret != EB_ErrorNone) { + fprintf(stderr, "Error initializing SVT-AV1 (error %08x)\n", ret); + exit(EXIT_FAILURE); + } + + if (wants_global_headers) { + EbBufferHeaderType *header = NULL; + + ret = svt_av1_enc_stream_header(encoder, &header); + if (ret != EB_ErrorNone) { + fprintf(stderr, "Error building SVT-AV1 header (error %08x)\n", ret); + exit(EXIT_FAILURE); + } + + global_headers = string(reinterpret_cast(header->p_buffer), header->n_filled_len); + + svt_av1_enc_stream_header_release(header); // Don't care about errors. + } +} + +void AV1Encoder::encoder_thread_func() +{ + if (nice(5) == -1) { + perror("nice()"); + // No exit; it's not fatal. + } + pthread_setname_np(pthread_self(), "AV1_encode"); + init_av1(); + av1_init_done = true; + + bool frames_left; + + do { + QueuedFrame qf; + + // Wait for a queued frame, then dequeue it. + { + unique_lock lock(mu); + queued_frames_nonempty.wait(lock, [this]() { return !queued_frames.empty() || should_quit; }); + if (!queued_frames.empty()) { + qf = queued_frames.front(); + queued_frames.pop(); + } else { + qf.pts = -1; + qf.duration = -1; + qf.data = nullptr; + } + + metric_av1_queued_frames = queued_frames.size(); + frames_left = !queued_frames.empty(); + } + + encode_frame(qf); + + { + lock_guard lock(mu); + free_frames.push(qf.data); + } + + // We should quit only if the should_quit flag is set _and_ we have nothing + // in our queue. + } while (!should_quit || frames_left); + + // Signal end of stream. + EbBufferHeaderType hdr; + hdr.n_alloc_len = 0; + hdr.n_filled_len = 0; + hdr.n_tick_count = 0; + hdr.p_app_private = nullptr; + hdr.pic_type = EB_AV1_INVALID_PICTURE; + hdr.p_buffer = nullptr; + hdr.metadata = nullptr; + hdr.flags = EB_BUFFERFLAG_EOS; + svt_av1_enc_send_picture(encoder, &hdr); + + bool seen_eof = false; + do { + EbBufferHeaderType *buf; + EbErrorType ret = svt_av1_enc_get_packet(encoder, &buf, /*pic_send_done=*/true); + if (ret == EB_NoErrorEmptyQueue) { + assert(false); + } + seen_eof = (buf->flags & EB_BUFFERFLAG_EOS); + process_packet(buf); + } while (!seen_eof); + + svt_av1_enc_deinit(encoder); + svt_av1_enc_deinit_handle(encoder); +} + +void AV1Encoder::encode_frame(AV1Encoder::QueuedFrame qf) +{ + if (qf.data) { + EbSvtIOFormat pic; + pic.luma = qf.data; + pic.cb = pic.luma + global_flags.width * global_flags.height; + pic.cr = pic.cb + global_flags.width * global_flags.height / 4; + pic.y_stride = global_flags.width; + pic.cb_stride = global_flags.width / 2; + pic.cr_stride = global_flags.width / 2; + pic.width = global_flags.width; + pic.height = global_flags.height; + pic.origin_x = 0; + pic.origin_y = 0; + pic.color_fmt = EB_YUV420; + pic.bit_depth = EB_EIGHT_BIT; // TODO: 10-bit. + + EbBufferHeaderType hdr; + hdr.p_buffer = reinterpret_cast(&pic); + hdr.n_alloc_len = global_flags.width * global_flags.height * 3 / 2; // TODO: 10-bit. + hdr.n_filled_len = hdr.n_alloc_len; + hdr.n_tick_count = 0; + hdr.p_app_private = reinterpret_cast(intptr_t(qf.duration)); + hdr.pic_type = EB_AV1_INVALID_PICTURE; // Actually means auto, according to FFmpeg. + hdr.metadata = nullptr; + hdr.flags = 0; + hdr.pts = av_rescale_q(qf.pts, AVRational{ 1, TIMEBASE }, AVRational{ global_flags.av1_fps_den, global_flags.av1_fps_num }); + if (hdr.pts <= last_pts) { + fprintf(stderr, "WARNING: Receiving frames faster than given --av1-fps value (%d/%d); dropping frame.\n", + global_flags.av1_fps_num, global_flags.av1_fps_den); + } else { + svt_av1_enc_send_picture(encoder, &hdr); + frames_being_encoded[hdr.pts] = qf.received_ts; + last_pts = hdr.pts; + } + } + + for ( ;; ) { + EbBufferHeaderType *buf; + EbErrorType ret = svt_av1_enc_get_packet(encoder, &buf, /*pic_send_done=*/false); + if (ret == EB_NoErrorEmptyQueue) { + return; + } + process_packet(buf); + } +} + +void AV1Encoder::process_packet(EbBufferHeaderType *buf) +{ + if (buf->n_filled_len == 0) { + // TODO: Can this ever happen? + svt_av1_enc_release_out_buffer(&buf); + return; + } + + switch (buf->pic_type) { + case EB_AV1_KEY_PICTURE: + case EB_AV1_INTRA_ONLY_PICTURE: + ++metric_av1_output_frames_i; + break; + case EB_AV1_INTER_PICTURE: // We don't really know whether it's P or B. + ++metric_av1_output_frames_p; + break; + default: + break; + } + metric_av1_qp.count_event(buf->qp); + + if (frames_being_encoded.count(buf->pts)) { + ReceivedTimestamps received_ts = frames_being_encoded[buf->pts]; + frames_being_encoded.erase(buf->pts); + + static int frameno = 0; + print_latency("Current AV1 latency (video inputs → network mux):", + received_ts, /*b_frame=*/false, &frameno, &av1_latency_histogram); + } else { + assert(false); + } + + AVPacket pkt; + memset(&pkt, 0, sizeof(pkt)); + pkt.buf = nullptr; + pkt.data = buf->p_buffer; + pkt.size = buf->n_filled_len; + pkt.stream_index = 0; + if (buf->pic_type == EB_AV1_KEY_PICTURE) { + pkt.flags = AV_PKT_FLAG_KEY; + } else if (buf->pic_type == EB_AV1_NON_REF_PICTURE) { + // I have no idea if this does anything in practice, + // but the libavcodec plugin does it. + pkt.flags = AV_PKT_FLAG_DISPOSABLE; + } else { + pkt.flags = 0; + } + pkt.pts = av_rescale_q(buf->pts, AVRational{ global_flags.av1_fps_den, global_flags.av1_fps_num }, AVRational{ 1, TIMEBASE }); + pkt.dts = av_rescale_q(buf->dts, AVRational{ global_flags.av1_fps_den, global_flags.av1_fps_num }, AVRational{ 1, TIMEBASE }); + + for (Mux *mux : muxes) { + mux->add_packet(pkt, pkt.pts, pkt.dts); + } + + svt_av1_enc_release_out_buffer(&buf); +} diff --git a/nageru/av1_encoder.h b/nageru/av1_encoder.h new file mode 100644 index 0000000..3431595 --- /dev/null +++ b/nageru/av1_encoder.h @@ -0,0 +1,120 @@ +// An AV1 encoder using the SVT-AV1 encoder library. (libaom does not seem +// to be suitable for real-time streaming as of 2022, as it is not using +// threads particularly efficiently.) AV1 is a newer format than H.264, +// obviously both for better and for worse: Higher coding efficiency +// (with sufficient amount of cores, SVT-AV1 is even better than x264 +// at higher frame rates), but generally smaller ecosystem, no speed +// control, less sophisticated rate control, etc.. +// +// We don't support storing AV1 to disk currently, only to HTTP. +// We also don't support hardware AV1 encoding, as hardware supporting it +// is very rare currently. +// +// Since SVT-AV1 does not support VFR, you need to declare the frame rate +// up-front, using the --av1-framerate flag. If the given frame rate is +// too high (ie., you are producing frames too slowly), rate control will get +// confused and use too little bitrate. If it is too low, Nageru will need to +// drop frames before input to the encoder. + +#ifndef _AV1_ENCODER_H +#define _AV1_ENCODER_H 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +#include + +#include "defs.h" +#include "shared/metrics.h" +#include "print_latency.h" +#include "video_codec_interface.h" + +class Mux; +struct EbBufferHeaderType; +struct EbComponentType; + +class AV1Encoder : public VideoCodecInterface { +public: + AV1Encoder(const AVOutputFormat *oformat); // Does not take ownership. + + // Called after the last frame. Will block; once this returns, + // the last data is flushed. + ~AV1Encoder() override; + + // Must be called before first frame. Does not take ownership. + void add_mux(Mux *mux) override { muxes.push_back(mux); } + + // is taken to be raw NV12 data of WIDTHxHEIGHT resolution. + // Does not block. + void add_frame(int64_t pts, int64_t duration, movit::YCbCrLumaCoefficients ycbcr_coefficients, const uint8_t *data, const ReceivedTimestamps &received_ts) override; + + std::string get_global_headers() const override { + while (!av1_init_done) { + sched_yield(); + } + return global_headers; + } + +private: + struct QueuedFrame { + int64_t pts, duration; + movit::YCbCrLumaCoefficients ycbcr_coefficients; + uint8_t *data; + ReceivedTimestamps received_ts; + }; + void encoder_thread_func(); + void init_av1(); + void encode_frame(QueuedFrame qf); + void process_packet(EbBufferHeaderType *buf); // Takes ownership. + + // One big memory chunk of all 50 (or whatever) frames, allocated in + // the constructor. All data functions just use pointers into this + // pool. + std::unique_ptr frame_pool; + + std::vector muxes; + const bool wants_global_headers; + + std::string global_headers; + + std::thread encoder_thread; + std::atomic av1_init_done{false}; + std::atomic should_quit{false}; + EbComponentType *encoder; + + int64_t last_pts = -1; + + // Protects everything below it. + std::mutex mu; + + // Frames that are not being encoded or waiting to be encoded, + // so that add_frame() can use new ones. + // TODO: Do we actually need this queue, or is SVT-AV1's queue + // non-blocking so that we can drop it? + std::queue free_frames; + + // Frames that are waiting to be encoded (ie., add_frame() has been + // called, but they are not picked up for encoding yet). + std::queue queued_frames; + + // Whenever the state of changes. + std::condition_variable queued_frames_nonempty; + + // Key is the pts of the frame. + std::unordered_map frames_being_encoded; +}; + +#endif // !defined(_AV1_ENCODER_H) diff --git a/nageru/defs.h b/nageru/defs.h index eb664d0..41ee96a 100644 --- a/nageru/defs.h +++ b/nageru/defs.h @@ -15,6 +15,7 @@ #define AUDIO_OUTPUT_CODEC_NAME "pcm_s32le" #define DEFAULT_AUDIO_OUTPUT_BIT_RATE 0 #define DEFAULT_X264_OUTPUT_BIT_RATE 4500 // 5 Mbit after making room for some audio and TCP overhead. +#define DEFAULT_AV1_OUTPUT_BIT_RATE 4500 // Same. #define LOCAL_DUMP_PREFIX "record-" #define LOCAL_DUMP_SUFFIX ".nut" @@ -27,8 +28,13 @@ // In number of frames. Comes in addition to any internal queues in x264 // (frame threading, lookahead, etc.). #define X264_QUEUE_LENGTH 50 +#define AV1_QUEUE_LENGTH 50 #define X264_DEFAULT_PRESET "ultrafast" #define X264_DEFAULT_TUNE "film" +#define DEFAULT_AV1_PRESET 12 +#define DEFAULT_AV1_FPS_NUM 60000 +#define DEFAULT_AV1_FPS_DEN 1001 + #endif // !defined(_DEFS_H) diff --git a/nageru/flags.cpp b/nageru/flags.cpp index 42fdb88..2bd0eec 100644 --- a/nageru/flags.cpp +++ b/nageru/flags.cpp @@ -23,6 +23,7 @@ enum LongOption { OPTION_V4L_OUTPUT, OPTION_HTTP_UNCOMPRESSED_VIDEO, OPTION_HTTP_X264_VIDEO, + OPTION_HTTP_AV1_VIDEO, OPTION_RECORD_X264_VIDEO, OPTION_SEPARATE_X264_DISK_ENCODE, OPTION_X264_PRESET, @@ -39,6 +40,10 @@ enum LongOption { OPTION_X264_SEPARATE_DISK_BITRATE, OPTION_X264_SEPARATE_DISK_CRF, OPTION_X264_SEPARATE_DISK_PARAM, + OPTION_AV1_PRESET, + OPTION_AV1_BITRATE, + OPTION_AV1_FPS, + OPTION_AV1_PARAM, OPTION_HTTP_MUX, OPTION_HTTP_COARSE_TIMEBASE, OPTION_HTTP_AUDIO_CODEC, @@ -185,6 +190,16 @@ void usage(Program program) fprintf(stderr, " incompatible with --x264-separate-disk-bitrate\n"); fprintf(stderr, " --x264-separate-disk-param=NAME[,VALUE] set any x264 parameter, for fine tuning\n"); } +#ifdef HAVE_AV1 + fprintf(stderr, " --http-av1-video send AV1-compressed video to HTTP clients\n"); + fprintf(stderr, " --av1-preset SVT-AV1 quality preset (default %d, from -2 to 13)\n", + DEFAULT_AV1_PRESET); + fprintf(stderr, " --av1-bitrate AV1 bitrate (in kilobit/sec, default %d)\n", + DEFAULT_AV1_OUTPUT_BIT_RATE); + fprintf(stderr, " --av1-fps=NUM[/DEN] AV1 encoded frame rate (default %d/%d)\n", + DEFAULT_AV1_FPS_NUM, DEFAULT_AV1_FPS_DEN); + fprintf(stderr, " --av1-param=NAME[,VALUE] set any SVT-AV1 parameter, for fine tuning\n"); +#endif fprintf(stderr, " --http-mux=NAME mux to use for HTTP streams (default " DEFAULT_STREAM_MUX_NAME ")\n"); fprintf(stderr, " --http-audio-codec=NAME audio codec to use for HTTP streams\n"); fprintf(stderr, " (default is to use the same as for the recording)\n"); @@ -289,6 +304,13 @@ void parse_flags(Program program, int argc, char * const argv[]) { "x264-separate-disk-bitrate", required_argument, 0, OPTION_X264_SEPARATE_DISK_BITRATE }, { "x264-separate-disk-crf", required_argument, 0, OPTION_X264_SEPARATE_DISK_CRF }, { "x264-separate-disk-param", required_argument, 0, OPTION_X264_SEPARATE_DISK_PARAM }, +#ifdef HAVE_AV1 + { "http-av1-video", no_argument, 0, OPTION_HTTP_AV1_VIDEO }, + { "av1-preset", required_argument, 0, OPTION_AV1_PRESET }, + { "av1-bitrate", required_argument, 0, OPTION_AV1_BITRATE }, + { "av1-fps", required_argument, 0, OPTION_AV1_FPS }, + { "av1-param", required_argument, 0, OPTION_AV1_PARAM }, +#endif { "http-mux", required_argument, 0, OPTION_HTTP_MUX }, { "http-audio-codec", required_argument, 0, OPTION_HTTP_AUDIO_CODEC }, { "http-audio-bitrate", required_argument, 0, OPTION_HTTP_AUDIO_BITRATE }, @@ -445,6 +467,9 @@ void parse_flags(Program program, int argc, char * const argv[]) global_flags.x264_video_to_http = true; global_flags.x264_separate_disk_encode = true; break; + case OPTION_HTTP_AV1_VIDEO: + global_flags.av1_video_to_http = true; + break; case OPTION_X264_PRESET: global_flags.x264_preset = optarg; break; @@ -487,6 +512,29 @@ void parse_flags(Program program, int argc, char * const argv[]) case OPTION_X264_SEPARATE_DISK_PARAM: global_flags.x264_separate_disk_extra_param.push_back(optarg); break; + case OPTION_AV1_PRESET: + global_flags.av1_preset = atoi(optarg); + break; + case OPTION_AV1_BITRATE: + global_flags.av1_bitrate = atoi(optarg); + break; + case OPTION_AV1_FPS: { + string str = optarg; + const size_t pos = str.find('/'); + if (pos == string::npos) { + global_flags.av1_fps_num = stoi(str); + global_flags.av1_fps_den = 1; + } else { + const string num = str.substr(0, pos); + const string den = str.substr(pos + 1); + global_flags.av1_fps_num = stoi(num); + global_flags.av1_fps_den = stoi(den); + } + break; + } + case OPTION_AV1_PARAM: + global_flags.av1_extra_param.push_back(optarg); + break; case OPTION_FLAT_AUDIO: // If --flat-audio is given, turn off everything that messes with the sound, // except the final makeup gain. @@ -570,8 +618,6 @@ void parse_flags(Program program, int argc, char * const argv[]) break; case OPTION_10_BIT_OUTPUT: global_flags.ten_bit_output = true; - global_flags.x264_video_to_disk = true; - global_flags.x264_video_to_http = true; global_flags.x264_bit_depth = 10; break; case OPTION_INPUT_YCBCR_INTERPRETATION: { @@ -645,11 +691,21 @@ void parse_flags(Program program, int argc, char * const argv[]) } } - if (global_flags.uncompressed_video_to_http && - global_flags.x264_video_to_http) { - fprintf(stderr, "ERROR: --http-uncompressed-video and --http-x264-video are mutually incompatible\n"); + if (global_flags.uncompressed_video_to_http + + global_flags.x264_video_to_http + + global_flags.av1_video_to_http > 1) { + fprintf(stderr, "ERROR: --http-{uncompressed,x264,av1}-video are mutually incompatible\n"); exit(1); } + if (global_flags.ten_bit_output) { + global_flags.x264_video_to_disk = true; // No 10-bit Quick Sync support. + if (global_flags.av1_video_to_http) { + fprintf(stderr, "ERROR: 10-bit AV1 output is not supported yet\n"); + exit(1); + } else { + global_flags.x264_video_to_http = true; + } + } if (global_flags.min_num_cards <= 0) { fprintf(stderr, "ERROR: --num-cards must be at least 1\n"); exit(1); diff --git a/nageru/flags.h b/nageru/flags.h index 734e290..96a4160 100644 --- a/nageru/flags.h +++ b/nageru/flags.h @@ -20,6 +20,7 @@ struct Flags { bool x264_video_to_http = false; bool x264_video_to_disk = false; // Disables Quick Sync entirely. Implies x264_video_to_http == true. bool x264_separate_disk_encode = false; // Disables Quick Sync entirely. Implies x264_video_to_disk == true. + bool av1_video_to_http = false; std::vector theme_dirs { ".", PREFIX "/share/nageru" }; std::string recording_dir = "."; std::string theme_filename = "theme.lua"; @@ -44,6 +45,11 @@ struct Flags { 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. + int av1_preset = DEFAULT_AV1_PRESET; + int av1_bitrate = DEFAULT_AV1_OUTPUT_BIT_RATE; + int av1_fps_num = DEFAULT_AV1_FPS_NUM, av1_fps_den = DEFAULT_AV1_FPS_DEN; + std::vector av1_extra_param; // In “key[,value]” format. + std::string x264_separate_disk_preset; // Empty will be overridden by X264_DEFAULT_PRESET, unless speedcontrol is set. std::string x264_separate_disk_tune = X264_DEFAULT_TUNE; int x264_separate_disk_bitrate = -1; diff --git a/nageru/quicksync_encoder.cpp b/nageru/quicksync_encoder.cpp index 19565ad..84bfaac 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.av1_video_to_http) { + fprintf(stderr, "Disabling zerocopy H.264 encoding due to --http-av1-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; @@ -1342,7 +1345,8 @@ void QuickSyncEncoderImpl::save_codeddata(GLSurface *surf, storage_task task) file_mux->add_packet(pkt, task.pts + global_delay(), task.dts + global_delay()); } if (!global_flags.uncompressed_video_to_http && - !global_flags.x264_video_to_http) { + !global_flags.x264_video_to_http && + !global_flags.av1_video_to_http) { stream_mux->add_packet(pkt, task.pts + global_delay(), task.dts + global_delay()); } } @@ -1450,6 +1454,8 @@ QuickSyncEncoderImpl::QuickSyncEncoderImpl(const std::string &filename, Resource if (global_flags.x264_video_to_http || global_flags.x264_video_to_disk) { assert(http_encoder != nullptr); assert(disk_encoder != nullptr); + } else if (global_flags.av1_video_to_http) { + assert(http_encoder != nullptr); } else { assert(http_encoder == nullptr); assert(disk_encoder == nullptr); @@ -1911,10 +1917,9 @@ void QuickSyncEncoderImpl::pass_frame(QuickSyncEncoderImpl::PendingFrame frame, uint8_t *data = reinterpret_cast(surf->y_ptr); if (global_flags.uncompressed_video_to_http) { add_packet_for_uncompressed_frame(pts, duration, data); - } else if (global_flags.x264_video_to_http || global_flags.x264_video_to_disk) { + } else if (http_encoder != nullptr) { http_encoder->add_frame(pts, duration, frame.ycbcr_coefficients, data, received_ts); - } - if (global_flags.x264_separate_disk_encode) { + } if (disk_encoder != nullptr && disk_encoder != http_encoder) { disk_encoder->add_frame(pts, duration, frame.ycbcr_coefficients, data, received_ts); } diff --git a/nageru/quicksync_encoder_impl.h b/nageru/quicksync_encoder_impl.h index 3d777dc..4c82131 100644 --- a/nageru/quicksync_encoder_impl.h +++ b/nageru/quicksync_encoder_impl.h @@ -171,7 +171,7 @@ private: std::mutex file_audio_encoder_mutex; std::unique_ptr file_audio_encoder; - VideoCodecInterface *http_encoder; // nullptr if not using x264. + VideoCodecInterface *http_encoder; // nullptr if not using x264/SVT-AV1. VideoCodecInterface *disk_encoder; std::unique_ptr v4l_output; // nullptr if not using V4L2 output. diff --git a/nageru/video_encoder.cpp b/nageru/video_encoder.cpp index c75c4e3..3342cfb 100644 --- a/nageru/video_encoder.cpp +++ b/nageru/video_encoder.cpp @@ -12,6 +12,9 @@ extern "C" { } #include "audio_encoder.h" +#ifdef HAVE_AV1 +#include "av1_encoder.h" +#endif #include "defs.h" #include "shared/ffmpeg_raii.h" #include "flags.h" @@ -61,8 +64,14 @@ VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const if (global_flags.x264_video_to_http || global_flags.x264_video_to_disk) { x264_encoder.reset(new X264Encoder(oformat, /*use_separate_disk_params=*/false)); } - X264Encoder *http_encoder = x264_encoder.get(); - X264Encoder *disk_encoder = x264_encoder.get(); + VideoCodecInterface *http_encoder = x264_encoder.get(); + VideoCodecInterface *disk_encoder = x264_encoder.get(); +#ifdef HAVE_AV1 + if (global_flags.av1_video_to_http) { + av1_encoder.reset(new AV1Encoder(oformat)); + http_encoder = av1_encoder.get(); + } +#endif if (global_flags.x264_separate_disk_encode) { x264_disk_encoder.reset(new X264Encoder(oformat, /*use_separate_disk_params=*/true)); disk_encoder = x264_disk_encoder.get(); @@ -77,6 +86,11 @@ VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const if (global_flags.x264_video_to_http) { x264_encoder->add_mux(stream_mux.get()); } +#ifdef HAVE_AV1 + if (global_flags.av1_video_to_http) { + av1_encoder->add_mux(stream_mux.get()); + } +#endif } VideoEncoder::~VideoEncoder() @@ -201,6 +215,8 @@ void VideoEncoder::open_output_stream() Mux::Codec video_codec; if (global_flags.uncompressed_video_to_http) { video_codec = Mux::CODEC_NV12; + } else if (global_flags.av1_video_to_http) { + video_codec = Mux::CODEC_AV1; } else { video_codec = Mux::CODEC_H264; } @@ -210,6 +226,10 @@ void VideoEncoder::open_output_stream() string video_extradata; if (global_flags.x264_video_to_http || global_flags.x264_video_to_disk) { video_extradata = x264_encoder->get_global_headers(); +#ifdef HAVE_AV1 + } else if (global_flags.av1_video_to_http) { + video_extradata = av1_encoder->get_global_headers(); +#endif } stream_mux.reset(new Mux(avctx, width, height, video_codec, video_extradata, stream_audio_encoder->get_codec_parameters().get(), diff --git a/nageru/video_encoder.h b/nageru/video_encoder.h index 3c82c00..7a5fef1 100644 --- a/nageru/video_encoder.h +++ b/nageru/video_encoder.h @@ -24,6 +24,7 @@ extern "C" { #include "shared/ref_counted_gl_sync.h" class AudioEncoder; +class AV1Encoder; class DiskSpaceEstimator; class HTTPD; class Mux; @@ -94,6 +95,9 @@ private: std::unique_ptr stream_audio_encoder; std::unique_ptr x264_encoder; // nullptr if not using x264. std::unique_ptr x264_disk_encoder; // nullptr if not using x264, or if not having separate disk encodes. +#ifdef HAVE_AV1 + std::unique_ptr av1_encoder; // nullptr if not using SVT-AV1. +#endif std::string stream_mux_header; MuxMetrics stream_mux_metrics; diff --git a/shared/mux.cpp b/shared/mux.cpp index b9ca553..f645279 100644 --- a/shared/mux.cpp +++ b/shared/mux.cpp @@ -59,6 +59,8 @@ Mux::Mux(AVFormatContext *avctx, int width, int height, Codec video_codec, const avstream_video->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; if (video_codec == CODEC_H264) { avstream_video->codecpar->codec_id = AV_CODEC_ID_H264; + } else if (video_codec == CODEC_AV1) { + avstream_video->codecpar->codec_id = AV_CODEC_ID_AV1; } else if (video_codec == CODEC_NV12) { avstream_video->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO; avstream_video->codecpar->codec_tag = avcodec_pix_fmt_to_codec_tag(AV_PIX_FMT_NV12); diff --git a/shared/mux.h b/shared/mux.h index 1b9fe93..075da16 100644 --- a/shared/mux.h +++ b/shared/mux.h @@ -50,6 +50,7 @@ class Mux { public: enum Codec { CODEC_H264, + CODEC_AV1, CODEC_NV12, // Uncompressed 4:2:0. CODEC_MJPEG }; -- 2.39.2