前言
因为本人有一个IM类的利用,为了欠缺它所以决定也加上直播和短视频性能。做直播目前有两种办法,一是间接对接第三方的直播服务产品,二是本人搭服务再开发。所以这里也从这两个办法举荐简略的实现形式,阿里云和腾讯云之类的大厂产品就不安利了。
选型
- 第三方,PHP+Uni-App+LiveQing
- 本人开发,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 --permanentfirewall-cmd add-port=10082/tcp --permanentfirewall-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模块权限如下。