summaryrefslogtreecommitdiff
path: root/bin/capture+encode+serve
blob: 83af18c9154d94f4943a2a2927a97772729f0a44 (plain)
  1. #!/usr/bin/perl
  2. # Send live video/audio media as RTP streams, published via RTSP
  3. # Depends: libglib-object-introspection-perl, gir1.2-gst-rtsp-server-1.0
  4. # Recommends: gstreamer1.0-plugins-good
  5. use v5.12;
  6. use strictures 2;
  7. use Glib qw( TRUE FALSE );
  8. use Glib::Object::Introspection;
  9. use IPC::System::Simple qw(capturex);
  10. BEGIN {
  11. Glib::Object::Introspection->setup(
  12. basename => 'Gst',
  13. version => '1.0',
  14. package => 'Gst',
  15. );
  16. Glib::Object::Introspection->setup(
  17. basename => 'GstRtspServer',
  18. version => '1.0',
  19. package => 'Gst',
  20. );
  21. }
  22. my $ADDRESS = shift || $ENV{'ADDRESS'} || '127.0.0.1';
  23. my $PORT = shift || $ENV{'PORT'} || '8554';
  24. my $VDEVICES = shift || $ENV{'VDEVICES'} || '';
  25. my $ADEVICES = shift || $ENV{'ADEVICES'} || '';
  26. my $VFORMAT = shift || $ENV{'VFORMAT'} || 'VP8'; # H264 VP8 RAW - default: VP8
  27. my $AFORMAT
  28. = shift || $ENV{'AFORMAT'} || 'OPUS'; # AMR OPUS RAW - default: OPUS
  29. my @VDEVICES = $VDEVICES ? split ' ', $VDEVICES : sort split ' ',
  30. capturex( 'find', qw(/dev -maxdepth 1 -type c -name video*) );
  31. # FIXME: Detect/blacklist and skip faulty devices
  32. #my @ADEVICES = grep { /^hw:/ } capturex( 'arecord', qw(-L) );
  33. my @ADEVICES = split ' ', $ADEVICES;
  34. chomp @ADEVICES;
  35. #use Data::Dump; die dd @ADEVICES;
  36. my $HEIGHT = 240;
  37. my $FRAMERATE = 25;
  38. my $AUDIORATE = 48000;
  39. my $VBITRATE;
  40. # inspired by Apple HLS recommendations
  41. if ( $HEIGHT le 234 ) { $VBITRATE = 145000 }
  42. elsif ( $HEIGHT le 270 ) { $VBITRATE = 365000 }
  43. elsif ( $HEIGHT le 360 ) { $VBITRATE = 730000 }
  44. elsif ( $HEIGHT le 432 ) { $VBITRATE = 1100000 }
  45. elsif ( $HEIGHT le 540 ) { $VBITRATE = 2000000 }
  46. elsif ( $HEIGHT le 720 ) { $VBITRATE = 3000000 }
  47. my $VCAPS = "video/x-raw,height=$HEIGHT";
  48. my $ACAPS = "audio/x-raw,rate=$AUDIORATE,channels=2,depth=16";
  49. # * http://stackoverflow.com/a/42237307
  50. my $ABUFFERS = 20000;
  51. # * force threads using queues - see http://stackoverflow.com/a/30738533
  52. # * generous queue sizes inspired by https://wiki.xiph.org/GST_cookbook
  53. my $QUEUE = "queue max-size-bytes=100000000 max-size-time=0";
  54. my %PIPELINE = (
  55. AMR => {
  56. AENC => [ 'amrnbenc', $QUEUE, 'rtpamrpay' ],
  57. },
  58. H264 => {
  59. # * let x264 use low-latency sliced-threads (i.e. don't disable treads)
  60. VENC => [
  61. "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\"",
  62. 'video/x-h264,profile=baseline',
  63. $QUEUE,
  64. 'rtph264pay',
  65. ],
  66. },
  67. OPUS => {
  68. AENC => [ 'opusenc', $QUEUE, 'rtpopuspay' ],
  69. },
  70. VP8 => {
  71. VENC => [
  72. "vp8enc threads=4 cpu-used=15 deadline=1000000 end-usage=1 target-bitrate=$VBITRATE undershoot=95 keyframe-max-dist=999999 max-quantizer=56 deadline=5000 static-threshold=500",
  73. 'video/x-vp8',
  74. $QUEUE,
  75. 'rtpvp8pay',
  76. ],
  77. },
  78. RAW => {
  79. AENC => ['rtpL16pay'],
  80. VENC => ['rtpvrawpay'],
  81. },
  82. );
  83. our $nextpayload = 0;
  84. sub cam
  85. {
  86. my ( $device, $payload ) = @_;
  87. my $factory = Gst::RTSPMediaFactory->new();
  88. my $pipeline = join(
  89. ' ! ',
  90. ( "v4l2src device=$device",
  91. $QUEUE,
  92. 'videoconvert',
  93. $VCAPS,
  94. $QUEUE,
  95. @{ $PIPELINE{$VFORMAT}{'VENC'} },
  96. )
  97. );
  98. return "( $pipeline name=pay$payload )";
  99. }
  100. sub mic
  101. {
  102. my ( $device, $payload ) = @_;
  103. my $factory = Gst::RTSPMediaFactory->new();
  104. my $pipeline = join(
  105. ' ! ',
  106. ( "alsasrc device=$device buffer-time=$ABUFFERS",
  107. $QUEUE,
  108. 'audioconvert',
  109. $QUEUE,
  110. @{ $PIPELINE{$AFORMAT}{'AENC'} },
  111. )
  112. );
  113. return "( $pipeline name=pay$payload )";
  114. }
  115. sub factory
  116. {
  117. my @pipeline = @_;
  118. my $factory = Gst::RTSPMediaFactory->new();
  119. $factory->set_launch( join( ' ', @pipeline ) );
  120. $factory->set_shared(TRUE);
  121. #say "media ($device): " . $factory->get_launch();
  122. # $factory->set_latency(5);
  123. #say "latency ($device): " . $factory->get_latency();
  124. return $factory;
  125. }
  126. Gst::init( [ $0, @ARGV ] );
  127. my $loop = Glib::MainLoop->new( undef, FALSE );
  128. # create a server instance
  129. my $server = Gst::RTSPServer->new();
  130. $server->set_address($ADDRESS);
  131. $server->set_service($PORT);
  132. # get the mount points for this server, every server has a default
  133. # object that be used to map uri mount points to media factories
  134. my $mounts = $server->get_mount_points();
  135. # attach media to URIs
  136. my @mounts;
  137. for my $i ( 0 .. $#VDEVICES ) {
  138. my $mount = "/cam$i";
  139. $mounts->add_factory(
  140. $mount,
  141. factory( cam( $VDEVICES[$i], $nextpayload++ ) )
  142. );
  143. push @mounts, $mount;
  144. }
  145. for my $i ( 0 .. $#ADEVICES ) {
  146. my $mount = "/mic$i";
  147. $mounts->add_factory(
  148. $mount,
  149. factory( mic( $ADEVICES[$i], $nextpayload++ ) )
  150. );
  151. push @mounts, $mount;
  152. }
  153. if ( @ADEVICES and @VDEVICES ) {
  154. my $mount = "/main";
  155. $mounts->add_factory(
  156. $mount,
  157. factory(
  158. mic( $ADEVICES[0], $#VDEVICES + 1 ),
  159. cam( $VDEVICES[0], 0 )
  160. )
  161. );
  162. push @mounts, $mount;
  163. }
  164. # don't need the ref to the mapper anymore
  165. undef $mounts;
  166. # attach the server to the default maincontext
  167. my $retval = $server->attach(undef);
  168. # start serving
  169. say "streams ready at the following URLs:";
  170. for (@mounts) {
  171. say "rtsp://$ADDRESS:$PORT$_";
  172. }
  173. $loop->run;