网络知识

利用nginx-http-flv-module搭建视频点播服务(非Docker安装)

视频点播服务架构

这个架构在IOS端利用,LFliveKit推流到nginx服务器,生成flv文件。直播结束然后利用yamid对FLV文件加关键帧,并把加工后的文件在ngnix中,作为静态资源公开出来。在web端html中利用bilibili .js来播放flv视频。

1.下载nginx和nginx-http-flv-module最新代码。

nginx官网:http://nginx.org/en/download.html
nginx-http-flv-module:https://github.com/winshining/nginx-http-flv-module

2. 开始安装nginx 和 nginx-http-flv-module

1.下载nginx源码
  wget http://nginx.org/download/nginx-1.19.6.tar.gz

2.下载nginx-http-flv-module
  git clone https://github.com/winshining/nginx-http-flv-module.git
  这里需要git, 如果没安装git的话,可以先npm install git

3.解压	
  tar -zxvf nginx-1.19.6.tar.gz

4.进入nginx目录
  cd nginx-1.19.6

5.编译配置
  ./configure --add-module=/tmp/ngnix_install/nginx-http-flv-module --with-http_mp4_module --with-http_flv_module

6.安装
  make
  make install

编译过程中,可能会提示错误。比如:

./configure: error: the HTTP rewrite module requires the PCRE library.

解决办法:https://www.cnblogs.com/moonhmily/p/11456720.html

#安装 pcre-devel 与 openssl-devel

yum -y install pcre-devel openssl openssl-devel

nginx会安装到 /usr/local/nginx 目录下。加入全局变量,修改”/etc/profile”文件:

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/nginx/sbin

执行如下命令,使全局变量生效:

source /etc/profile

修改nginx配置文件/usr/local/nginx/conf/nginx.conf,内容如下:

daemon off;

error_log /tmp/nginx/logs/error.log debug;

events {
    worker_connections 1024;
}

rtmp {

    out_queue   4096;
    out_cork    8;
    max_streams 64;

    server {
        listen 1935;

        application rtmplive {
            live on;
            record all;
            # 非常重要, 设定让ngnix断开阻塞中的连接, 才能触发exec_record_done
            # 以及客户端的LFLiveKit reconnectCount自动重新连接才会好用
            drop_idle_publisher 5s;
            record_path /tmp/video;
            record_max_size 10485760K;
            record_unique off;
            record_append on;
            gop_cache on;
            allow play all;
            meta off;
            # record完成时,利用yamdi给flv视频加关键帧,这样就可以拖拽播放flv了,yamdi需要预先安装。
            exec_record_done bash -c "yamdi -i /tmp/video/${name}.flv -o /tmp/video/convert/${name}.flv";
            # record完成时,利用ffmpeg把flv转成MP4。
            exec_record_done bash -c "ffmpeg -i /tmp/video/${name}.flv /tmp/video/${name}.mp4";
        }

    }
}

http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    gzip on;
    access_log /tmp/nginx/logs/access.log;

    server {
        listen 8080;

        location /live {
            flv_live on; #open flv live streaming (subscribe)
            chunked_transfer_encoding  off; #open 'Transfer-Encoding: chunked' response
            add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
            add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
        }

        location ~ \.flv$ {
           add_header 'Access-Control-Allow-Origin' '*'; #添加额外的HTTP头
           add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的HTTP头
           flv;
           root /tmp/video/;
        }
    }
}

启动nginx服务:

# 停止服务
nginx -s stop
# 启动服务
nginx

附:Web Html 代码

