summaryrefslogtreecommitdiff
path: root/bin/capture+encode+serve
blob: 40615955f069421d6822dd4200d67946f0be42e6 (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 = 288;
  37. my $FRAMERATE = 25;
  38. my $AUDIORATE = 48000;
  39. my $RATIO_NUM = 4;
  40. my $RATIO_DEN = 3;
  41. # * bitrates and bits in parens based on https://developer.apple.com/library/content/documentation/General/Reference/HLSAuthoringSpec/Requirements.html
  42. # + bits rounded up to include nearby modulo 16 formats
  43. # + best (i.e. high-modulo) 16:9 heights: 216 288 360 432 576 720
  44. # + best (i.e. high-modulo) 4:3 heights: 288 312 384 480 624 816
  45. # * speeds tuned to just below 100% cpu usage for each combination on a multi-core computer
  46. # TODO: Externalize speeds to site-specific configfile
  47. my ($VBITRATE, $SPEED_X264, $SPEED_X264_ALONE, $SPEED_VP8,
  48. $SPEED_VP8_ALONE
  49. );
  50. $RATIO_NUM ||= 16;
  51. $RATIO_DEN ||= 9;
  52. my $WIDTH ||= $HEIGHT * $RATIO_NUM / $RATIO_DEN;
  53. my $BITS = $WIDTH * $HEIGHT;
  54. if ( $BITS le 110592 ) { # 234p → 97344
  55. $VBITRATE = 145000;
  56. $SPEED_X264 = 'fast';
  57. $SPEED_X264_ALONE = 'fast';
  58. $SPEED_VP8 = 3;
  59. $SPEED_VP8_ALONE = 2;
  60. }
  61. elsif ( $BITS le 150528 ) { # 270p → 129600
  62. $VBITRATE = 365000;
  63. $SPEED_X264 = 'faster';
  64. $SPEED_X264_ALONE = 'fast';
  65. $SPEED_VP8 = 4;
  66. $SPEED_VP8_ALONE = 2;
  67. }
  68. elsif ( $BITS le 196608 ) { # 360p → 172800
  69. $VBITRATE = 730000;
  70. $SPEED_X264 = 'veryfast';
  71. $SPEED_X264_ALONE = 'fast';
  72. $SPEED_VP8 = 5;
  73. $SPEED_VP8_ALONE = 3;
  74. }
  75. elsif ( $BITS le 331776 ) { # 432p → 331776
  76. $VBITRATE = 1100000;
  77. $SPEED_X264 = 'ultrafast';
  78. $SPEED_X264_ALONE = 'fast';
  79. $SPEED_VP8 = 8;
  80. $SPEED_VP8_ALONE = 4;
  81. }
  82. elsif ( $BITS le 589824 ) { # 540p → 518400
  83. $VBITRATE = 2000000;
  84. $SPEED_X264 = 'toofast';
  85. $SPEED_X264_ALONE = 'veryfast';
  86. $SPEED_VP8_ALONE = 5;
  87. }
  88. elsif ( $BITS le 921600 ) { # 720p → 921600
  89. $VBITRATE = 3000000;
  90. $SPEED_X264 = 'toofast';
  91. $SPEED_X264_ALONE = 'ultrafast';
  92. $SPEED_VP8_ALONE = 15;
  93. }
  94. die "Not enough CPU - reduce size or streams"
  95. if ( $SPEED_X264 eq 'toofast' );
  96. # TODO: implement codec-specific height pools
  97. #if ( toofast == $SPEED_X264 ) { @HEIGHTS_MPEG = () };
  98. #unless (@HEIGHTS_MPEG) { $SPEED_VP8 = $SPEED_VP8_ALONE }
  99. #unless (@HEIGHTS_WEBM) { $SPEED_X264 = $SPEED_X264_ALONE }
  100. my $VCAPS = "video/x-raw,height=$HEIGHT";
  101. my $ACAPS = "audio/x-raw,rate=$AUDIORATE,channels=2,depth=16";
  102. # * http://stackoverflow.com/a/42237307
  103. my $ABUFFERS = 20000;
  104. # * force threads using queues - see http://stackoverflow.com/a/30738533
  105. # * generous queue sizes inspired by https://wiki.xiph.org/GST_cookbook
  106. my $QUEUE = "queue max-size-bytes=100000000 max-size-time=0";
  107. my %PIPELINE = (
  108. AMR => {
  109. AENC => [ 'amrnbenc', $QUEUE, 'rtpamrpay' ],
  110. },
  111. H264 => {
  112. # * let x264 use low-latency sliced-threads (i.e. don't disable treads)
  113. VENC => [
  114. "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\"",
  115. 'video/x-h264,profile=baseline',
  116. $QUEUE,
  117. 'rtph264pay',
  118. ],
  119. },
  120. OPUS => {
  121. AENC => [ 'opusenc', $QUEUE, 'rtpopuspay' ],
  122. },
  123. VP8 => {
  124. VENC => [
  125. "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",
  126. 'video/x-vp8',
  127. $QUEUE,
  128. 'rtpvp8pay',
  129. ],
  130. },
  131. RAW => {
  132. AENC => ['rtpL16pay'],
  133. VENC => ['rtpvrawpay'],
  134. },
  135. );
  136. our $nextpayload = 0;
  137. sub cam
  138. {
  139. my ( $device, $payload ) = @_;
  140. my $factory = Gst::RTSPMediaFactory->new();
  141. my $pipeline = join(
  142. ' ! ',
  143. ( "v4l2src device=$device",
  144. $QUEUE,
  145. 'videoconvert',
  146. $VCAPS,
  147. $QUEUE,
  148. @{ $PIPELINE{$VFORMAT}{'VENC'} },
  149. )
  150. );
  151. return "( $pipeline name=pay$payload )";
  152. }
  153. sub mic
  154. {
  155. my ( $device, $payload ) = @_;
  156. my $factory = Gst::RTSPMediaFactory->new();
  157. my $pipeline = join(
  158. ' ! ',
  159. ( "alsasrc device=$device buffer-time=$ABUFFERS",
  160. $QUEUE,
  161. 'audioconvert',
  162. $QUEUE,
  163. @{ $PIPELINE{$AFORMAT}{'AENC'} },
  164. )
  165. );
  166. return "( $pipeline name=pay$payload )";
  167. }
  168. sub factory
  169. {
  170. my @pipeline = @_;
  171. my $factory = Gst::RTSPMediaFactory->new();
  172. $factory->set_launch( join( ' ', @pipeline ) );
  173. $factory->set_shared(TRUE);
  174. #say "media ($device): " . $factory->get_launch();
  175. # $factory->set_latency(5);
  176. #say "latency ($device): " . $factory->get_latency();
  177. return $factory;
  178. }
  179. Gst::init( [ $0, @ARGV ] );
  180. my $loop = Glib::MainLoop->new( undef, FALSE );
  181. # create a server instance
  182. my $server = Gst::RTSPServer->new();
  183. $server->set_address($ADDRESS);
  184. $server->set_service($PORT);
  185. # get the mount points for this server, every server has a default
  186. # object that be used to map uri mount points to media factories
  187. my $mounts = $server->get_mount_points();
  188. # attach media to URIs
  189. my @mounts;
  190. for my $i ( 0 .. $#VDEVICES ) {
  191. my $mount = "/cam$i";
  192. $mounts->add_factory(
  193. $mount,
  194. factory( cam( $VDEVICES[$i], $nextpayload++ ) )
  195. );
  196. push @mounts, $mount;
  197. }
  198. for my $i ( 0 .. $#ADEVICES ) {
  199. my $mount = "/mic$i";
  200. $mounts->add_factory(
  201. $mount,
  202. factory( mic( $ADEVICES[$i], $nextpayload++ ) )
  203. );
  204. push @mounts, $mount;
  205. }
  206. if ( @ADEVICES and @VDEVICES ) {
  207. my $mount = "/main";
  208. $mounts->add_factory(
  209. $mount,
  210. factory(
  211. mic( $ADEVICES[0], $#VDEVICES + 1 ),
  212. cam( $VDEVICES[0], 0 )
  213. )
  214. );
  215. push @mounts, $mount;
  216. }
  217. # don't need the ref to the mapper anymore
  218. undef $mounts;
  219. # attach the server to the default maincontext
  220. my $retval = $server->attach(undef);
  221. # start serving
  222. say "streams ready at the following URLs:";
  223. for (@mounts) {
  224. say "rtsp://$ADDRESS:$PORT$_";
  225. }
  226. $loop->run;