- #!/usr/bin/perl
- # Send live video/audio media as RTP streams, published via RTSP
- # Depends: libglib-object-introspection-perl, gir1.2-gst-rtsp-server-1.0
- # Recommends: gstreamer1.0-plugins-good
- use v5.12;
- use strictures 2;
- use Glib qw( TRUE FALSE );
- use Glib::Object::Introspection;
- use IPC::System::Simple qw(capturex);
- BEGIN {
- Glib::Object::Introspection->setup(
- basename => 'Gst',
- version => '1.0',
- package => 'Gst',
- );
- Glib::Object::Introspection->setup(
- basename => 'GstRtspServer',
- version => '1.0',
- package => 'Gst',
- );
- }
- my $ADDRESS = shift || $ENV{'ADDRESS'} || '127.0.0.1';
- my $PORT = shift || $ENV{'PORT'} || '8554';
- my $VDEVICES = shift || $ENV{'VDEVICES'} || '';
- my $ADEVICES = shift || $ENV{'ADEVICES'} || '';
- my $VFORMAT = shift || $ENV{'VFORMAT'} || 'VP8'; # H264 VP8 RAW - default: VP8
- my $AFORMAT
- = shift || $ENV{'AFORMAT'} || 'OPUS'; # AMR OPUS RAW - default: OPUS
- my @VDEVICES = $VDEVICES ? split ' ', $VDEVICES : sort split ' ',
- capturex( 'find', qw(/dev -maxdepth 1 -type c -name video*) );
- # FIXME: Detect/blacklist and skip faulty devices
- #my @ADEVICES = grep { /^hw:/ } capturex( 'arecord', qw(-L) );
- my @ADEVICES = split ' ', $ADEVICES;
- chomp @ADEVICES;
- #use Data::Dump; die dd @ADEVICES;
- my $HEIGHT = 288;
- my $FRAMERATE = 25;
- my $AUDIORATE = 48000;
- my $RATIO_NUM = 4;
- my $RATIO_DEN = 3;
- # * bitrates and bits in parens based on https://developer.apple.com/library/content/documentation/General/Reference/HLSAuthoringSpec/Requirements.html
- # + bits rounded up to include nearby modulo 16 formats
- # + best (i.e. high-modulo) 16:9 heights: 216 288 360 432 576 720
- # + best (i.e. high-modulo) 4:3 heights: 288 312 384 480 624 816
- # * speeds tuned to just below 100% cpu usage for each combination on a multi-core computer
- # TODO: Externalize speeds to site-specific configfile
- my ($VBITRATE, $SPEED_X264, $SPEED_X264_ALONE, $SPEED_VP8,
- $SPEED_VP8_ALONE
- );
- $RATIO_NUM ||= 16;
- $RATIO_DEN ||= 9;
- my $WIDTH ||= $HEIGHT * $RATIO_NUM / $RATIO_DEN;
- my $BITS = $WIDTH * $HEIGHT;
- if ( $BITS le 110592 ) { # 234p → 97344
- $VBITRATE = 145000;
- $SPEED_X264 = 'fast';
- $SPEED_X264_ALONE = 'fast';
- $SPEED_VP8 = 3;
- $SPEED_VP8_ALONE = 2;
- }
- elsif ( $BITS le 150528 ) { # 270p → 129600
- $VBITRATE = 365000;
- $SPEED_X264 = 'faster';
- $SPEED_X264_ALONE = 'fast';
- $SPEED_VP8 = 4;
- $SPEED_VP8_ALONE = 2;
- }
- elsif ( $BITS le 196608 ) { # 360p → 172800
- $VBITRATE = 730000;
- $SPEED_X264 = 'veryfast';
- $SPEED_X264_ALONE = 'fast';
- $SPEED_VP8 = 5;
- $SPEED_VP8_ALONE = 3;
- }
- elsif ( $BITS le 331776 ) { # 432p → 331776
- $VBITRATE = 1100000;
- $SPEED_X264 = 'ultrafast';
- $SPEED_X264_ALONE = 'fast';
- $SPEED_VP8 = 8;
- $SPEED_VP8_ALONE = 4;
- }
- elsif ( $BITS le 589824 ) { # 540p → 518400
- $VBITRATE = 2000000;
- $SPEED_X264 = 'toofast';
- $SPEED_X264_ALONE = 'veryfast';
- $SPEED_VP8_ALONE = 5;
- }
- elsif ( $BITS le 921600 ) { # 720p → 921600
- $VBITRATE = 3000000;
- $SPEED_X264 = 'toofast';
- $SPEED_X264_ALONE = 'ultrafast';
- $SPEED_VP8_ALONE = 15;
- }
- die "Not enough CPU - reduce size or streams"
- if ( $SPEED_X264 eq 'toofast' );
- # TODO: implement codec-specific height pools
- #if ( toofast == $SPEED_X264 ) { @HEIGHTS_MPEG = () };
- #unless (@HEIGHTS_MPEG) { $SPEED_VP8 = $SPEED_VP8_ALONE }
- #unless (@HEIGHTS_WEBM) { $SPEED_X264 = $SPEED_X264_ALONE }
- my $VCAPS = "video/x-raw,height=$HEIGHT";
- my $ACAPS = "audio/x-raw,rate=$AUDIORATE,channels=2,depth=16";
- # * http://stackoverflow.com/a/42237307
- my $ABUFFERS = 20000;
- # * force threads using queues - see http://stackoverflow.com/a/30738533
- # * generous queue sizes inspired by https://wiki.xiph.org/GST_cookbook
- my $QUEUE = "queue max-size-bytes=100000000 max-size-time=0";
- my %PIPELINE = (
- AMR => {
- AENC => [ 'amrnbenc', $QUEUE, 'rtpamrpay' ],
- },
- H264 => {
- # * let x264 use low-latency sliced-threads (i.e. don't disable treads)
- VENC => [
- "x264enc speed-preset=$SPEED_X264 tune=zerolatency bitrate=800 byte-stream=true key-int-max=15 intra-refresh=true option-string=\"slice-max-size=8192:vbv-maxrate=80:vbv-bufsize=10\"",
- 'video/x-h264,profile=baseline',
- $QUEUE,
- 'rtph264pay',
- ],
- },
- OPUS => {
- AENC => [ 'opusenc', $QUEUE, 'rtpopuspay' ],
- },
- VP8 => {
- VENC => [
- "vp8enc threads=4 cpu-used=$SPEED_VP8 deadline=1000000 end-usage=1 target-bitrate=$VBITRATE undershoot=95 keyframe-max-dist=999999 max-quantizer=56 deadline=5000 static-threshold=500",
- 'video/x-vp8',
- $QUEUE,
- 'rtpvp8pay',
- ],
- },
- RAW => {
- AENC => ['rtpL16pay'],
- VENC => ['rtpvrawpay'],
- },
- );
- our $nextpayload = 0;
- sub cam
- {
- my ( $device, $payload ) = @_;
- my $factory = Gst::RTSPMediaFactory->new();
- my $pipeline = join(
- ' ! ',
- ( "v4l2src device=$device",
- $QUEUE,
- 'videoconvert',
- $VCAPS,
- $QUEUE,
- @{ $PIPELINE{$VFORMAT}{'VENC'} },
- )
- );
- return "( $pipeline name=pay$payload )";
- }
- sub mic
- {
- my ( $device, $payload ) = @_;
- my $factory = Gst::RTSPMediaFactory->new();
- my $pipeline = join(
- ' ! ',
- ( "alsasrc device=$device buffer-time=$ABUFFERS",
- $QUEUE,
- 'audioconvert',
- $QUEUE,
- @{ $PIPELINE{$AFORMAT}{'AENC'} },
- )
- );
- return "( $pipeline name=pay$payload )";
- }
- sub factory
- {
- my @pipeline = @_;
- my $factory = Gst::RTSPMediaFactory->new();
- $factory->set_launch( join( ' ', @pipeline ) );
- $factory->set_shared(TRUE);
- #say "media ($device): " . $factory->get_launch();
- # $factory->set_latency(5);
- #say "latency ($device): " . $factory->get_latency();
- return $factory;
- }
- Gst::init( [ $0, @ARGV ] );
- my $loop = Glib::MainLoop->new( undef, FALSE );
- # create a server instance
- my $server = Gst::RTSPServer->new();
- $server->set_address($ADDRESS);
- $server->set_service($PORT);
- # get the mount points for this server, every server has a default
- # object that be used to map uri mount points to media factories
- my $mounts = $server->get_mount_points();
- # attach media to URIs
- my @mounts;
- for my $i ( 0 .. $#VDEVICES ) {
- my $mount = "/cam$i";
- $mounts->add_factory(
- $mount,
- factory( cam( $VDEVICES[$i], $nextpayload++ ) )
- );
- push @mounts, $mount;
- }
- for my $i ( 0 .. $#ADEVICES ) {
- my $mount = "/mic$i";
- $mounts->add_factory(
- $mount,
- factory( mic( $ADEVICES[$i], $nextpayload++ ) )
- );
- push @mounts, $mount;
- }
- if ( @ADEVICES and @VDEVICES ) {
- my $mount = "/main";
- $mounts->add_factory(
- $mount,
- factory(
- mic( $ADEVICES[0], $#VDEVICES + 1 ),
- cam( $VDEVICES[0], 0 )
- )
- );
- push @mounts, $mount;
- }
- # don't need the ref to the mapper anymore
- undef $mounts;
- # attach the server to the default maincontext
- my $retval = $server->attach(undef);
- # start serving
- say "streams ready at the following URLs:";
- for (@mounts) {
- say "rtsp://$ADDRESS:$PORT$_";
- }
- $loop->run;
|