nginx-rtmp-module直播實驗

流程

流程

端口規(guī)劃

端口 用途
8000 nginx服務器
http在線觀看視頻
8020 nodejs + express,處理nginx-rtmp-module回調
1)將rtmp流地址寫文件/數(shù)據(jù)庫
2)生成視頻的縮略圖
8040 rtmp傳輸端口

編譯 安裝 nginx + nginx-rtmp-module

$ ./configure --add-module=/home/troyz/software/nginx-rtmp-module --with-openssl=/home/troyz/software/openssl-OpenSSL_1_0_2 --with-http_ssl_module --with-debug
$ sudo make
$ sudo make install

配置文件

  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"

rtmp配置 - nginx.conf

http {
     server{
        listen       8000;
        location /stat {
            rtmp_stat all;
            # NOTE: please copy file `stat.xsl` from `nginx-rtmp-module` to nginx's html folder
            rtmp_stat_stylesheet stat.xsl;
        }
        location /stat.xsl {
            root html;
        }
        location /videos {
            root html;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods *;
            add_header Access-Control-Allow-Headers *;
        }
     }
}
rtmp {
     server {
            listen 8040;
            application live {
                live on;
                record all;
                record_path /usr/local/nginx/html/videos/videos;
                on_publish http://127.0.0.1:8020/rtmp/push;
                on_publish_done http://127.0.0.1:8020/rtmp/push_done;
                notify_method get;
            }
      }
}

創(chuàng)建文件夾

// 視頻文件保存的路徑
$ mkdir /usr/local/nginx/html/videos/videos
$ chmod a+w /usr/local/nginx/html/videos/videos

// 縮略圖文件保存的路徑
$ mkdir /usr/local/nginx/html/videos/preview
$ chmod a+w /usr/local/nginx/html/videos/preview

啟動nginx

$ /usr/local/nginx/sbin/nginx -t
$ /usr/local/nginx/sbin/nginx

stream 列表

  • 首先拷貝文件cp nginx-rtmp-module/stat.xls nginx/html/
  • 訪問 http://127.0.0.1:8000/stat
  • 解析<stream>節(jié)點下的<name>節(jié)點
  • 用處不大届搁,請使用后面的express處理

安裝ffmpeg

請自行查找相當文檔爆阶,我在centos6上是源碼安裝,在mac上是brew安裝的缭黔。

Express處理rtmp播放回調

$ yum install -y nodejs
$ mkdir /usr/local/nginx/express.js && cd /usr/local/nginx/express.js
$ npm init
$ npm install express --save
$ vim index.js
var fs = require('fs');
var express = require('express');
var app = express();
var exec = require('child_process').exec; 

// 所有的視頻地址信息都保存在json文件中
var filePath = '/usr/local/nginx/html/videos/video_list.json';

var videoList = [];

function saveVideoList()
{
  fs.writeFile(filePath, JSON.stringify(videoList), function(err){
    if(err) return;
    console.log('save videos successfully');
  });  
}

// 生成視頻的縮略圖
function createPreviewImage(video)
{
  var previewFilePath = getPreviewFilePath(video);
  var videoFilePath = getVideoFilePath(video);
  fs.stat(previewFilePath, function (err, stats){
    if(stats && stats.isFile()){
      console.log("prefiew file is exists! " + previewFilePath);
    }
    else{
      console.log("prefiew file is not exists! " + previewFilePath + ", let's generate it!");
      var cmdStr = "ffmpeg -i " + videoFilePath + "  -vcodec png -vframes 1 -an -f rawvideo -s 640x480 -ss 00:00:01 -y " + previewFilePath;
      exec(cmdStr, function(err,stdout,stderr){
        if(err){
          console.log("create preview image file error: " + stderr);
        }
        else{
          console.log("create preview image file successfully: " + stdout);
        }
      });
    }
  });
}

function getVideoFilePath(video)
{
  return "/usr/local/nginx/html/videos/videos/" + video.name + ".flv";
}

function getPreviewFilePath(video)
{
  return "/usr/local/nginx/html/videos/preview/" + video.name + ".png";
}

function removeVideo(video){
  fs.unlink(getVideoFilePath(video), function(err){});
  fs.unlink(getPreviewFilePath(video), function(err){});
}

fs.readFile(filePath, 'utf-8', function(err, data){
  if(err) return;
  if(data && data.length > 0)
  {
    videoList = JSON.parse(data);
  }
  videoList = videoList ? videoList : [];
  console.log('data: ' + videoList);
});

