乐趣区

关于javascript:个人如何搭建Rtmp服务结合uniapp开发直播APP

前言

因为本人有一个 IM 类的利用,为了欠缺它所以决定也加上直播和短视频性能。做直播目前有两种办法,一是间接对接第三方的直播服务产品,二是本人搭服务再开发。所以这里也从这两个办法举荐简略的实现形式,阿里云和腾讯云之类的大厂产品就不安利了。

选型

  1. 第三方,PHP+Uni-App+LiveQing
  2. 本人开发,PHP+Uni-app+Nginx-rtmp-module

实现流程

1. 客户端采集视频流。(开摄像头,录屏等)

2. 客户端推流到 rtmp 服务器上。

3.rtmp 推流到某个特定端口。

4. 其余客户端再对该视频流进行拉流,实现直播。

第三方形式

第三方这次举荐的是一个叫 LiveQing 的平台,有点是搭建快捷不便,功能完善。在服务器上运行了他们的包后除了能实现支流业务场景的直播,而且还提供短视频的点播服务。还包含 API 调用,通过接口实现直播的创立,删除,直播数据统计。然而是要免费,该软件包在一台物理机或云服务器上只能收费试用一个月。

1. 找到该官网,抉择 rtmp 直播点播流媒体,下载试用把对应零碎解压到本人服务器。

2. 目录如下,将 start.sh 受权为 777。而后./start.sh 运行该文件。

3. 运行前能够关上 liveqing.ini 进行设置,比方后盾登录明码,端口号等。

4. 默认须要开启 10080 和 10085,所以须要用防火墙放行,操作如下。

systemctl start firewalld.service    // 开启防火墙

firewall-cmd add-port=10080/tcp --permanent
firewall-cmd add-port=10082/tcp --permanent

firewall-cmd --reload               // 重启

firewall-cmd --list-ports           // 查看放行的所有端口 

5. 端口放行,而后在运行 start.sh 呈现上面图标示意胜利。

6. 浏览器输出服务器的外网 IP:10080,就能够进入控制面板了。

7. 创立一个直播,设置名称和 ID,而后抉择编辑获取推流地址。

8. 为了测试能够本地下载一个 OBS 软件推流到该地址,只有一推流,直播状态就会显示直播中并且点击编辑能够获取拉流的地址。

9. 同样为了不便能够应用 VLS 软件进行拉流或者 wowza 在线网站测试直播。

代码实现

不应用第三方的话,就须要搭建 rtmp 服务,配置 Nginx,APP 视频采集推流,拉流等等。如果是大型平台,须要进行分流集群等。流媒体服务器依赖的服务,1.nginx 服务器;2.nginx 服务器装置须要依赖的服务 OpenSSL、pcre、zlib、c++、gcc 等,服务器环境是 Centos 7.3 64 位。

1. 进入根目录,mkdir source #创立源码目录,前面的源码都放在这个目录。cd source 进入该目录。

2. 下载 git,yum -y install git,而后通过网络下载须要的包。

git clone https://github.com/nginx/nginx.git                 #从 github 服务器上将 nginx 的源代码下载下来
git clone https://github.com/arut/nginx-rtmp-module.git     #将 rtmp 模块的源码下载下来
wget https://www.openssl.org/source/openssl-1.1.0.tar.gz     #下载 OpenSSL 源码包
wget https://ftp.pcre.org/pub/pcre/pcre-8.39.tar.gz         #下载 pcre 源码包
wget http://www.zlib.net/zlib-1.2.11.tar.gz                 #下载 zlib 包源码 

3.tar -zxvf 包名 #解压各个包源码

4. 在将 nginx 和须要的包编译前须要先装置 gcc,装置过能够省过。

yum -y install gcc             #确保依赖的 gcc 装置
yum -y install gcc-c++         #确保依赖的 c ++ 曾经装置 

5. 而后 cd 命令进入 source 下的 nginx 目录,输出上面命令。

./auto/configure --prefix=/usr/local/nginx \
        --with-pcre=../pcre-8.39 \
        --with-openssl=../openssl-1.1.0 \
        --with-zlib=../zlib-1.2.11 \
        --with-http_v2_module \
        --with-http_flv_module \
        --with-http_mp4_module \
        --add-module=../nginx-rtmp-module/