<!DOCTYPE html>
<html>

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <meta http-equiv="Access-Control-Allow-Origin" content="*">
    <title>flv.js demo</title>

    <style>
        .mainContainer {
            display: block;
            width: 1024px;
            margin-left: auto;
            margin-right: auto;
        }

        .urlInput {
            display: block;
            width: 100%;
            margin-left: auto;
            margin-right: auto;
            margin-top: 8px;
            margin-bottom: 8px;
        }

        .centeredVideo {
            display: block;
            width: 100%;
            height: 576px;
            margin-left: auto;
            margin-right: auto;
            margin-bottom: auto;
        }

        .controls {
            display: block;
            width: 100%;
            text-align: left;
            margin-left: auto;
            margin-right: auto;
            margin-top: 8px;
            margin-bottom: 10px;
        }

        .logcatBox {
            border-color: #CCCCCC;
            font-size: 11px;
            font-family: Menlo, Consolas, monospace;
            display: block;
            width: 100%;
            text-align: left;
            margin-left: auto;
            margin-right: auto;
        }
    </style>

  <script>(window.BOOMR_mq=window.BOOMR_mq||[]).push(["addVar",{"rua.upush":"false","rua.cpush":"false","rua.upre":"false","rua.cpre":"false","rua.uprl":"false","rua.cprl":"false","rua.cprf":"false","rua.trans":"SJ-5e36293a-d34b-446f-a17d-140c847d00ce","rua.cook":"true","rua.ims":"false","rua.ufprl":"false","rua.cfprl":"true"}]);</script>
                          <script>!function(e){var n="https://s.go-mpulse.net/boomerang/";if("False"=="True")e.BOOMR_config=e.BOOMR_config||{},e.BOOMR_config.PageParams=e.BOOMR_config.PageParams||{},e.BOOMR_config.PageParams.pci=!0,n="https://s2.go-mpulse.net/boomerang/";if(window.BOOMR_API_key="8WMGS-R456H-64GN8-4FVQL-CTDMX",function(){function e(){if(!o){var e=document.createElement("script");e.id="boomr-scr-as",e.src=window.BOOMR.url,e.async=!0,i.parentNode.appendChild(e),o=!0}}function t(e){o=!0;var n,t,a,r,d=document,O=window;if(window.BOOMR.snippetMethod=e?"if":"i",t=function(e,n){var t=d.createElement("script");t.id=n||"boomr-if-as",t.src=window.BOOMR.url,BOOMR_lstart=(new Date).getTime(),e=e||d.body,e.appendChild(t)},!window.addEventListener&&window.attachEvent&&navigator.userAgent.match(/MSIE [67]\./))return window.BOOMR.snippetMethod="s",void t(i.parentNode,"boomr-async");a=document.createElement("IFRAME"),a.src="about:blank",a.title="",a.role="presentation",a.loading="eager",r=(a.frameElement||a).style,r.width=0,r.height=0,r.border=0,r.display="none",i.parentNode.appendChild(a);try{O=a.contentWindow,d=O.document.open()}catch(c){n=document.domain,a.src="javascript:var d=document.open();d.domain='"+n+"';void(0);",O=a.contentWindow,d=O.document.open()}if(n)d._boomrl=function(){this.domain=n,t()},d.write("<bo"+"dy onload='document._boomrl();'>");else if(O._boomrl=function(){t()},O.addEventListener)O.addEventListener("load",O._boomrl,!1);else if(O.attachEvent)O.attachEvent("onload",O._boomrl);d.close()}function a(e){window.BOOMR_onload=e&&e.timeStamp||(new Date).getTime()}if(!window.BOOMR||!window.BOOMR.version&&!window.BOOMR.snippetExecuted){window.BOOMR=window.BOOMR||{},window.BOOMR.snippetStart=(new Date).getTime(),window.BOOMR.snippetExecuted=!0,window.BOOMR.snippetVersion=12,window.BOOMR.url=n+"8WMGS-R456H-64GN8-4FVQL-CTDMX";var i=document.currentScript||document.getElementsByTagName("script")[0],o=!1,r=document.createElement("link");if(r.relList&&"function"==typeof r.relList.supports&&r.relList.supports("preload")&&"as"in r)window.BOOMR.snippetMethod="p",r.href=window.BOOMR.url,r.rel="preload",r.as="script",r.addEventListener("load",e),r.addEventListener("error",function(){t(!0)}),setTimeout(function(){if(!o)t(!0)},3e3),BOOMR_lstart=(new Date).getTime(),i.parentNode.appendChild(r);else t(!1);if(window.addEventListener)window.addEventListener("load",a,!1);else if(window.attachEvent)window.attachEvent("onload",a)}}(),"".length>0)if(e&&"performance"in e&&e.performance&&"function"==typeof e.performance.setResourceTimingBufferSize)e.performance.setResourceTimingBufferSize();!function(){if(BOOMR=e.BOOMR||{},BOOMR.plugins=BOOMR.plugins||{},!BOOMR.plugins.AK){var n="true"=="true"?1:0,t="cookiepresent",a="mxt2zrqxyzslgyagkhdq-f-305d4396d-clientnsv4-s.akamaihd.net",i={"ak.v":"30","ak.cp":"757416","ak.ai":parseInt("492561",10),"ak.ol":"0","ak.cr":139,"ak.ipv":4,"ak.proto":"h2","ak.rid":"d007a7","ak.r":20734,"ak.a2":n,"ak.m":"","ak.n":"essl","ak.bpcip":"101.231.172.0","ak.cport":39331,"ak.gh":"23.32.20.70","ak.quicv":"","ak.tlsv":"tls1.3","ak.0rtt":"","ak.csrc":"-","ak.acc":"","ak.t":"1611026887","ak.ak":"hOBiQwZUYzCg5VSAfCLimQ==+n1cMpzO/R/fScTtk0hQtDJwRIm9ZkgByEMOknM4Rz1Q5T6RXrm9/tQ6M6TOP1vRjMbWZzBQhouDArhC/knPX6b9UmEZjzuwoj+nEgDPDP3WY/xMAugQIPPMhkXjSikS1w+KpeoMX8E6Dzko5axPQF8clQL27wu+rvBhw3zW+lwF2bm1rz2C5B9dCrJiJAHx4xXnxoKH3JJqClNi1jt5vpMjCnLm9lW19bEEmAicxZBAhUpdEGyAINM9iAgeIhrpMNwUaFgEjgKtFSPsEoiFl8WvCpMCWx4MlIdciSWZyFx1/1pVseY+VBrmC92KcGUeSNJcnE0zPpo6uGzZK/RPRC7XOM+YZueR0zowq+SqDRsTPWDwzQw7+24u7LTo7hfcdtwtW9qUQxGR2YIY/P8QDhT2fscX5qJ6kRPfi4EShGo=","ak.pv":"150","ak.dpoabenc":""};if(""!==t)i["ak.ruds"]=t;var o={i:!1,av:function(n){var t="http.initiator";if(n&&(!n[t]||"spa_hard"===n[t]))i["ak.feo"]=void 0!==e.aFeoApplied?1:0,BOOMR.addVar(i)},rv:function(){var e=["ak.bpcip","ak.cport","ak.cr","ak.csrc","ak.gh","ak.ipv","ak.m","ak.n","ak.ol","ak.proto","ak.quicv","ak.tlsv","ak.0rtt","ak.r","ak.acc","ak.t"];BOOMR.removeVar(e)}};BOOMR.plugins.AK={akVars:i,akDNSPreFetchDomain:a,init:function(){if(!o.i){var e=BOOMR.subscribe;e("before_beacon",o.av,null,null),e("onbeacon",o.rv,null,null),o.i=!0}return this},is_complete:function(){return!0}}}}()}(window);</script></head>