// 當有新的`rtmp`流上傳時被`nginx-rtmp-module`調用
app.get('/rtmp/push', function (req, res) {
  console.log('ok push: ' + JSON.stringify(req.query));
  if(req.query)
  {
    var exist = false;
    for(var i = 0; i < videoList.length; i++)
    {
      var item = videoList[i];
      if(item.app == req.query.app && item.name == req.query.name)
      {
        videoList = videoList.slice(0, i).concat(req.query).concat(videoList.slice(i + 1));
        exist = true;
        break;
      }
    }
    if(!exist)
    {
      videoList.push(req.query);
    }
    saveVideoList();
    // 生成視頻的縮略圖
    setTimeout(function(){
      createPreviewImage(req.query);
    }, 5000);
  }
  res.send('passed');
});

// 當`rtmp`流播放結束時被`nginx-rtmp-module`調用(修改json文件中視頻的狀態(tài)字段)
app.get('/rtmp/push_done', function(req, res){
  console.log('ok push done: ' + JSON.stringify(req.query));
  if(req.query)
  {
    for(var i = 0; i < videoList.length; i++)
    {
      var item = videoList[i];
      if(item.app == req.query.app && item.name == req.query.name)
      {
        videoList = videoList.slice(0, i).concat(req.query).concat(videoList.slice(i + 1));
        saveVideoList();

        var filePath = getVideoFilePath(item);
        // remove video file is size is ZERO
        fs.stat(filePath, function (err, stats){
          if(!err && stats){
            if(stats.size <= 0){
              console.log("remove invalid video file: " + filePath);
              videoList = videoList.slice(0, i).concat(videoList.slice(i + 1));
              saveVideoList();
              removeVideo(item);
            }
          }
        });
        break;
      }
    }
    createPreviewImage(req.query);
  }
  res.send('passed');
});

// 刪除所有視頻、縮略圖
app.get('/rtmp/clean', function(req, res){
  for(var i = 0; i < videoList.length; i++)
  {
    var item = videoList[i];
    removeVideo(item);
  }
  videoList = [];
  saveVideoList();
  res.send('passed');
});

// 生成視頻的縮略圖
app.get('/rtmp/g', function(req, res){
  for(var i = 0; i < videoList.length; i++)
  {
    var item = videoList[i];
    createPreviewImage(item);
  }
  res.send('passed');
});

// 刪除某個視頻+縮略圖
app.get('/rtmp/delete', function(req, res){
  if(req.query && req.query["name"]){
    for(var i = 0; i < videoList.length; i++)
    {
      var item = videoList[i];
      if(item.name == req.query["name"]){
        removeVideo(item);
        videoList = videoList.slice(0, i).concat(videoList.slice(i + 1));
        saveVideoList();
        break;
      }
    }
  }
  res.send('passed');
});

var server = app.listen(8020, function () {
  var host = server.address().address;
  var port = server.address().port;
  console.log('app listening at http://%s:%s', host, port);
});

啟動express

$ npm install -g forever

// 守護進程運行
$ forever start express.js/index.js

在線觀看

$ vim /usr/local/nginx/html/videos/index.html
<!DOCTYPE html>
<html>

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>flv.js demo</title>
    <link  rel="stylesheet">
    <script src="http://vjs.zencdn.net/5.11/video.min.js"></script>

    <style>
        .videoContainer {
            display: block;
            /*width: 1024px;*/
            flex: 1;
            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: none;
            width: 100%;
            text-align: left;
            margin-left: auto;
            margin-right: auto;
        }
        .container{
            display: flex;
            flex-flow: row;
        }
        .left{
            width: 30%;
        }
        .videoList{
            width: 100%;
            display: flex;
            flex-flow: row;
        }
        .leftVideoList{
            flex: 1;
        }
        .rightVideoList{
            flex: 1;
        }
        .videoDiv{
        }
        .emptyVideo{
            width: 20px;
        }
        .active{
            background-color: blue;
            font-weight: bold;
        }
        .normal{
            background-color: gray;
            font-weight: normal;
        }
        .videoList img{
            width: 100%;
            margin-bottom: 20px;
            background-color: black;
        }
        .videoStatus{
            position: absolute;
            margin-top: -50px;
            width: calc((30vw - 20px) / 2.0);
            text-align: center;
            color: white;
            padding-top: 5px;
            padding-bottom: 5px;
            font-size: 12px;
        }
    </style>
</head>

