前言:
由于自己有一个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模块权限如下。