前言:
由于自己有一個IM類的應(yīng)用,為了完善它所以決定也加上直播和短視頻功能。做直播目前有兩種方法,一是直接對接第三方的直播服務(wù)產(chǎn)品,二是自己搭服務(wù)再開發(fā)。所以這里也從這兩個方法推薦簡單的實(shí)現(xiàn)方式,阿里云和騰訊云之類的大廠產(chǎn)品就不安利了。
選型:
1. 第三方,php+Uni-App+LiveQing
2. 自己開發(fā),PHP+Uni-app+Nginx-rtmp-module
實(shí)現(xiàn)流程:
1. 客戶端采集視頻流。(開攝像頭,錄屏等)
2. 客戶端推流到rtmp服務(wù)器上。
3. rtmp推流到某個特定端口。
4. 其他客戶端再對該視頻流進(jìn)行拉流,實(shí)現(xiàn)直播。
一、第三方方式
第三方這次推薦的是一個叫LiveQing的平臺,優(yōu)點(diǎn)是搭建快捷方便,功能完善。在服務(wù)器上運(yùn)行了他們的包后除了能實(shí)現(xiàn)主流業(yè)務(wù)場景的直播,而且還提供短視頻的點(diǎn)播服務(wù)。還包括API調(diào)用,通過接口實(shí)現(xiàn)直播的創(chuàng)建,刪除,直播數(shù)據(jù)統(tǒng)計(jì)。但是是要收費(fèi),該軟件包在一臺物理機(jī)或云服務(wù)器上只能免費(fèi)試用一個月。
1. 找到該官網(wǎng),選擇rtmp直播點(diǎn)播流媒體,下載試用把對應(yīng)系統(tǒng)解壓到自己服務(wù)器。
2. 目錄如下,將start.sh授權(quán)為777。然后./start.sh 運(yùn)行該文件。
3. 運(yùn)行前可以打開liveqing.ini進(jìn)行設(shè)置,比如后臺登錄密碼,端口號等。
4. 默認(rèn)需要開啟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. 端口放行,然后在運(yùn)行start.sh出現(xiàn)下面圖標(biāo)表示成功。
6. 瀏覽器輸入服務(wù)器的外網(wǎng)IP:10080,就可以進(jìn)入控制面板了。
7. 創(chuàng)建一個直播,設(shè)置名稱和ID,然后選擇編輯獲取推流地址。
8. 為了測試可以本地下載一個OBS軟件推流到該地址,只要一推流,直播狀態(tài)就會顯示直播中并且點(diǎn)擊編輯可以獲取拉流的地址。
9. 同樣為了方便可以使用VLS軟件進(jìn)行拉流或者wowza在線網(wǎng)站測試直播。
二、代碼實(shí)現(xiàn)
不使用第三方的話,就需要搭建rtmp服務(wù),配置Nginx,APP視頻采集推流,拉流等等。如果是大型平臺,需要進(jìn)行分流集群等。流媒體服務(wù)器依賴的服務(wù),1.nginx 服務(wù)器;2.nginx服務(wù)器安裝需要依賴的服務(wù) OpenSSL、pcre、zlib、 c++、gcc等,服務(wù)器環(huán)境是centos 7.3 64 位。
1. 進(jìn)入根目錄,mkdir source #創(chuàng)建源碼目錄,后面的源碼都放在這個目錄。cd source進(jìn)入該目錄。
2. 下載git,yum -y install git,然后通過網(wǎng)絡(luò)下載需要的包。
git clone https://github.com/nginx/nginx.git #從github服務(wù)器上將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++已經(jīng)安裝
5. 然后cd命令進(jìn)入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. 檢查成功會出現(xiàn)如下,然后make編譯一下。
7. make install 安裝
8. 以上操作后表示Nginx編譯安裝完成,然后cd到根目錄,/usr/local/nginx/sbin,如果要測試Nginx是否可以訪問。先放行80端口重啟防火墻,在sbin下輸入./nginx啟動Nginx服務(wù)。瀏覽器訪問IP地址:80,出現(xiàn)以下表示成功。
9. 在nginx配置文件中配置rtmp服務(wù),記住rtmp服務(wù)是和http服務(wù)是平級,所以我們需要在和http配置平級的位置另起rtmp服務(wù)。
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://服務(wù)器的ip/vod;
}
application hls {
live on;
hls on;
hls_path /tmp/hls;
}
}
}
/usr/local/nginx/sbin/nginx -s reload #修改配置文件重啟nginx服務(wù)
10. 上面rtmp服務(wù)的端口是1935,所以也需要按之前方法給1935端口放行,檢查云服務(wù)器的安全組是否也放行,然后再重啟防火墻。
11. 本地電腦測試1935是否開啟,可以cmd命令telnet 服務(wù)器IP地址 端口號,如果出現(xiàn)一下界面說明端口已經(jīng)通了 。
12. 接下來也可以通過OBS推流到該地址,然后用WOWZA拉流進(jìn)行測試。
rtmp://你的服務(wù)器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">翻轉(zhuǎn)</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">倒計(jì)時(shí)</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:'',//登錄驗(yàn)證加密串,
homeworkcont:'',//作業(yè)信息
jiexititle:'',//作業(yè)解析標(biāo)題
index: 0,//定時(shí)
indextu:0,//是否開啟定時(shí)
contTime:'',
array: [{//話題標(biāo)簽
"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: '當(dāng)前非Wifi網(wǎng)絡(luò),請注意您的流量是否夠用',
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: '當(dāng)前網(wǎng)絡(luò)質(zhì)量差,請切換為4G網(wǎng)絡(luò)或Wifi網(wǎng)絡(luò)',
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: {
//倒計(jì)時(shí)
bindPickerChange: function(e) {
console.log('picker發(fā)送選擇改變,攜帶值為', e.target.value)
this.index = e.target.value
// this.indexs = e.target.value
this.contTime=this.array[e.target.value].time
uni.showToast({
title: '請點(diǎn)擊紅色按鈕,開始進(jìn)入倒計(jì)時(shí)',
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('用戶點(diǎn)擊取消');
}
}
});
},
/**
* 獲取當(dāng)前顯示的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()//創(chuàng)建LivePusher對象
},
/**
* 創(chuàng)建LivePusher對象 即推流對象
*/
plusReady(){
// 創(chuàng)建直播推流控件
this.pusher =new plus.video.LivePusher('pusher',{
url:'',
top:'0',
left:'0px',
width: '100%',
height: uni.getSystemInfoSync().windowHeight-15 + 'px',
position: 'absolute',//static靜態(tài)布局模式,如果頁面存在滾動條則隨窗口內(nèi)容滾動,absolute絕對布局模式,如果頁面存在滾動條不隨窗口內(nèi)容滾動; 默認(rèn)值為"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))
//將創(chuàng)建的對象 追加到webview中
this.currentWebview.append(this.pusher);
// 監(jiān)聽狀態(tài)變化事件
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()//創(chuàng)建LivePusher對象
},
// 開始推流
startPusher(){
//判斷是否倒計(jì)時(shí)開始
if(this.contTime!=''){
if(this.indextu!=1){
this.conttimejs()
}
}else{
this.beginlivepush()
}
},
conttimejs(){
if(this.contTime!=''){
this.indextu=1;//開啟計(jì)時(shí)
if(this.contTime==1){
console.log("開始")
this.contTime=""
this.beginlivepush()
return false
}
this.contTime--
setTimeout(()=>{
this.conttimejs()
},1000)
}
},
beginlivepush() {
this.indextu=0;//關(guān)閉計(jì)時(shí)
if(this.begin==false){//未開啟推流
this.begin=true;//顯示錄制動畫
// 設(shè)置推流服務(wù)器 ***此處需要通過ajax向后端獲取
this.pusher.setOptions({
url:this.livepushurl //推流地址********************************* 此處設(shè)置推流地址
});
this.pusher.start();//推流開啟
uni.showToast({
title: '開始錄制',
icon:'none',
duration: 2000,
});
}else{
if(this.pause==true){//暫停推流狀態(tài)
this.begin=true;//顯示錄制動畫
this.pause=false;//推流開關(guān)置為默認(rèn)狀態(tài)
this.pusher.resume();//恢復(fù)推流
uni.showToast({
title: '開始錄制',
icon:'none',
duration: 2000,
});
}else{
this.begin=false;//關(guān)閉錄制動畫
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('用戶點(diǎn)擊完成');
this.pusher.pause();;//暫停推流
this.endlivepush()
/* setTimeout(()=>{
this.endlivepush()
},1000) */
} else if (res.cancel) {
console.log('用戶點(diǎn)擊取消');
}
}
});
},
//結(jié)束推流,此處需要調(diào)用后臺接口向云服務(wù)商提交結(jié)束狀態(tài)
endlivepush(){
uni.showToast({
icon:'loading',
title: '結(jié)束...',
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);
}
/* 產(chǎn)生動畫(向外擴(kuò)散變大)的圓圈 */
.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()//關(guān)閉推流控件
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()//創(chuàng)建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('點(diǎn)擊了');
}
}
}
</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模塊權(quán)限如下。