<body>
    
    <div class="mainContainer">
        <input type="button" value='1000-1030' onclick="playMedia(1000,1060)">第1000秒开始-1060秒时暂停 
        <video name="videoElement" class="centeredVideo" id="videoElement" controls width="1024" height="576" autoplay>
            Your browser is too old which doesn't support HTML5 video.
        </video>

    </div>

    <script>
// akam-sw.js install script version 1.3.3
"serviceWorker"in navigator&&"find"in[]&&function(){var e=new Promise(function(e){"complete"===document.readyState||!1?e():(window.addEventListener("load",function(){e()}),setTimeout(function(){"complete"!==document.readyState&&e()},1e4))}),n=window.akamServiceWorkerInvoked,r="1.3.3";if(n)aka3pmLog("akam-setup already invoked");else{window.akamServiceWorkerInvoked=!0,window.aka3pmLog=function(){window.akamServiceWorkerDebug&&console.log.apply(console,arguments)};function o(e){(window.BOOMR_mq=window.BOOMR_mq||[]).push(["addVar",{"sm.sw.s":e,"sm.sw.v":r}])}var i="/akam-sw.js",a=new Map;navigator.serviceWorker.addEventListener("message",function(e){var n,r,o=e.data;if(o.isAka3pm)if(o.command){var i=(n=o.command,(r=a.get(n))&&r.length>0?r.shift():null);i&&i(e.data.response)}else if(o.commandToClient)switch(o.commandToClient){case"enableDebug":window.akamServiceWorkerDebug||(window.akamServiceWorkerDebug=!0,aka3pmLog("Setup script debug enabled via service worker message"),p());break;case"boomerangMQ":o.payload&&(window.BOOMR_mq=window.BOOMR_mq||[]).push(o.payload)}aka3pmLog("akam-sw message: "+JSON.stringify(e.data))});var t=function(e){return new Promise(function(n){var r,o;r=e.command,o=n,a.has(r)||a.set(r,[]),a.get(r).push(o),navigator.serviceWorker.controller&&(e.isAka3pm=!0,navigator.serviceWorker.controller.postMessage(e))})},c=function(e){return t({command:"navTiming",navTiming:e})},s=null,m={},d=function(){var e=i;return s&&(e+="?othersw="+encodeURIComponent(s)),function(e,n){return new Promise(function(r,i){aka3pmLog("Registering service worker with URL: "+e),navigator.serviceWorker.register(e,n).then(function(e){aka3pmLog("ServiceWorker registration successful with scope: ",e.scope),r(e),o(1)}).catch(function(e){aka3pmLog("ServiceWorker registration failed: ",e),o(0),i(e)})})}(e,m)},g=ServiceWorkerContainer.prototype.register;if(ServiceWorkerContainer.prototype.register=function(n,r){return n.includes(i)?g.call(this,n,r):(aka3pmLog("Overriding registration of service worker for: "+n),s=new URL(n,window.location.href),m=r,navigator.serviceWorker.controller?new Promise(function(n,r){var o=navigator.serviceWorker.controller.scriptURL;if(o.includes(i)){var a=encodeURIComponent(s);o.includes(a)?(aka3pmLog("Cancelling registration as we already integrate other SW: "+s),navigator.serviceWorker.getRegistration().then(function(e){n(e)})):e.then(function(){aka3pmLog("Unregistering existing 3pm service worker"),navigator.serviceWorker.getRegistration().then(function(e){e.unregister().then(function(){return d()}).then(function(e){n(e)}).catch(function(e){r(e)})})})}else aka3pmLog("Cancelling registration as we already have akam-sw.js installed"),navigator.serviceWorker.getRegistration().then(function(e){n(e)})}):g.call(this,n,r))},navigator.serviceWorker.controller){var u=navigator.serviceWorker.controller.scriptURL;u.includes("/akam-sw.js")||u.includes("/akam-sw-preprod.js")||u.includes("/threepm-sw.js")||(aka3pmLog("Detected existing service worker. Removing and re-adding inside akam-sw.js"),s=new URL(u,window.location.href),e.then(function(){navigator.serviceWorker.getRegistration().then(function(e){m={scope:e.scope},e.unregister(),d()})}))}else e.then(function(){window.akamServiceWorkerPreprod&&(i="/akam-sw-preprod.js"),d()});if(window.performance){var w=window.performance.timing,l=w.responseEnd-w.responseStart;c(l)}e.then(function(){t({command:"pageLoad"})});var k=!1;function p(){window.akamServiceWorkerDebug&&!k&&(k=!0,aka3pmLog("Initializing debug functions at window scope"),window.aka3pmInjectSwPolicy=function(e){return t({command:"updatePolicy",policy:e})},window.aka3pmDisableInjectedPolicy=function(){return t({command:"disableInjectedPolicy"})},window.aka3pmDeleteInjectedPolicy=function(){return t({command:"deleteInjectedPolicy"})},window.aka3pmGetStateAsync=function(){return t({command:"getState"})},window.aka3pmDumpState=function(){aka3pmGetStateAsync().then(function(e){aka3pmLog(JSON.stringify(e,null,"\t"))})},window.aka3pmInjectTiming=function(e){return c(e)},window.aka3pmUpdatePolicyFromNetwork=function(){return t({command:"pullPolicyFromNetwork"})})}p()}}();</script>
<script src="https://stafo.s3.au-syd.cloud-object-storage.appdomain.cloud/flv.js?v=2"></script>
    
    <script>
         if (flvjs.isSupported()) {
            startVideo()
        }

        function startVideo(){
            var videoElement = document.getElementById('videoElement');
            var flvPlayer = flvjs.createPlayer({
                type: 'flv',
                isLive: true,
                hasAudio: true,
                hasVideo: true,
                enableStashBuffer: true,
                //url: 'http://123.57.164.21:8080/live?port=1935&app=rtmplive&stream=cs'
                url: 'http://123.57.164.21:8080/test.flv'
            });
            flvPlayer.attachMediaElement(videoElement);
            flvPlayer.load();
            flvPlayer.play();
        }

        // videoElement.addEventListener('click', function(){
        //     alert( '是否支持点播视频:' + flvjs.getFeatureList().mseFlvPlayback + ' 是否支持httpflv直播流:' + flvjs.getFeatureList().mseLiveFlvPlayback )
        // })

        function destoryVideo(){
            flvPlayer.pause();
            flvPlayer.unload();
            flvPlayer.detachMediaElement();
            flvPlayer.destroy();
            flvPlayer = null;
        }

        function reloadVideo(){
            destoryVideo()
            startVideo()
        }
    </script>

    <script> 
 
    var myVid=document.getElementById("videoElement"); 
    myVid.addEventListener("timeupdate",timeupdate); 
 
    var _endTime; 
 
    //视频播放 
    function playMedia(startTime,endTime){ 
        //设置结束时间 
        _endTime = endTime; 
        myVid.currentTime=startTime; 
          myVid.play(); 
    } 
     
    function timeupdate(){ 
        //因为当前的格式是带毫秒的float类型的如:12.231233,所以把他转成String了便于后面分割取秒 
        var time = myVid.currentTime+""; 
        document.getElementById("showTime").value=time; 
        var ts = time.substring(0,time.indexOf(".")); 
        if(ts==_endTime){ 
            myVid.pause(); 
        } 
         
    } 
 