<body>

    <div id="container" class="container">
        <div class="left">
            <h3>Video List</h3>
            <div class="videoList">
                <div class="leftVideoList">
                    <div v-for="(video, index) in leftVideoList" class="videoDiv" v-on:click="playVideo(video.index)">
                        <image v-bind:src="'preview/' + video.name + '.png'">
                        <div class="videoStatus" v-bind:class="{ active: video.index==selectedIndex, normal: video.index!=selectedIndex }">
                            {{ video.name + (video.call == 'publish' ? '(進行中)' : '(已截止)')}}
                        </div>
                    </div>
                </div>
                <div class="emptyVideo"></div>
                <div class="rightVideoList">
                    <div v-for="(video, index) in rightVideoList" class="videoDiv" v-on:click="playVideo(video.index)">
                        <image v-bind:src="'preview/' + video.name + '.png'">
                        <div class="videoStatus" v-bind:class="{ active: video.index==selectedIndex, normal: video.index!=selectedIndex }">
                            {{ video.name + (video.call == 'publish' ? '(進行中)' : '(已截止)')}}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div style="width: 70%; height: 576px;position: absolute;top:0;right:0;">
        <video id="videoJsPlayer" name="videoJsPlayer" class="video-js centeredVideo" preload="auto" controls autoplay width="1024px" height="576px">
        </video>
    </div>
    <div style="width: 70%; height: 576px;position: absolute;top:0;right:0;" v-bind:style="{display: selectedIndex==-1?'none':'block'}">
        <video name="flvjsPlayer" autoplay class="centeredVideo" preload="auto" controls autoplay width="1024" height="576">
            Your browser is too old which doesn't support HTML5 video.
        </video>
        <br>
        <div class="controls">
            <button onclick="flv_load()">Load</button>
            <button onclick="flv_start()">Start</button>
            <button onclick="flv_pause()">Pause</button>
            <button onclick="flv_destroy()">Destroy</button>
            <input style="width:100px" type="text" name="seekpoint"/>
            <button onclick="flv_seekto()">SeekTo</button>
        </div>
    </div>

    <script src="http://cdn.bootcss.com/flv.js/1.1.0/flv.min.js"></script>
    <script src="http://cdn.bootcss.com/vue/2.2.1/vue.min.js"></script>
    
    <script>
        function getAllVideoList()
        {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', 'video_list.json', true);
            xhr.onload = function (e) {
                var videoList = JSON.parse(xhr.response);
                if(videoList && videoList.length > 0){
                    for(var i = 0; i < videoList.length; i++){
                        videoList[i].index = i;
                    }
                }
                app.videoList = videoList;
            }
            xhr.send();
        }

        function flv_load() {
            if(app.selectedIndex == -1)
            {
                return;
            }
            var video = app.videoList[app.selectedIndex];
            console.log(video.name + ".flv" + " isrecording: " + (video.call == 'publish'));

            var element = document.getElementsByName('flvjsPlayer')[0];
            player = flvjs.createPlayer({
                type: 'flv',
                url: "videos/" + video.name + ".flv",
                isLive: video.call == 'publish'
            }, {
                enableWorker: false,
                lazyLoadMaxDuration: 3 * 60,
                seekType: 'range',
            });
            player.attachMediaElement(element);
            player.load();
        }

        function videoJs_load()
        {
            if(app.selectedIndex == -1)
            {
                return;
            }
            var video = app.videoList[app.selectedIndex];
            var rtmpUrl = video.tcurl + "/" + video.name;
            var options = {
                sources: [{
                    src: rtmpUrl,
                    type: 'rtmp/flv'
                }]
            };
            videojsplayer.poster("preview/" + video.name + ".png");
            if (typeof videojsplayer !== "undefined") {
                if (videojsplayer != null) {
                    videojsplayer.show();
                    videojsplayer.src({
                        src: rtmpUrl,
                        type: 'rtmp/flv'
                    });
                    videojsplayer.load();
                    videojsplayer.play();
                    return;
                }
            }
            videojsplayer = videojs('videoJsPlayer', options, function onPlayerReady() {
              videojs.log('Your player is ready!');

              // In this context, `this` is the player that was created by Video.js.
              this.play();

              // How about an event listener?
              this.on('ended', function() {
                videojs.log('Awww...over so soon?!');
              });
            });
        }

        function destroyFlvPlayer()
        {
            if (typeof player !== "undefined") {
                if (player != null) {
                    player.unload();
                    player.detachMediaElement();
                    player.destroy();
                    player = null;
                }
            }
        }

        function destroyVideoJsPlayer()
        {
            if (typeof videojsplayer !== "undefined") {
                if (videojsplayer != null) {
                    videojsplayer.pause();
                    // videojsplayer.hide();
                    // videojsplayer = null;
                }
            }
        }

        function flv_start() {
            player.play();
        }

        function flv_pause() {
            player.pause();
        }

        function flv_destroy() {
            player.pause();
            player.unload();
            player.detachMediaElement();
            player.destroy();
            player = null;
        }

        function flv_seekto() {
            var input = document.getElementsByName('seekpoint')[0];
            player.currentTime = parseFloat(input.value);
        }

        function getUrlParam(key, defaultValue) {
            var pageUrl = window.location.search.substring(1);
            var pairs = pageUrl.split('&');
            for (var i = 0; i < pairs.length; i++) {
                var keyAndValue = pairs[i].split('=');
                if (keyAndValue[0] === key) {
                    return keyAndValue[1];
                }
            }
            return defaultValue;
        }
        var app = new Vue({
          el: '#container',
          data: {
            videoList: [],
            selectedIndex: -1,
            isPublishing: false
          },
          computed: {
            leftVideoList: function(){
                return this.videoList.filter(function (item, index) {
                  return index % 2 === 0
                })
            },
            rightVideoList: function(){
                return this.videoList.filter(function (item, index) {
                  return index % 2 === 1
                })
            }
          },
          methods: {
            playVideo: function(index){
                if(app.selectedIndex == index){
                    return;
                }
                app.selectedIndex = index;
                var video = app.videoList[index];
                destroyFlvPlayer();
                destroyVideoJsPlayer();
                if(video.call == 'publish_done'){
                    app.isPublishing = false;
                    flv_load();
                }
                else if(video.call == 'publish'){
                    app.isPublishing = true;
                    videoJs_load();
                }
                else{
                    alert("視頻狀態(tài):" + video.call);
                }
            }
          }
        });
        getAllVideoList();
        videojsplayer = videojs('videoJsPlayer', {});
        videojsplayer.hide();
        // document.addEventListener('DOMContentLoaded', function () {
        //     flv_load();
        // });
    </script>
    
