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