共计 14470 个字符,预计需要花费 37 分钟才能阅读完成。
前言
因为本人有一个 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 --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 模块权限如下。