{"id":256184,"date":"2024-02-01T11:07:02","date_gmt":"2024-02-01T03:07:02","guid":{"rendered":"https:\/\/blog.zhenglei.net\/?p=256184"},"modified":"2024-02-01T11:35:58","modified_gmt":"2024-02-01T03:35:58","slug":"256184","status":"publish","type":"post","link":"https:\/\/blog.zhenglei.net\/?p=256184","title":{"rendered":"Live Streaming with Android Handset"},"content":{"rendered":"\n<h1 class=\"wp-block-heading has-text-align-center\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-blue-color\">Create your own streaming server with nginx<\/mark><\/h1>\n\n\n\n<p><\/p>\n\n\n\n<p>FWD:   <a href=\"https:\/\/camonlivestreaming.jimdofree.com\/2021\/12\/28\/create-your-own-streaming-server-with-nginx\/\">CamOn Living Streaming<\/a><\/p>\n\n\n\n\n\n<p><em><strong>Note:    Have been verified  on Redmi-6\/MiUi 14,  and  openresty + RTMP Module + Linux<\/strong><\/em><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>We saw how to setup a streaming server with MistServer in <a href=\"https:\/\/camonlivestreaming.jimdofree.com\/2021\/12\/13\/create-your-own-streaming-server-with-mistserver\/\">this post<\/a>, let&#8217;s see how to do the same with <a href=\"https:\/\/nginx.org\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>nginx<\/strong><\/a>.<\/p>\n\n\n\n<p>nginx, pronounced &#8220;<em>engine X<\/em>&#8220;, is a web server that can also be used as a reverse proxy, load balancer, mail proxy, HTTP cache and, why not, RTMP server.&nbsp;It is free and open-source software, released under the terms of the <a href=\"https:\/\/en.wikipedia.org\/wiki\/2-clause_BSD\" target=\"_blank\" rel=\"noreferrer noopener\">2-clause BSD<\/a> license.<\/p>\n\n\n\n<p>For the purpose of this trial, we will see how to install and configure the server on a Raspberry Pi board running&nbsp;<strong>Raspberry Pi OS Lite<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824098511\">Install nginx with&nbsp;RTMP support<\/h3>\n\n\n\n<p>First, we must install the server and an add-on module that will allow it to handle&nbsp;the RTMP protocol. sudo apt install nginxsudo apt install libnginx-mod-rtmp<\/p>\n\n\n\n<p>After the installation is complete, we should be able to reach the welcome page simply by entering the IP address of the server in our favorite browser,&nbsp;http:\/\/192.168.1.18\/ for us.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"554\" height=\"218\" src=\"https:\/\/blog.zhenglei.net\/wp-content\/uploads\/2024\/02\/image-1.png\" alt=\"\" class=\"wp-image-256186\" srcset=\"\/wp-content\/uploads\/2024\/02\/image-1.png 554w, \/wp-content\/uploads\/2024\/02\/image-1-300x118.png 300w, \/wp-content\/uploads\/2024\/02\/image-1-500x197.png 500w\" sizes=\"auto, (max-width: 554px) 100vw, 554px\" \/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824099411\">Configure the RTMP server<\/h3>\n\n\n\n<p>The way nginx and its modules work is determined in the configuration file. By default, the configuration file is named <em>nginx.conf<\/em> and placed in the directory <em>\/etc\/nginx<\/em>. For details, please check out the&nbsp;<a href=\"https:\/\/nginx.org\/en\/docs\/beginners_guide.html\" target=\"_blank\" rel=\"noreferrer noopener\">Beginner\u2019s Guide<\/a>&nbsp;and other resources available in the&nbsp;<a href=\"https:\/\/nginx.org\/en\/docs\/\" target=\"_blank\" rel=\"noreferrer noopener\">nginx documentation<\/a>.<\/p>\n\n\n\n<p>To enable the RTMP protocol, edit the configuration file sudo nano \/etc\/nginx\/nginx.conf<\/p>\n\n\n\n<p>then add these few lines at the very end <strong>#<\/strong> protocol imap; <strong>#<\/strong> proxy on; <strong>#<\/strong> } <strong>#<\/strong>}<br>rtmp { server { listen 1935; application live { live on;<br>hls on; hls_path \/tmp\/hls; } } }<\/p>\n\n\n\n<p>finally save the file and restart the server so that the new configuration will be loaded sudo nginx -s reload<\/p>\n\n\n\n<p>In this example, we are configuring the RTMP server to <strong>listen on the port 1935<\/strong> (the default RTMP port), and to handle an <strong>application named <em>live<\/em><\/strong>. This application has the <strong>live mode<\/strong> (one-to-many broadcasting) enabled. The <strong>HLS output<\/strong> is also enabled, the playlist and the fragments will be saved in&nbsp;<em>\/tmp\/hls<\/em>&nbsp;(if the directory does not exist it will be created).<\/p>\n\n\n\n<p>The complete reference about the available&nbsp;RTMP directives can be found <a href=\"https:\/\/github.com\/arut\/nginx-rtmp-module\/wiki\/Directives\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824108811\">Configure the HTTP server<\/h3>\n\n\n\n<p>We need to configure the HTTP server so that it can access&nbsp;the files in <em>\/tmp\/hls<\/em>&nbsp;for clients to play HLS. nginx uses the so called <em>Server Blocks<\/em> to serve multiple sites in parallel, let&#8217;s change the configuration of the default one sudo nano \/etc\/nginx\/sites-enabled\/default<\/p>\n\n\n\n<p>by adding a new <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_core_module.html#location\" target=\"_blank\" rel=\"noreferrer noopener\">location<\/a> entry according to the&nbsp;<a href=\"https:\/\/github.com\/arut\/nginx-rtmp-module\/wiki\/Directives#hls-1\" target=\"_blank\" rel=\"noreferrer noopener\">documentation<\/a> location \/ { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri\/ =404; } location \/hls { types { application\/vnd.apple.mpegurl m3u8; } root \/tmp; add_header Cache-Control no-cache; add_header Access-Control-Allow-Origin *; } # pass PHP scripts to FastCGI server #<\/p>\n\n\n\n<p>then save the file and restart the server once again sudo nginx -s reload<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824126511\">Configure the app<\/h3>\n\n\n\n<p>From CamON Live Streaming app settings, enable the&nbsp;<strong><em>Live streaming<\/em><\/strong>&nbsp;adapter and configure it<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>in he&nbsp;<em><strong>Server<\/strong><\/em>&nbsp;field, specify the RTMP URL for the application we configured, rtmp:\/\/192.168.1.18\/live in this example<\/li>\n\n\n\n<li>in the <strong><em>Stream<\/em><\/strong> field enter a streaming key of your choice, let&#8217;s use <em>spynet<\/em><\/li>\n<\/ul>\n\n\n\n<p><strong>TIP:<\/strong> the streaming key will be used by nginx as the base name for the HLS files<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:\"><img loading=\"lazy\" decoding=\"async\" width=\"360\" height=\"640\" src=\"https:\/\/blog.zhenglei.net\/wp-content\/uploads\/2024\/02\/image-2.png\" alt=\"\" class=\"wp-image-256187\" srcset=\"\/wp-content\/uploads\/2024\/02\/image-2.png 360w, \/wp-content\/uploads\/2024\/02\/image-2-169x300.png 169w\" sizes=\"auto, (max-width: 360px) 100vw, 360px\" \/><\/a><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:\"><img loading=\"lazy\" decoding=\"async\" width=\"360\" height=\"640\" src=\"https:\/\/blog.zhenglei.net\/wp-content\/uploads\/2024\/02\/image.png\" alt=\"\" class=\"wp-image-256185\" srcset=\"\/wp-content\/uploads\/2024\/02\/image.png 360w, \/wp-content\/uploads\/2024\/02\/image-169x300.png 169w\" sizes=\"auto, (max-width: 360px) 100vw, 360px\" \/><\/a><\/figure>\n\n\n\n<p>To start the stream use the arrow icon in the bottom-right corner of the main screen. By tapping on it a countdown will be shown, at the end of which the device will connect to&nbsp;nginx.<\/p>\n\n\n\n<p><strong>TIP:<\/strong> during the countdown, tap on the arrow again if you wish to abort<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:\"><img decoding=\"async\" src=\"https:\/\/image.jimcdn.com\/app\/cms\/image\/transf\/none\/path\/sb59bda039175c93d\/image\/i9ea9860d1436cab6\/version\/1640774445\/image.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:\"><img decoding=\"async\" src=\"https:\/\/image.jimcdn.com\/app\/cms\/image\/transf\/none\/path\/sb59bda039175c93d\/image\/i6fe6f17b6b4f6c8b\/version\/1640774445\/image.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824131611\">Let&#8217;s see it in action<\/h3>\n\n\n\n<p>To verify that everything is working as expected, we can use VLC as the client to see the nginx broadcast.<\/p>\n\n\n\n<p>It is possible to see the <strong>HLS output<\/strong>&nbsp;using the URL <strong>http:\/\/192.168.1.18\/hls\/spynet.m3u8<\/strong>, where <em>hls<\/em> is the location we&nbsp;configured for the HTTP server to find the files and <em>spynet<\/em> is the streaming key we have chosen.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:;\"><img decoding=\"async\" src=\"https:\/\/image.jimcdn.com\/app\/cms\/image\/transf\/dimension=520x10000:format=png\/path\/sb59bda039175c93d\/image\/i5b49f095c00ba9d5\/version\/1640776041\/image.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p>It is also possible to see the <strong>RTMP<\/strong> <strong>output<\/strong>&nbsp;using the URL&nbsp;<strong>rtmp:\/\/192.168.1.18\/live\/spynet<\/strong>, where <em>live<\/em> is the name of the application we configured and <em>spynet<\/em> is the streaming key.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"javascript:;\"><img decoding=\"async\" src=\"https:\/\/image.jimcdn.com\/app\/cms\/image\/transf\/dimension=518x10000:format=png\/path\/sb59bda039175c93d\/image\/i0cd134d1c4077ca3\/version\/1640776189\/image.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824141811\">Embed the player<\/h3>\n\n\n\n<p>For a better user experience, we may want to embed the player in our web page, This way the broadcast will be available with no extra effort. As the player, <a href=\"https:\/\/videojs.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">Video.js<\/a> is a good choice to see the HLS broadcast.<\/p>\n\n\n\n<p>Let&#8217;s create our <em>index.html<\/em> page in&nbsp;<em>\/var\/www\/html<\/em> sudo nano \/var\/www\/html\/index.html<\/p>\n\n\n\n<p>with the following HTML code<\/p>\n\n\n\n<p><strong>TIP:<\/strong> the key point is to set the correct source,&nbsp;<em>src=&#8221;\/hls\/spynet.m3u8&#8243;<\/em>, as described above<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td>1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32<\/td><td>&lt;!DOCTYPE html&gt; <strong>&lt;html<\/strong> lang=&#8221;en&#8221;<strong>&gt;<\/strong> <strong>&lt;head&gt;<\/strong> <strong>&lt;link<\/strong> href=&#8221;https:\/\/vjs.zencdn.net\/7.17.0\/video-js.css&#8221; rel=&#8221;stylesheet&#8221; <strong>\/&gt;<\/strong> <em>&lt;!&#8211; If you&#8217;d like to support IE8 (for Video.js versions prior to v7) &#8211;&gt;<\/em> <strong>&lt;script <\/strong>src=&#8221;https:\/\/vjs.zencdn.net\/ie8\/1.1.2\/videojs-ie8.min.js&#8221;<strong>&gt;&lt;\/script&gt;<\/strong> <strong>&lt;\/head&gt;<\/strong> <strong>&lt;body&gt;<\/strong> <strong>&lt;h1&gt;<\/strong>My nginx streaming server<strong>&lt;\/h1&gt;<\/strong> <strong>&lt;video<\/strong> id=&#8221;my-video&#8221; class=&#8221;video-js&#8221; controls preload=&#8221;auto&#8221; width=&#8221;640&#8243; height=&#8221;360&#8243; data-setup=&#8221;{}&#8221; <strong>&gt;<\/strong> <strong>&lt;source<\/strong> src=&#8221;\/hls\/spynet.m3u8&#8243; type=&#8221;application\/vnd.apple.mpegurl m3u8&#8243; <strong>\/&gt;<\/strong> <strong>&lt;p<\/strong> class=&#8221;vjs-no-js&#8221;<strong>&gt;<\/strong> To view this video please enable JavaScript, and consider upgrading to a web browser that <strong>&lt;a<\/strong> href=&#8221;https:\/\/videojs.com\/html5-video-support\/&#8221; target=&#8221;_blank&#8221;<strong>&gt;<\/strong> supports HTML5 video <strong>&lt;\/a&gt;<\/strong> <strong>&lt;\/p&gt;<\/strong> <strong>&lt;\/video&gt;<\/strong> <strong>&lt;script <\/strong>src=&#8221;https:\/\/vjs.zencdn.net\/7.17.0\/video.js&#8221;<strong>&gt;&lt;\/script&gt;<\/strong> <strong>&lt;\/body&gt;<\/strong> <strong>&lt;\/html&gt;<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>After the file has been saved (no need to restart the server), by navigating to address of the server,&nbsp;http:\/\/192.168.1.18\/, we can see the new homepage in action<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/image.jimcdn.com\/app\/cms\/image\/transf\/none\/path\/sb59bda039175c93d\/image\/i1206e9e76f8b848b\/version\/1640798159\/image.png\" alt=\"\"\/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cc-m-header-7824191011\">Some small tweaks<\/h3>\n\n\n\n<p>Since we are planning to broadcast our video over the Internet, we should make the server publicly reachable. To keep it simple, we should consider setting up port forwarding and the Dynamic DNS as described in this <a href=\"https:\/\/camonlivestreaming.jimdofree.com\/2021\/12\/13\/create-your-own-streaming-server-with-mistserver#port-forward\" target=\"_blank\" rel=\"noreferrer noopener\">post<\/a>.<\/p>\n\n\n\n<p>In summary, if the router supports the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Universal_Plug_and_Play\" target=\"_blank\" rel=\"noreferrer noopener\">UPnP<\/a> protocol, we can use the command line utility <a href=\"https:\/\/www.nat32.com\/v2\/upnpc.htm\" target=\"_blank\" rel=\"noreferrer noopener\">upnpc<\/a> to forward the HTTP port directly from the server. If not, we can manually configure the router. sudo apt install miniupnpcupnpc -a <em>server_ip<\/em><em>server_port<\/em><em>external_port<\/em> tcpupnpc -a 192.168.1.18 80 8282 tcp<\/p>\n\n\n\n<p>This way the server will be reachable from anywhere at http:\/\/<em>public_ip_address<\/em>:8282\/ or http:\/\/<em>myserver.dyndns.org<\/em>:8282\/.<\/p>\n\n\n\n<p>As discussed, HLS is continuously writes files to disk while updating the playlist and the fragments. This consumes resources and can dramatically reduce the life of the SD card used by the server as storage. A better solution is to use a ramdisk to temporary store those files.<\/p>\n\n\n\n<p>Examine the available memory to find out how much we can use. free -h<\/p>\n\n\n\n<p>Examine the typical HLS disk usage to find out how much memory we expect to need. sudo du -sh \/tmp\/hls\/<\/p>\n\n\n\n<p>Create a folder where to mount the ramdisk. sudo mkdir -p \/mnt\/ramdisk<\/p>\n\n\n\n<p>Add an entry to fstab to configure the ramdisk (50M is enough for this example). sudo nano \/etc\/fstabproc \/proc proc defaults 0 0 PARTUUID=4b551375-01 \/boot vfat defaults 0 2 PARTUUID=4b551375-02 \/ ext4 defaults,noatime 0 1 tmpfs \/mnt\/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=50M 0 0<\/p>\n\n\n\n<p>Reboot the server. sudo reboot<\/p>\n\n\n\n<p>Verify that the ramdisk was mounted. sudo df -hFilesystem Size Used Avail Use% Mounted on \/dev\/root 3.4G 1.7G 1.6G 52% \/ devtmpfs 87M 0 87M 0% \/dev tmpfs 215M 0 215M 0% \/dev\/shm tmpfs 86M 632K 86M 1% \/run tmpfs 5.0M 4.0K 5.0M 1% \/run\/lock tmpfs 50M 0 50M 0% \/mnt\/ramdisk \/dev\/mmcblk0p1 253M 49M 204M 20% \/boot tmpfs 43M 0 43M 0% \/run\/user\/1000<\/p>\n\n\n\n<p>Change the nginx configuration so that HLS files will be saved in <em>\/mnt\/ramdisk\/hls<\/em> instead of in <em>\/tmp\/hls<\/em>. sudo nano \/etc\/nginx\/nginx.confrtmp { server { listen 1935; application live { live on; hls on; hls_path \/mnt\/ramdisk\/hls; } } }<\/p>\n\n\n\n<p>Change the nginx configuration so that the HTTP server will know where to find the HLS files. sudo nano \/etc\/nginx\/sites-enabled\/default location \/hls { types { application\/vnd.apple.mpegurl m3u8; } root \/mnt\/ramdisk; add_header Cache-Control no-cache; add_header Access-Control-Allow-Origin *; }<\/p>\n\n\n\n<p>Restart the server. sudo nginx -s reload<\/p>\n\n\n\n<p>Verify that the ramdisk is now used. sudo df -hFilesystem Size Used Avail Use% Mounted on \/dev\/root 3.4G 1.7G 1.6G 52% \/ devtmpfs 87M 0 87M 0% \/dev tmpfs 215M 0 215M 0% \/dev\/shm tmpfs 86M 632K 86M 1% \/run tmpfs 5.0M 4.0K 5.0M 1% \/run\/lock tmpfs 50M 12M 39M 24% \/mnt\/ramdisk \/dev\/mmcblk0p1 253M 49M 204M 20% \/boot tmpfs 43M 0 43M 0% \/run\/user\/1000<\/p>\n\n\n\n<p>Your server should now run much smoother!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Create your own streaming server with ng &hellip; <a href=\"https:\/\/blog.zhenglei.net\/?p=256184\">\u7ee7\u7eed\u9605\u8bfb <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"quote","meta":{"footnotes":""},"categories":[193,6,12],"tags":[58,407,73],"class_list":["post-256184","post","type-post","status-publish","format-quote","hentry","category-cloud-2","category-internet","category-streaming","tag-android-2","tag-camon","tag-linux-2","post_format-post-format-quote"],"_links":{"self":[{"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/posts\/256184","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=256184"}],"version-history":[{"count":3,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/posts\/256184\/revisions"}],"predecessor-version":[{"id":256194,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=\/wp\/v2\/posts\/256184\/revisions\/256194"}],"wp:attachment":[{"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=256184"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=256184"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.zhenglei.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=256184"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}