nginx-rtmp-module直播实验

流程

流程

端口规划

端口 用途
8000 nginx服务器
http在线观看视频
8020 nodejs + express,处理nginx-rtmp-module回调
1)将rtmp流地址写文件/数据库
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;
            }
      }
}

创建文件夹

// 视频文件保存的路径
$ 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>节点下的<name>节点
  • 用处不大,请使用后面的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文件中视频的状态字段)
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 href="//vjs.zencdn.net/5.11/video-js.min.css" rel="stylesheet">
    <script src="//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="//cdn.bootcss.com/flv.js/1.1.0/flv.min.js"></script>
    <script src="//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("视频状态:" + 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静态视频时有bug,只在左上角显示播放小窗口
flv.js 播放flv静态视频
live stream有bug播放不了
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容