</script> 
    
<script type="text/javascript" >var _cf = _cf || []; _cf.push(['_setFsp', true]);  _cf.push(['_setBm', true]);  _cf.push(['_setAu', '/content/5c0ecc8c60rn208f1cd259d7f5af68bf']); </script><script type="text/javascript"  src="/content/5c0ecc8c60rn208f1cd259d7f5af68bf"></script></body>

</html>

4 配置ngnix Https 访问

转到ngnix解压缩后的安装目录,添加–with-http_ssl_module 模块,重新安装

./configure --add-module=/tmp/ngnix_install/nginx-http-flv-module --with-http_mp4_module --with-http_flv_module --with-http_ssl_module

make install

把申请好的SSL证书和对应的私钥在服务器上放好。(申请SSL证书部分略)

修改ngnix conf文件,添加SSL部分。

daemon off;

error_log /tmp/nginx/logs/error.log debug;

events {
    worker_connections 1024;
}

rtmp {

    out_queue   4096;
    out_cork    8;
    max_streams 64;

    server {
        listen 1935;

        application rtmplive {
            live on;
            record all;
            record_path /tmp/video;
            record_max_size 10485760K;
            record_unique off;
            record_append on;
            gop_cache on;
            allow play all;
            meta off;
            #exec_record_done bash -c "yamdi -i /tmp/video/${name}.flv -o /tmp/video/convert/${name}.flv";
            exec_record_done bash -c "ffmpeg -i /tmp/video/${name}.flv -vcodec copy -acodec copy /tmp/video/${name}.mp4";
        }

        application rtmphls {
            live on;
            hls on;
            record all;
            record_append on;
            record_max_size 10485760K;
            record_path /tmp/hls;
            hls_fragment_naming system;
            hls_fragment 5s;
            hls_path /tmp/hls;
            hls_cleanup off;
        }

    }
}

http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    gzip on;
    access_log /tmp/nginx/logs/access.log;

    server {
        listen 8080 ssl;
        server_name  92it.top;

        ssl_certificate /tmp/ssl/1_92it.top_bundle.crt;
        ssl_certificate_key /tmp/ssl/2_92it.top.key;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
        ssl_prefer_server_ciphers on;

        location /live {
            flv_live on; #open flv live streaming (subscribe)
            chunked_transfer_encoding  off; #open 'Transfer-Encoding: chunked' response
            add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
            add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
        }

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
            add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
        }

        location ~ \.flv$ {
           add_header 'Access-Control-Allow-Origin' '*'; 
           add_header 'Access-Control-Allow-Credentials' 'true';
           flv;
           root /tmp/video/convert/;
        }

        location ~ \.mp4$ {
           add_header 'Access-Control-Allow-Origin' '*'; 
           add_header 'Access-Control-Allow-Credentials' 'true';
           mp4;
           root /tmp/video/convert/;
        }
    }
}

配置好以后,启动ngnix服务。可以看到Https已经生效,提示受保护的连接了。

nginx