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.
- 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).
#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"
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,
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",
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");
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=<KEY>,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=<KEY>,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");
{ "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 },
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;
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);
+ }
}
bool use_zerocopy = false; // Not user-settable.
bool fullscreen = false;
std::map<unsigned, unsigned> 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;
#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
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());
}
}
}
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();
}
~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<float> audio); // Thread-safe.
bool is_zerocopy() const; // Thread-safe.
{
http_mux = mux;
}
+ void set_srt_mux(Mux *mux)
+ {
+ srt_mux = mux;
+ }
// So we never get negative dts.
int64_t global_delay() const {
std::unique_ptr<V4LOutput> 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<Mux> file_mux; // To local disk.
// Encoder parameters
#include <stdio.h>
#include <time.h>
#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
#include <string>
#include <thread>
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()) {
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
}
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)
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 "<unknown address>"; // 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;
+}
+
#include <libavformat/avio.h>
}
+#include <srt/srt.h>
+
#include "shared/mux.h"
#include "shared/ref_counted_gl_sync.h"
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<QuickSyncEncoder> quicksync_encoder; // Under <qs_mu> _and_ <qs_audio_mu>.
movit::ResourcePool *resource_pool;
bool seen_sync_markers = false;
std::unique_ptr<Mux> http_mux; // To the HTTP server.
+ std::unique_ptr<Mux> srt_mux; // To the SRT endpoint (if any).
std::unique_ptr<AudioEncoder> stream_audio_encoder;
std::unique_ptr<X264Encoder> x264_encoder; // nullptr if not using x264.
std::unique_ptr<X264Encoder> x264_disk_encoder; // nullptr if not using x264, or if not having separate disk encodes.
std::unique_ptr<AV1Encoder> 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<int> quicksync_encoders_in_shutdown{0};
std::atomic<int> overriding_bitrate{0};