</body>

</html>

ffmpeg rtmp推流

ffmpeg  -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -acodec libfaac -f flv rtmp://localhost:8040/live/test

瀏覽器在線觀看

http://127.0.0.1:8000/videos/index.html

瀏覽器在線觀看

rtmp推流庫

Library Platform 特點
LFLiveKit iOS 基本滿足需求蒂破、可以本地錄制
PLMediaStreamingKit iOS 七牛云馏谨,需要將視頻推送到七牛云平臺
yasea android 經常花屏
SopCastComponent android 延時卡頓嚴重
librestreaming android 基本滿足需求

rtmp播放庫

Library Platform
ijkplayer android/iOS

web在線觀看

Library 特點
video.js 播放rtmp流附迷,
播放flv靜態(tài)視頻時有bug惧互,只在左上角顯示播放小窗口
flv.js 播放flv靜態(tài)視頻
live stream有bug播放不了
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市喇伯,隨后出現(xiàn)的幾起案子喊儡,更是在濱河造成了極大的恐慌,老刑警劉巖稻据,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾猜,死亡現(xiàn)場離奇詭異,居然都是意外死亡捻悯,警方通過查閱死者的電腦和手機匆赃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來今缚,“玉大人算柳,你說我怎么就攤上這事⌒昭裕” “怎么了瞬项?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長何荚。 經常有香客問我滥壕,道長,這世上最難降的妖魔是什么兽泣? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任绎橘,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘称鳞。我一直安慰自己涮较,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布冈止。 她就那樣靜靜地躺著狂票,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熙暴。 梳的紋絲不亂的頭發(fā)上闺属,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音周霉,去河邊找鬼掂器。 笑死,一個胖子當著我的面吹牛俱箱,可吹牛的內容都是我干的国瓮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼狞谱,長吁一口氣:“原來是場噩夢啊……” “哼乃摹!你這毒婦竟也來了?” 一聲冷哼從身側響起跟衅,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤孵睬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伶跷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掰读,經...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年撩穿,在試婚紗的時候發(fā)現(xiàn)自己被綠了磷支。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谒撼。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡食寡,死狀恐怖,靈堂內的尸體忽然破棺而出廓潜,到底是詐尸還是另有隱情抵皱,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布辩蛋,位于F島的核電站呻畸,受9級特大地震影響,放射性物質發(fā)生泄漏悼院。R本人自食惡果不足惜伤为,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绞愚,春花似錦叙甸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至糖驴,卻和暖如春僚祷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贮缕。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工辙谜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跷睦。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓筷弦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抑诸。 傳聞我的和親對象是個殘疾皇子烂琴,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內容