#!/usr/bin/perl

# Send live video/audio media as RTP streams, published via RTSP

use v5.12;
use warnings;

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'} || 'RAW'; # H264 VP8 RAW - default: RAW
my $AFORMAT = shift || $ENV{'AFORMAT'} || 'RAW'; # AMR OPUS RAW - default: RAW

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 = 240;
my $FRAMERATE = 25;
my $AUDIORATE = 48000;

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 %VFORMAT = (
	H264 => {
		# * let x264 use low-latency sliced-threads (i.e. don't disable treads)
		VENC => "x264enc speed-preset=ultrafast 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",
	},
	VP8 => {
		VENC => "vp8enc cpu-used=10 threads=2 deadline=10000 ! video/x-vp8 ! $QUEUE ! rtpvp8pay",
	},
	RAW => {
		VENC => "rtpvrawpay",
	},
);

my %AFORMAT = (
	AMR => {
		AENC => "amrnbenc ! $QUEUE ! rtpamrpay",
	},
	OPUS => {
		AENC => "opusenc ! $QUEUE ! rtpopuspay",
	},
	RAW => {
		AENC => "rtpL16pay",
	},
);

our $nextpayload = 0;

sub cam {
	my $device = shift;
	my $payload = "pay" . $nextpayload++;

	my $factory = Gst::RTSPMediaFactory->new();
	$factory->set_launch("( v4l2src device=$device ! $QUEUE ! videoconvert ! $VCAPS ! $QUEUE ! $VFORMAT{$VFORMAT}{'VENC'} name=$payload )");
	$factory->set_shared(TRUE);
say "media ($device): " . $factory->get_launch();
#	$factory->set_latency(5);
#say "latency ($device): " . $factory->get_latency();

	return $factory;
}

sub mic {
	my $device = shift;
	my $payload = "pay" . $nextpayload++;

	my $factory = Gst::RTSPMediaFactory->new();
	$factory->set_launch("( alsasrc device=$device buffer-time=$ABUFFERS ! $QUEUE ! audioconvert ! $QUEUE ! $AFORMAT{$AFORMAT}{'AENC'} name=$payload )");
	$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, cam($VDEVICES[$i]));
	push @mounts, $mount;
};
for my $i ( 0 .. $#ADEVICES ) {
	my $mount = "/mic$i";
	$mounts->add_factory($mount, mic($ADEVICES[$i]));
	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;