From e2b654d6a8cc8c64142a9a8ef8bcd82e9d9a9289 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sun, 6 Aug 2023 22:51:18 +0200 Subject: [PATCH] Implement SRT output. This is useful for push, and for bad networks (e.g. 4G). You can in theory push to another Nageru instance, but the most logical would either be to a Cubemap (running FFmpeg to demux, unfortunately), or to something like YouTube, which is now working on SRT ingest. Note that for YouTube SRT ingest to work, someone from YouTube needs to set a special flag on your account for now. --- README | 5 +- nageru/defs.h | 1 + nageru/flags.cpp | 63 +++++++++- nageru/flags.h | 5 + nageru/main.cpp | 2 +- nageru/quicksync_encoder.cpp | 6 + nageru/quicksync_encoder.h | 1 + nageru/quicksync_encoder_impl.h | 5 + nageru/video_encoder.cpp | 201 ++++++++++++++++++++++++++++---- nageru/video_encoder.h | 15 ++- 10 files changed, 271 insertions(+), 33 deletions(-) diff --git a/README b/README index 0950547..b4690fa 100644 --- a/README +++ b/README @@ -91,8 +91,9 @@ Nageru currently needs: - Optional: libsrt, for SRT inputs (by default, Nageru will listen on port 9710, although you can change this port on the command line, turn it off with --srt-port -1, or turn it off live in the UI). - If you build with libsrt, make sure it is not linked to OpenSSL, - for license reasons. + SRT can also be used for output in addition to listening for HTTP + (see --srt-destination). 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). diff --git a/nageru/defs.h b/nageru/defs.h index a8817f6..8f320e1 100644 --- a/nageru/defs.h +++ b/nageru/defs.h @@ -23,6 +23,7 @@ #define DEFAULT_STREAM_MUX_NAME "nut" // Only for HTTP. Local dump guesses from LOCAL_DUMP_SUFFIX. #define DEFAULT_HTTPD_PORT 9095 #define DEFAULT_SRT_PORT 9710 +#define DEFAULT_SRT_OUTPUT_LATENCY_MS 2000 #include "shared/shared_defs.h" diff --git a/nageru/flags.cpp b/nageru/flags.cpp index 3c15755..965cdc7 100644 --- a/nageru/flags.cpp +++ b/nageru/flags.cpp @@ -51,6 +51,11 @@ enum LongOption { OPTION_HTTP_PORT, OPTION_SRT_PORT, OPTION_NO_SRT, + OPTION_SRT_DESTINATION, + OPTION_SRT_STREAMID, + OPTION_SRT_PASSPHRASE, + OPTION_SRT_YOUTUBE_STREAM_KEY, + OPTION_SRT_LATENCY, OPTION_NO_TRANSCODE_VIDEO, OPTION_NO_TRANSCODE_AUDIO, OPTION_DISABLE_AUDIO, @@ -186,7 +191,7 @@ void usage(Program program) 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, " --http-av1-video send AV1-compressed video to HTTP clients and SRT output\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", @@ -196,9 +201,9 @@ void usage(Program program) 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, " --http-audio-codec=NAME audio codec to use for HTTP streams and SRT output\n"); fprintf(stderr, " (default is to use the same as for the recording)\n"); - fprintf(stderr, " --http-audio-bitrate=KBITS audio codec bit rate to use for HTTP streams\n"); + fprintf(stderr, " --http-audio-bitrate=KBITS audio codec bit rate to use for HTTP streams and SRT output\n"); fprintf(stderr, " (default is %d, ignored unless --http-audio-codec is set)\n", DEFAULT_AUDIO_OUTPUT_BIT_RATE / 1000); fprintf(stderr, " --http-port=PORT which port to use for the built-in HTTP server\n"); @@ -216,6 +221,15 @@ void usage(Program program) fprintf(stderr, " --disable-audio do not include any audio in the stream\n"); } if (program == PROGRAM_NAGERU) { +#ifdef HAVE_SRT + fprintf(stderr, " --srt-destination=HOST:PORT send HTTP video stream also to given destination\n"); + fprintf(stderr, " --srt-streamid=STREAMID SRT stream identifying ID\n"); + fprintf(stderr, " --srt-passphrase=PASSPHRASE SRT encryption key\n"); + fprintf(stderr, " --srt-youtube-stream-key=KEY shortcut for --srt-destination=a.srt.youtube.com:2010\n"); + fprintf(stderr, " --srt-streamid=#!::u=,copy=0,encoder=Nageru\n"); + fprintf(stderr, " --srt-latency=MS output SRT latency in milliseconds (default is %d)\n", DEFAULT_SRT_OUTPUT_LATENCY_MS); + fprintf(stderr, " --srt-streamid=#!::u=,copy=0,encoder=Nageru\n"); +#endif fprintf(stderr, " --flat-audio start with most audio processing turned off\n"); fprintf(stderr, " (can be overridden by e.g. --enable-limiter)\n"); fprintf(stderr, " --gain-staging=DB set initial gain staging to the given value\n"); @@ -306,6 +320,13 @@ void parse_flags(Program program, int argc, char * const argv[]) { "srt-port", required_argument, 0, OPTION_SRT_PORT }, #endif { "no-srt", no_argument, 0, OPTION_NO_SRT }, // We silently allow this even without HAVE_SRT. +#ifdef HAVE_SRT + { "srt-destination", required_argument, 0, OPTION_SRT_DESTINATION }, + { "srt-streamid", required_argument, 0, OPTION_SRT_STREAMID }, + { "srt-passphrase", required_argument, 0, OPTION_SRT_PASSPHRASE }, + { "srt-youtube-stream-key", required_argument, 0, OPTION_SRT_YOUTUBE_STREAM_KEY }, + { "srt-latency", required_argument, 0, OPTION_SRT_LATENCY }, +#endif { "no-transcode-video", no_argument, 0, OPTION_NO_TRANSCODE_VIDEO }, { "no-transcode-audio", no_argument, 0, OPTION_NO_TRANSCODE_AUDIO }, { "disable-audio", no_argument, 0, OPTION_DISABLE_AUDIO }, @@ -446,6 +467,37 @@ void parse_flags(Program program, int argc, char * const argv[]) case OPTION_NO_SRT: global_flags.srt_port = -1; break; +#ifdef HAVE_SRT + case OPTION_SRT_DESTINATION: { + const char *ptr = strrchr(optarg, ':'); + if (ptr == nullptr || strlen(optarg) < 3) { + fprintf(stderr, "ERROR: --srt-destination must be of the form host:port\n"); + exit(1); + } + global_flags.srt_destination_host = string(optarg, ptr - optarg); + if (global_flags.srt_destination_host[0] == '[' && + global_flags.srt_destination_host.back() == ']') { + // Support [IPv6]:port, in a sort of hackish way. + global_flags.srt_destination_host = global_flags.srt_destination_host.substr(1, global_flags.srt_destination_host.size() - 2); + } + global_flags.srt_destination_port = ptr + 1; + break; + } + case OPTION_SRT_STREAMID: + global_flags.srt_streamid = optarg; + break; + case OPTION_SRT_PASSPHRASE: + global_flags.srt_passphrase = optarg; + break; + case OPTION_SRT_YOUTUBE_STREAM_KEY: + global_flags.srt_destination_host = "a.srt.youtube.com"; + global_flags.srt_destination_port = "2010"; + global_flags.srt_streamid = string("#!::u=") + optarg + ",copy=0,encoder=Nageru " NAGERU_VERSION; + break; + case OPTION_SRT_LATENCY: + global_flags.srt_output_latency = atoi(optarg); + break; +#endif case OPTION_NO_TRANSCODE_VIDEO: global_flags.transcode_video = false; break; @@ -831,4 +883,9 @@ void parse_flags(Program program, int argc, char * const argv[]) global_flags.card_to_mjpeg_stream_export[card_idx] = card_idx; } } + + if (global_flags.srt_destination_host == "a.srt.youtube.com" && global_flags.srt_passphrase.empty()) { + fprintf(stderr, "ERROR: Cannot stream to YouTube without --srt-passphrase (get it from the YouTube streaming console)\n"); + exit(1); + } } diff --git a/nageru/flags.h b/nageru/flags.h index 54e971a..91e8d85 100644 --- a/nageru/flags.h +++ b/nageru/flags.h @@ -83,6 +83,11 @@ struct Flags { bool use_zerocopy = false; // Not user-settable. bool fullscreen = false; std::map card_to_mjpeg_stream_export; // If a card is not in the map, it is not exported. + std::string srt_destination_host; + std::string srt_destination_port; + std::string srt_streamid; + std::string srt_passphrase; + int srt_output_latency = DEFAULT_SRT_OUTPUT_LATENCY_MS; }; extern Flags global_flags; diff --git a/nageru/main.cpp b/nageru/main.cpp index 37034ed..9b26426 100644 --- a/nageru/main.cpp +++ b/nageru/main.cpp @@ -82,7 +82,7 @@ int main(int argc, char *argv[]) #endif #ifdef HAVE_SRT - if (global_flags.srt_port >= 0) { + if (global_flags.srt_port >= 0 || !global_flags.srt_destination_host.empty()) { srt_startup(); } #endif diff --git a/nageru/quicksync_encoder.cpp b/nageru/quicksync_encoder.cpp index 0d0fbec..d75dbeb 100644 --- a/nageru/quicksync_encoder.cpp +++ b/nageru/quicksync_encoder.cpp @@ -1353,6 +1353,7 @@ void QuickSyncEncoderImpl::save_codeddata(GLSurface *surf, storage_task task) if (!global_flags.x264_video_to_http && !global_flags.av1_video_to_http) { http_mux->add_packet(pkt, task.pts + global_delay(), task.dts + global_delay()); + srt_mux->add_packet(pkt, task.pts + global_delay(), task.dts + global_delay()); } } } @@ -2050,6 +2051,11 @@ void QuickSyncEncoder::set_http_mux(Mux *mux) impl->set_http_mux(mux); } +void QuickSyncEncoder::set_srt_mux(Mux *mux) +{ + impl->set_srt_mux(mux); +} + int64_t QuickSyncEncoder::global_delay() const { return impl->global_delay(); } diff --git a/nageru/quicksync_encoder.h b/nageru/quicksync_encoder.h index 6c27cfb..c900540 100644 --- a/nageru/quicksync_encoder.h +++ b/nageru/quicksync_encoder.h @@ -67,6 +67,7 @@ public: ~QuickSyncEncoder(); void set_http_mux(Mux *mux); // Does not take ownership. Must be called unless x264 is used for the stream. + void set_srt_mux(Mux *mux); // Does not take ownership. Must be called if SRT is to be used, unless x264 is used for the stream. void add_audio(int64_t pts, std::vector audio); // Thread-safe. bool is_zerocopy() const; // Thread-safe. diff --git a/nageru/quicksync_encoder_impl.h b/nageru/quicksync_encoder_impl.h index ee8ec47..94c6251 100644 --- a/nageru/quicksync_encoder_impl.h +++ b/nageru/quicksync_encoder_impl.h @@ -56,6 +56,10 @@ public: { http_mux = mux; } + void set_srt_mux(Mux *mux) + { + srt_mux = mux; + } // So we never get negative dts. int64_t global_delay() const { @@ -175,6 +179,7 @@ private: std::unique_ptr v4l_output; // nullptr if not using V4L2 output. Mux* http_mux = nullptr; // To the HTTP server. + Mux* srt_mux = nullptr; // To the remote SRT endpoint, if any. std::unique_ptr file_mux; // To local disk. // Encoder parameters diff --git a/nageru/video_encoder.cpp b/nageru/video_encoder.cpp index ca5ed01..29f3105 100644 --- a/nageru/video_encoder.cpp +++ b/nageru/video_encoder.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include #include #include @@ -54,6 +57,10 @@ string generate_local_dump_filename(int frame) VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const std::string &va_display, int width, int height, HTTPD *httpd, DiskSpaceEstimator *disk_space_estimator) : resource_pool(resource_pool), surface(surface), va_display(va_display), width(width), height(height), httpd(httpd), disk_space_estimator(disk_space_estimator) { + // TODO: If we're outputting AV1, we can't use MPEG-TS currently. + srt_oformat = av_guess_format("mpegts", nullptr, nullptr); + assert(srt_oformat != nullptr); + oformat = av_guess_format(global_flags.stream_mux_name.c_str(), nullptr, nullptr); assert(oformat != nullptr); if (global_flags.stream_audio_codec_name.empty()) { @@ -80,15 +87,19 @@ VideoEncoder::VideoEncoder(ResourcePool *resource_pool, QSurface *surface, const string filename = generate_local_dump_filename(/*frame=*/0); quicksync_encoder.reset(new QuickSyncEncoder(filename, resource_pool, surface, va_display, width, height, oformat, http_encoder, disk_encoder, disk_space_estimator)); - open_output_stream(); + open_output_streams(); stream_audio_encoder->add_mux(http_mux.get()); + stream_audio_encoder->add_mux(srt_mux.get()); quicksync_encoder->set_http_mux(http_mux.get()); + quicksync_encoder->set_srt_mux(srt_mux.get()); if (global_flags.x264_video_to_http) { x264_encoder->add_mux(http_mux.get()); + x264_encoder->add_mux(srt_mux.get()); } #ifdef HAVE_AV1 if (global_flags.av1_video_to_http) { av1_encoder->add_mux(http_mux.get()); + av1_encoder->add_mux(srt_mux.get()); } #endif } @@ -202,38 +213,54 @@ RefCountedGLsync VideoEncoder::end_frame() return quicksync_encoder->end_frame(); } -void VideoEncoder::open_output_stream() +void VideoEncoder::open_output_streams() { - AVFormatContext *avctx = avformat_alloc_context(); - avctx->oformat = oformat; + for (bool is_srt : {false, true}) { + if (is_srt && global_flags.srt_destination_host.empty()) { + continue; + } - uint8_t *buf = (uint8_t *)av_malloc(MUX_BUFFER_SIZE); - avctx->pb = avio_alloc_context(buf, MUX_BUFFER_SIZE, 1, this, nullptr, nullptr, nullptr); - avctx->pb->write_data_type = &VideoEncoder::write_packet2_thunk; - avctx->pb->ignore_boundary_point = 1; + AVFormatContext *avctx = avformat_alloc_context(); + avctx->oformat = is_srt ? srt_oformat : oformat; - Mux::Codec video_codec; - if (global_flags.av1_video_to_http) { - video_codec = Mux::CODEC_AV1; - } else { - video_codec = Mux::CODEC_H264; - } + uint8_t *buf = (uint8_t *)av_malloc(MUX_BUFFER_SIZE); + avctx->pb = avio_alloc_context(buf, MUX_BUFFER_SIZE, 1, this, nullptr, nullptr, nullptr); + if (is_srt) { + avctx->pb->write_packet = &VideoEncoder::write_srt_packet_thunk; + } else { + avctx->pb->write_data_type = &VideoEncoder::write_packet2_thunk; + avctx->pb->ignore_boundary_point = 1; + } + + Mux::Codec video_codec; + if (global_flags.av1_video_to_http) { + video_codec = Mux::CODEC_AV1; + } else { + video_codec = Mux::CODEC_H264; + } - avctx->flags = AVFMT_FLAG_CUSTOM_IO; + avctx->flags = AVFMT_FLAG_CUSTOM_IO; - string video_extradata; - if (global_flags.x264_video_to_http) { - video_extradata = x264_encoder->get_global_headers(); + string video_extradata; + if (global_flags.x264_video_to_http) { + 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(); + } else if (global_flags.av1_video_to_http) { + video_extradata = av1_encoder->get_global_headers(); #endif - } + } - http_mux.reset(new Mux(avctx, width, height, video_codec, video_extradata, stream_audio_encoder->get_codec_parameters().get(), - get_color_space(global_flags.ycbcr_rec709_coefficients), COARSE_TIMEBASE, - /*write_callback=*/nullptr, Mux::WRITE_FOREGROUND, { &http_mux_metrics })); - http_mux_metrics.init({{ "destination", "http" }}); + Mux *mux = new Mux(avctx, width, height, video_codec, video_extradata, stream_audio_encoder->get_codec_parameters().get(), + get_color_space(global_flags.ycbcr_rec709_coefficients), COARSE_TIMEBASE, + /*write_callback=*/nullptr, is_srt ? Mux::WRITE_BACKGROUND : Mux::WRITE_FOREGROUND, { is_srt ? &srt_mux_metrics : &http_mux_metrics }); + if (is_srt) { + srt_mux.reset(mux); + srt_mux_metrics.init({{ "destination", "srt" }}); + } else { + http_mux.reset(mux); + http_mux_metrics.init({{ "destination", "http" }}); + } + } } int VideoEncoder::write_packet2_thunk(void *opaque, uint8_t *buf, int buf_size, AVIODataMarkerType type, int64_t time) @@ -261,3 +288,127 @@ int VideoEncoder::write_packet2(uint8_t *buf, int buf_size, AVIODataMarkerType t return buf_size; } +int VideoEncoder::write_srt_packet_thunk(void *opaque, uint8_t *buf, int buf_size) +{ + VideoEncoder *video_encoder = (VideoEncoder *)opaque; + return video_encoder->write_srt_packet(buf, buf_size); +} + +static string print_addrinfo(const addrinfo *ai) +{ + char hoststr[NI_MAXHOST], portstr[NI_MAXSERV]; + if (getnameinfo(ai->ai_addr, ai->ai_addrlen, hoststr, sizeof(hoststr), portstr, sizeof(portstr), NI_DGRAM | NI_NUMERICHOST | NI_NUMERICSERV) != 0) { + return ""; // Should basically never happen, since we're not doing DNS lookups. + } + + if (ai->ai_family == AF_INET6) { + return string("[") + hoststr + "]:" + portstr; + } else { + return string(hoststr) + ":" + portstr; + } +} + +int VideoEncoder::open_srt_socket() +{ + int sock = srt_create_socket(); + if (sock == -1) { + fprintf(stderr, "srt_create_socket(): %s\n", srt_getlasterror_str()); + return -1; + } + + SRT_TRANSTYPE live = SRTT_LIVE; + if (srt_setsockopt(sock, 0, SRTO_TRANSTYPE, &live, sizeof(live)) < 0) { + fprintf(stderr, "srt_setsockopt(SRTO_TRANSTYPE): %s\n", srt_getlasterror_str()); + srt_close(sock); + return -1; + } + + if (srt_setsockopt(sock, 0, SRTO_LATENCY, &global_flags.srt_output_latency, sizeof(global_flags.srt_output_latency)) < 0) { + fprintf(stderr, "srt_setsockopt(SRTO_LATENCY): %s\n", srt_getlasterror_str()); + srt_close(sock); + return -1; + } + + if (!global_flags.srt_streamid.empty()) { + if (srt_setsockopt(sock, 0, SRTO_STREAMID, global_flags.srt_streamid.data(), global_flags.srt_streamid.size()) < 0) { + fprintf(stderr, "srt_setsockopt(SRTO_STREAMID): %s\n", srt_getlasterror_str()); + srt_close(sock); + return -1; + } + } + + if (!global_flags.srt_passphrase.empty()) { + if (srt_setsockopt(sock, 0, SRTO_PASSPHRASE, global_flags.srt_passphrase.data(), global_flags.srt_passphrase.size()) < 0) { + fprintf(stderr, "srt_setsockopt(SRTO_PASSPHRASE): %s\n", srt_getlasterror_str()); + srt_close(sock); + return -1; + } + } + + return sock; +} + +int VideoEncoder::connect_to_srt() +{ + // We need to specify SOCK_DGRAM as a hint, or we'll get all addresses + // three times (for each of TCP, UDP, raw). + addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_ADDRCONFIG; + hints.ai_socktype = SOCK_DGRAM; + + addrinfo *ai; + int ret = getaddrinfo(global_flags.srt_destination_host.c_str(), global_flags.srt_destination_port.c_str(), &hints, &ai); + if (ret != 0) { + fprintf(stderr, "getaddrinfo(%s:%s): %s\n", global_flags.srt_destination_host.c_str(), global_flags.srt_destination_port.c_str(), gai_strerror(ret)); + return -1; + } + + for (const addrinfo *cur = ai; cur != nullptr; cur = cur->ai_next) { + // Seemingly, srt_create_socket() isn't universal; once we try to connect, + // it gets locked to either IPv4 or IPv6. So we need to create a new one + // for every address we try. + int sock = open_srt_socket(); + if (sock == -1) { + // Die immediately. + return sock; + } + if (srt_connect(sock, cur->ai_addr, cur->ai_addrlen) < 0) { + fprintf(stderr, "srt_connect(%s): %s\n", print_addrinfo(cur).c_str(), srt_getlasterror_str()); + srt_close(sock); + continue; + } + fprintf(stderr, "Connected to destination SRT endpoint at %s.\n", print_addrinfo(cur).c_str()); + freeaddrinfo(ai); + return sock; + } + + // Out of candidates, so give up. + freeaddrinfo(ai); + return -1; +} + +int VideoEncoder::write_srt_packet(uint8_t *buf, int buf_size) +{ + while (buf_size > 0) { + if (srt_sock == -1) { + srt_sock = connect_to_srt(); + if (srt_sock == -1) { + usleep(100000); + continue; + } + } + int to_send = min(buf_size, SRT_LIVE_DEF_PLSIZE); + int ret = srt_send(srt_sock, (char *)buf, to_send); + if (ret < 0) { + fprintf(stderr, "srt_send(): %s\n", srt_getlasterror_str()); + srt_close(srt_sock); + srt_sock = connect_to_srt(); + continue; + } + buf += ret; + buf_size -= ret; + } + return buf_size; +} + diff --git a/nageru/video_encoder.h b/nageru/video_encoder.h index 61b16f7..86badf2 100644 --- a/nageru/video_encoder.h +++ b/nageru/video_encoder.h @@ -20,6 +20,8 @@ extern "C" { #include } +#include + #include "shared/mux.h" #include "shared/ref_counted_gl_sync.h" @@ -75,11 +77,16 @@ public: void change_x264_bitrate(unsigned rate_kbit); private: - void open_output_stream(); + void open_output_streams(); static int write_packet2_thunk(void *opaque, uint8_t *buf, int buf_size, AVIODataMarkerType type, int64_t time); int write_packet2(uint8_t *buf, int buf_size, AVIODataMarkerType type, int64_t time); - const AVOutputFormat *oformat; + static int write_srt_packet_thunk(void *opaque, uint8_t *buf, int buf_size); + int write_srt_packet(uint8_t *buf, int buf_size); + int open_srt_socket(); // Returns -1 on error. + int connect_to_srt(); // Returns -1 on error. + + const AVOutputFormat *oformat, *srt_oformat; mutable std::mutex qs_mu, qs_audio_mu; std::unique_ptr quicksync_encoder; // Under _and_ . movit::ResourcePool *resource_pool; @@ -92,6 +99,7 @@ private: bool seen_sync_markers = false; std::unique_ptr http_mux; // To the HTTP server. + std::unique_ptr srt_mux; // To the SRT endpoint (if any). 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. @@ -99,8 +107,11 @@ private: std::unique_ptr av1_encoder; // nullptr if not using SVT-AV1. #endif + SRTSOCKET srt_sock = -1; + std::string http_mux_header; MuxMetrics http_mux_metrics; + MuxMetrics srt_mux_metrics; std::atomic quicksync_encoders_in_shutdown{0}; std::atomic overriding_bitrate{0}; -- 2.39.2