6. 查看胜利会呈现如下,而后 make 编译一下。

7.make install 装置

8. 以上操作后示意 Nginx 编译装置实现,而后 cd 到根目录,/usr/local/nginx/sbin,如果要测试 Nginx 是否能够拜访。先放行 80 端口重启防火墙,在 sbin 下输出./nginx 启动 Nginx 服务。浏览器拜访 IP 地址:80,呈现以下示意胜利。

9. 在 nginx 配置文件中配置 rtmp 服务,记住 rtmp 服务是和 http 服务是平级,所以咱们须要在和 http 配置平级的地位另起 rtmp 服务。

vi /usr/local/nginx/conf/nginx.conf #批改配置文件 
rtmp  {
    server  {
        listen 1935;
        chunk_size 4096;
        application live  {
            live on;
            record off;
        }
        application live2  {
            live on;
            record off;
        }
        application vod  {play /var/flvs;}
        application vod_http  {play http:// 服务器的 ip/vod;}
        application hls  {
            live on;
            hls on;
            hls_path /tmp/hls;
        }
    }
}
/usr/local/nginx/sbin/nginx -s reload  #批改配置文件重启 nginx 服务 

10. 下面 rtmp 服务的端口是 1935,所以也须要按之前办法给 1935 端口放行,查看云服务器的平安组是否也放行,而后再重启防火墙。

11. 本地电脑测试 1935 是否开启,能够 cmd 命令 telnet 服务器 IP 地址 端口号,如果呈现一下界面阐明端口曾经通了。

12. 接下来也能够通过 OBS 推流到该地址,而后用 WOWZA 拉流进行测试。
rtmp:// 你的服务器 ip: 端口 (1935)/live #URL 填写流的地址

13. 接下来演示 uni-app 的推流写法。

<template>
    <view class="content">        
        <view class="butlist">
            <view @click="back" class="buticon martp10">
                <image src="../../static/zhiwen-livepush/back2.png"></image>    
                <view class="mar10"> 返回 </view>                
            </view>
            <view @click="switchCamera" class="buticon martp10">
                <image src="../../static/zhiwen-livepush/reversal.png"></image>    
                <view class="mar10"> 翻转 </view>                
            </view>
            <view class="buticon" @click="startPusher">
                <view class="x_f"></view>
                <view :class="begin==true?'givebegin':'give'" >{{contTime}}</view>
                <view class="pulse" v-if="begin"></view>
            </view>
            <view class="buticon martp10">
                <image src="../../static/zhiwen-livepush/beautiful.png"></image>    
                <view class="mar10"> 丑化 </view>                
            </view>
            <view   class="buticon martp10" v-if="begin==false">
                <picker :value="index" @change="bindPickerChange" :range="array" range-key='cont'>
                    <image src="../../static/zhiwen-livepush/countdown.png"></image>    
                    <view class="mar10"> 倒计时 </view>
                </picker>    
            </view>

            <view @click="upload" class="buticon martp10" v-if="begin">
                <image src="../../static/zhiwen-livepush/yes.png"></image>    
                <view class="mar10"> 实现 </view>                
            </view>            
        </view>
        
         
    </view>

</template>

<script>
    export default {data() {
            return {
                begin:false,// 开始录制
                complete:false,// 录制结束
                pause:false,// 暂停推流
                currentWebview:null,
                pusher:null,
                livepushurl:'rtmp://106.52.216.244:10089/hls/1',  // 这里批改本人的推流地址就能够了
                logininfokey:'',// 登录验证加密串,homeworkcont:'',// 作业信息
                jiexititle:'',// 作业解析题目
                index: 0,// 定时
                indextu:0,// 是否开启定时
                contTime:'',
                array: [{// 话题标签
                        "id": 1,
                        "cont": "10 秒",
                        "time": 10
                    }, {
                        "id": 2,
                        "cont": "20 秒",
                        "time": 20
                    }, {
                        "id": 3,
                        "cont": "30 秒",
                        "time": 30
                    }, {
                        "id": 4,
                        "cont": "40 秒",
                        "time": 40
                    },{
                        "id": 5,
                        "cont": "50 秒",
                        "time": 50
                    },
                    {
                        "id": 6,
                        "cont": "60 秒",
                        "time": 60
                    }],
            }
        },
         
        onShow() {
             uni.getNetworkType({success: function (res) {console.log(res.networkType);
                    if(res.networkType != 'wifi'){
                        uni.showModal({ // 揭示用户更新
                            title: '舒适提醒',
                            content: '以后非 Wifi 网络,请留神您的流量是否够用',
                            success: (res) => {}})
                    }
                }
            });
            uni.onNetworkStatusChange(function (res) {console.log(res.isConnected);
                console.log(res.networkType);
                if(res.networkType != '4g' && res.networkType != 'wifi'){
                    uni.showModal({ // 揭示用户更新
                        title: '舒适提醒',
                        content: '以后网络品质差,请切换为 4G 网络或 Wifi 网络',
                        success: (res) => {}})
                }
            });
        /*     plus.key.addEventListener("backbutton",()=>{console.log("BackButton Key pressed!");
                //this.back()
                return false
            }); */
        },
         onBackPress(){this.back()
                console.log("BackButton Key pressed!");
                return true;
         },
        onLoad(res) {console.log(res)
            this.jiexititle=res.title
            uni.getStorage({
                key: 'logininfokey',
                success:(res) =>{console.log(res.data);
                    this.logininfokey=res.data
                    console.log(this.logininfokey)
                }
            });
            uni.getStorage({
                key: 'clickworkcont',
                success:(res) =>{console.log(res.data);
                    this.homeworkcont=res.data
                    //console.log(this.logininfokey)
                }
            });
            
            uni.getStorage({
                key: 'livepushurl',
                success:(res) =>{console.log(res.data);
                    this.livepushurl=res.data
                }
            });
            console.log(this.livepushurl)
            this.getwebview()// 获取 webview},
        methods: {
            // 倒计时
            bindPickerChange: function(e) {console.log('picker 发送抉择扭转,携带值为', e.target.value)
                this.index = e.target.value
                // this.indexs = e.target.value
                this.contTime=this.array[e.target.value].time
                uni.showToast({
                    title: '请点击红色按钮,开始进入倒计时',
                    icon:'none',
                    duration: 4000,                     
                });
            },
            
            /**
             * 返回
             */
            back(){
                uni.showModal({
                    title: '提醒',
                    content: '返回后未上传的视频须要从新录制哦',
                    success: function (res) {if (res.confirm) {
                            /* this.currentWebview=null;
                            this.pusher=null */
                            uni.redirectTo({url:'../user/issue'})
                            //this.currentWebview=null
                        } else if (res.cancel) {console.log('用户点击勾销');
                        }
                    }
                });
                
            },
            /**
             * 获取以后显示的 webview
             */
            getwebview(){var pages = getCurrentPages();
                var page = pages[pages.length - 1];
                // #ifdef APP-PLUS
                var getcurrentWebview = page.$getAppWebview();
                console.log(this.pages)
                console.log(this.page)
                console.log(JSON.stringify(page.$getAppWebview()))
                this.currentWebview=getcurrentWebview;
                // #endif
                this.plusReady()// 创立 LivePusher 对象},

            /**
             * 创立 LivePusher 对象 即推流对象
             */ 
            plusReady(){                
                // 创立直播推流控件
                this.pusher =new plus.video.LivePusher('pusher',{
                    url:'',
                    top:'0',
                    left:'0px',
                    width: '100%',
                    height:  uni.getSystemInfoSync().windowHeight-15 + 'px',                
                    position: 'absolute',//static 动态布局模式,如果页面存在滚动条则随窗口内容滚动,absolute 相对布局模式,如果页面存在滚动条不随窗口内容滚动;默认值为 "static"
                    beauty:'0',// 美颜 0-off  1-on  
                    whiteness:'0',//0、1、2、3、4、5,0 不应用美白,值越大美白水平越大。aspect:'9:16',                    
                 });
                console.log(JSON.stringify(this.pusher))
                console.log(JSON.stringify(this.currentWebview))
                // 将创立的对象 追加到 webview 中
                this.currentWebview.append(this.pusher);
                // 监听状态变动事件  
                this.pusher.addEventListener('statechange',(e)=>{console.log('statechange:'+JSON.stringify(e));
                }, false);
            },            
            // 美颜
            beautiful(){console.log(JSON.stringify(this.pusher))
                this.pusher.options.beauty=1
                this.plusReady()// 创立 LivePusher 对象},
            // 开始推流
            startPusher(){
                // 判断是否倒计时开始
                if(this.contTime!=''){if(this.indextu!=1){this.conttimejs()
                    }
                }else{this.beginlivepush()
                }
            },
            conttimejs(){if(this.contTime!=''){
                    this.indextu=1;// 开启计时
                    if(this.contTime==1){console.log("开始")
                        this.contTime=""
                        this.beginlivepush()
                        return false
                    }
                    this.contTime--
                    setTimeout(()=>{this.conttimejs()
                    },1000)
                }
            },
            beginlivepush() {
                this.indextu=0;// 敞开计时
                if(this.begin==false){// 未开启推流
                    this.begin=true;// 显示录制动画
                    // 设置推流服务器  *** 此处须要通过 ajax 向后端获取
                    this.pusher.setOptions({url:this.livepushurl // 推流地址 ********************************* 此处设置推流地址});
                    this.pusher.start();// 推流开启
                    uni.showToast({
                        title: '开始录制',
                        icon:'none',
                        duration: 2000,                     
                    });
                }else{if(this.pause==true){// 暂停推流状态
                        this.begin=true;// 显示录制动画
                        this.pause=false;// 推流开关置为默认状态
                        this.pusher.resume();// 复原推流
                        uni.showToast({
                            title: '开始录制',
                            icon:'none',
                            duration: 2000,                     
                        });
                    }else{
                        this.begin=false;// 敞开录制动画
                        this.pause=true;// 推流暂停
                        this.pusher.pause();;// 暂停推流
                        uni.showToast({
                            title: '暂停录制',
                            icon:'none',
                            duration: 2000,                     
                        });
                        // 提醒是否上传
                        this.upload()}

                }
            },
            /**
             * 切换摄像头
             */ 
            switchCamera() {this.pusher.switchCamera();
            },
            /**
             * 实现录制
             */
            upload(){
                 uni.showModal({
                     title: '提醒',
                     content: '确定保留吗',
                     success:(res)=> {if (res.confirm) {console.log('用户点击实现');
                             this.pusher.pause();;// 暂停推流
                             this.endlivepush()
                             
                            /* setTimeout(()=>{this.endlivepush()
                             },1000) */
                         } else if (res.cancel) {console.log('用户点击勾销');
                         }
                     }
                 });
            }, 
            // 完结推流, 此处须要调用后盾接口向云服务商提交完结状态
            endlivepush(){
                    uni.showToast({
                    icon:'loading',
                    title: '完结...',
                    duration: 5000
                });
                return false
                uni.request({
                        url: "",        
                           method: 'POST',
                        // dataType:'JSON',
                       data:{},
                       success:(res)=>{console.log(JSON.parse(res.data))
                           console.log(JSON.stringify(res.data))
                            uni.showToast({
                                icon:'loading',
                                title: '视频上传中...',
                                duration: 5000
                            });
                            
                            setTimeout(()=>{                            
                                uni.showToast({
                                    icon:'none',
                                    title: '上传实现',
                                    duration: 2000
                                });
                            },5000)
                            setTimeout(()=>{                            
                                uni.redirectTo({url: 'setvideotit?id='+this.homeworkcont.id,});
                            },7000)
                       },
                       error: (data)=>{//alert(JSON.stringify(data)+'谬误')                
                       }
                   });
            },
             
        },
        components:{}}
</script>

<style>.content{
        background: #000;
        overflow: hidden;
    }
    .butlist{
        height: 140upx;
        position: absolute;
        bottom: 0;
        display: flex;
        width: 100%;
        justify-content: space-around;
        padding-top: 20upx;
        border-top: 1px solid #fff;
        background: #000;
    }
    .buticon{
        height: 120upx;
        width: 120upx;
        color: #fff;
        position: relative;
        text-align: center;
        margin-bottom: 20upx;
    }
    .buticon image{
        height: 64upx;
        width: 64upx;
    }
    .buticon .mar10{margin-top: -20upx;}
    .martp10{margin-top: 10upx;}
    .give {
        width: 90upx;
        height: 90upx;
        background: #F44336;    
        border-radius: 50%;
        box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
          position: absolute; 
        left:15upx;
        top:15upx; 
            font-size: 44upx;
    line-height: 90upx;
    }
    .givebegin {
        width: 60upx;
        height: 60upx;
        background: #F44336;    
        border-radius: 20%;
        box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
          position: absolute; 
        left:30upx;
        top:30upx; 
    }
    .x_f{
        /* border: 6upx solid #F44336; */
        width: 120upx;
        height: 120upx;
        background: #fff;
        border-radius: 50%;
        position: absolute;
        text-align: center;
        top:0;
        left: 0;
      box-shadow: 0 0 28upx 0 rgb(251, 99, 24);
    }
    
    /* 产生动画(向外扩散变大)的圆圈  */
    .pulse {
        width: 160upx;
        height: 160upx;
        position: absolute;
        border: 12upx solid #F44336;
        border-radius: 100%;
        z-index: 1;
        opacity: 0;
        -webkit-animation: warn 2s ease-out;
        animation: warn 2s ease-out;
        -webkit-animation-iteration-count: infinite;
        animation-iteration-count: infinite;
        left: -28upx;
        top: -28upx;
    }
        
    
    /**
     * 动画
     */
    @keyframes warn {
    0% {transform: scale(0);
        opacity: 0.0;
    }
    25% {transform: scale(0);
        opacity: 0.1;
    }
    50% {transform: scale(0.1);
        opacity: 0.3;
    }
    75% {transform: scale(0.5);
        opacity: 0.5;
    }
    100% {transform: scale(1);
        opacity: 0.0;
    }
}
    
     
</style>

14. 拉流演示代码。

<template class='fullscreen'>
    <view class='fullscreen'>
        <view v-if="beCalling"  class="backols">
            <view class='becalling-text'> 对方邀请你开始视频聊天 </view>
            <view class="butlist2">
                <view @click="rejectCallHandler" class="buticon2 martp10">
                    <image src="../../static/img/netcall-reject.png"></image>    
                </view>
                    <view @click="acceptCallHandler" class="buticon2 martp10">
                        <image src="../../static/img/netcall-accept.png"></image>    
                    </view>
                </view>
        </view>
        <view v-else class="butlist">
                <view @click="switchaudio" class="buticon martp10">
                    <image src="../../static/img/netcall-call-voice.png"></image>    
                
                </view>
                <view @click="switchCamera" class="buticon martp10">
                    <image src="../../static/img/netcall-revert-camera.png"></image>    
                        
                </view>
                <view @click="close" class="buticon martp10">
                    <image src="../../static/img/netcall-reject.png"></image>    
                </view>
             
            </view>
    </view>
    
    
</template>

<script>
    export default {data() {
             return{
                beCalling: true,
                videourl:'',
                width:'',
                currentWebview:null,
                pushers:'',
                video :''
          }
        },
        
        onLoad: function (options) {this.getwebview()// 获取 webview
        },
        onUnload() {},
        methods: {close(){this.pusher.pause();// 暂停推流
                        this.pusher.close()// 敞开推流控件
                        uni.switchTab({url:''})
                },
                getwebview(){var pages = getCurrentPages();
                    var page = pages[pages.length - 1];
                    // #ifdef APP-PLUS
                    var getcurrentWebview = page.$getAppWebview();
                    console.log(this.pages)
                    console.log(this.page)
                    console.log(JSON.stringify(page.$getAppWebview()))
                    this.currentWebview=getcurrentWebview;
                    // #endif
                    this.plusReady()// 创立 LivePusher 对象},
                 
                plusReady(){                
                
                    this.pushers =new plus.video.VideoPlayer('video',{// src:self.userlist[0].url,
                        src:"rtmp://58.200.131.2:1935/livetv/hunantv", // 这里替换本人的拉流地址
                        top:'0px',
                        left:'0px',
                        controls:false,
                        width: '100%',
                        height: uni.getSystemInfoSync().windowHeight-150 + 'px',
                        position: 'static'        
                    });                 
                    this.currentWebview.append(this.pushers);
                 this.pushers.play()},
         
         
         /**
             * 切换摄像头
             */ 
            switchCamera() {this.pusher.switchCamera();
            },
            switchaudio() {console.log('点击了');
            }
                
        }    
    }
    
</script>

<style>
    
    .backols{background: rgba(0, 0, 0, 0.74);
    height: 100%;
    position: absolute;
    width: 100%;
    }
    uni-page{background:#000000;}
    .butlist{
        height: 140upx;
        position: absolute;
        bottom: 0;
        display: flex;
        width: 100%;
        justify-content: space-around;
        padding-top: 20upx;
        border-top: 1px solid #fff;
    }
    .buticon{
        height: 120upx;
        width: 120upx;
        color: #fff;
        position: relative;
        text-align: center;
        margin-bottom: 20upx;
    }
    .buticon image{
        height: 90upx;
        width: 90upx;
    }
    .buticon .mar10{margin-top: -20upx;}
    .martp10{margin-top: 10upx;}
    .becalling-text{
        text-align: center;
        color: #FFFFFF;
        font-size: 28upx;
        padding: 60upx;
        margin-top: 40%;
    }
    .butlist2{
        height: 140upx;
        position: absolute;
        bottom: 5%;
        display: flex;
        width: 100%;
        justify-content: space-around;
        padding-top: 20upx;
     
    }
    .buticon2{
        height: 120upx;
        width: 120upx;
        color: #fff;
        position: relative;
        text-align: center;
        margin-bottom: 20upx;
    }
    .buticon2 image{
        height: 110upx;
        width: 110upx;
    }
 
 
    .container {
      width: 100%;
      height: 100%;
    }
    /* 被叫 */
    .becalling-wrapper {
      position: relative;
      width:100%;
      height:800upx;
      background-color:#777;
      color:#fff;
      font-size:40rpx;
    }
    .becalling-wrapper .becalling-text {
      position: absolute;
      top:400rpx;
      left:50%;
      margin-left:-220rpx;
    }
    .becalling-wrapper .becalling-button-group {
      position: absolute;
      width:100%;
      box-sizing:border-box;
      bottom: 100rpx;
      padding: 0 40rpx;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
    }
    .becalling-button-group .button {
      width:220rpx;
      height:80rpx;
      border-radius:10rpx;
      justify-content:center;
      display:flex;
      align-items:center;
      font-size:33rpx;
      color:#000;
    }
    .becalling-button-group .reject-button {background-color:#f00;}
    .becalling-button-group .accept-button {background-color:rgb(26, 155, 252);
    }
    
    .calling-coverview {
      width:100%;
      height:100rpx;
      background-color:#ccc;
      color:#fff;
      font-size:40rpx;
      text-align:center;
      line-height:100rpx;
    }
    /* 视频容器 */
    .video-wrapper {
      width: 100%;
      height: 100%;
      padding-bottom: 100rpx;
      box-sizing: border-box;
      position: relative;
      background-color: #000;
    }
    .control-wrapper {
      width: 100%;
      box-sizing: border-box;
      position: absolute;
      bottom: 0;
    }
    .calling-voerview {
      background-color:#ccc;
      color:#fff;
      height: 160rpx;
      font-size: 40rpx;
      text-align: center;
      line-height: 160rpx;
    }
    .control-wrapper {
      position: fixed;
      bottom: 18px;
      left:0;
      display: flex;
      width: 100%;
      box-sizing: border-box;
      flex-direction:row;
      justify-content: space-between;
      padding: 0 42rpx;
      height: 200rpx;
    }
    .control-wrapper .item{
      width: 92rpx;
      height: 92rpx;
      margin-top: 100rpx;
    }
    .netcall-time-text {
      position:absolute;
      bottom:160rpx;
      width:100%;
      height: 40rpx;
      color:#fff;
      font-size:40rpx;
      text-align:center;
      left:0;
    }
    
    
    .fullscreen{
        display: flex;
        background: #000000;
        height: 100%;
        width: 100%;
        position: absolute;
    }
    
</style>

15.uni-app 模块权限如下。

 

退出移动版