前言:
由於自己有一個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模塊權限如下。