流程
流程端口规划
端口 | 用途 |
---|---|
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播放不了 |
网友评论