概要
前端:借助uniapp组件(live-pusher,video)进行推流、拉流
后端:借助node-media-server插件:一个 Node.js 实现的RTMP/HTTP/WebSocket/HLS/DASH流媒体服务器
整体架构流程
一、前端:
视频页面布局
<view>
<!-- 拉流 -->
<video id="myVideo" class="live-video" :src="src" @error="videoErrorCallback" autoplay objectFit="cover"
:style="{width:windowW+'px',height:windowH+'px'}"></video>
<!-- 推流 -->
<live-pusher id='livePusher' ref="livePusher" class="live-pusher"
:style="{width:livePusherStyle[0]+'px',height:livePusherStyle[1]+'px',top:livePusherStyle[2]+'px',right:livePusherStyle[3]+'px'}"
:url="url" mode="SD" :muted="false" :enable-camera="true" :auto-focus="true" :beauty="0" whiteness="0"
aspect="9:16" @statechange="statechange" @netstatus="netstatus" @error="error"></live-pusher>
<!-- 背景虚拟图片 -->
<view class="img-bg" v-if="liveStyle != 0 && liveStyle != 2">
<!-- <view class="img-bg" v-if="liveStyle != 0 && liveStyle != 2"> -->
<image :src="fimg" mode="aspectFill" class="img-bg-user"
:style="{width:windowW+'px',height:windowH+'px',opacity:(liveStyle == 2 || liveStyle == 1 ||liveStyle == 0 ? '0':'1')}">
</image>
<view class="blur"
:style="{width:windowW+'px',height:windowH+'px',opacity:(liveStyle == 2 || liveStyle == 0 ? '0':'1')}">
</view>
</view>
<!-- 背景 -->
<view class="bg">
<image src="../../../static/live/topbg.png" class="bg-top" :style="{width:windowW+'px'}"></image>
<image src="../../../static/live/btbg.png" class="bg-bottom"
:style="{width:windowW+'px',top:windowH-168+'px'}"></image>
</view>
<!-- 头部信息 -->
<view class="top-title" :style="{width:windowW+'px',top:statusBarH+'px'}">
<!-- <image src="../../../static/live/colses.png" class="closs-img"></image> -->
<text class="title" v-if="liveStyle != 2 && liveStyle != 5">{{title}}</text>
<text class="timer" v-if="liveStyle == 2 || liveStyle == 5">{{timer}}</text>
</view>
<!-- 用户信息和按钮 -->
<view v-if="liveStyle == 0">
<view class="user" :style="{width:windowW+'px',top:statusBarH+84+'px'}">
<image :src="fimg" class="user-img"></image>
<text class="user-name">{{fname}}</text>
</view>
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="back(1)">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">取消</text>
</view>
<view class="btn" @click="changeState(3)">
<view class="btn-r voice">
<image src="../../../static/live/yuyin.png" class="voice-img"></image>
</view>
<text class="btn-name">切到语音通话</text>
</view>
</view>
</view>
<view v-if="liveStyle == 1">
<view class="user" :style="{width:windowW+'px',top:windowH*0.28+'px'}">
<image :src="fimg" class="user-img-bag"></image>
<text class="user-name">{{fname}}</text>
</view>
<view class="btns-top" :style="{width:windowW+'px',bottom:bottomH+195+'px'}">
<view class="btn">
</view>
<view class="btn" @click="changeState(5)">
<view class="btn-r voice">
<image src="../../../static/live/yuyin.png" class="voice-img"></image>
</view>
<text class="btn-name">切到语音通话</text>
</view>
</view>
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="cancelLive()">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">挂断</text>
</view>
<view class="btn" @click="ingLive()">
<view class="btn-r live">
<image src="../../../static/live/lives.png" class="live-img"></image>
</view>
<text class="btn-name">接听</text>
</view>
</view>
</view>
<view v-if="liveStyle == 2">
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="changeState(5)">
<view class="btn-r voice">
<image src="../../../static/live/yuyin.png" class="voice-img"></image>
</view>
<text class="btn-name">切到语音通话</text>
</view>
<view class="btn" @click="back(min)">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">挂断</text>
</view>
<view class="btn" @click="tapVioce">
<view class="btn-r voice">
<image src="../../../static/live/xiangji.png" class="live-img">
</image>
</view>
<text class="btn-name">切换摄像头</text>
</view>
</view>
</view>
<view v-if="liveStyle == 3">
<view class="user" :style="{width:windowW+'px',top:windowH*0.28+'px'}">
<image :src="fimg" class="user-img-bag"></image>
<text class="user-name">{{fname}}</text>
</view>
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="tapMkf">
<view class="btn-r" :style="{backgroundColor:(mkf?'#fff':'rgba(0,0,0,0.3)')}">
<image :src="'../../../static/live/'+(mkf?'opmkf.png':'comkf.png') " class="live-img"></image>
</view>
<text class="btn-name">{{mkfTxt}}</text>
</view>
<view class="btn" @click="back(1)">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">挂断</text>
</view>
<view class="btn" @click="tapVioce">
<view class="btn-r" :style="{backgroundColor:(vioce?'#fff':'rgba(0,0,0,0.3)')}">
<image :src="'../../../static/live/'+(vioce?'opvoice.png':'covoice.png')" class="live-img">
</image>
</view>
<text class="btn-name">{{vioceTxt}}</text>
</view>
</view>
</view>
<view v-if="liveStyle == 4">
<view class="user" :style="{width:windowW+'px',top:statusBarH+84+'px'}">
<image :src="fimg" class="user-img"></image>
<text class="user-name">{{fname}}</text>
</view>
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="back(2)">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">挂断</text>
</view>
<view class="btn" @click="changeState(5)">
<view class="btn-r live">
<image src="../../../static/live/phone1.png" class="live-img"></image>
</view>
<text class="btn-name">接听</text>
</view>
</view>
</view>
<view v-if="liveStyle == 5">
<view class="user" :style="{width:windowW+'px',top:windowH*0.28+'px'}">
<image :src="fimg" class="user-img-bag"></image>
<text class="user-name">{{fname}}</text>
</view>
<view class="btns" :style="{width:windowW+'px',bottom:bottomH+46+'px'}">
<view class="btn" @click="tapMkf">
<view class="btn-r" :style="{backgroundColor:(mkf?'#fff':'rgba(0,0,0,0.3)')}">
<image :src="'../../../static/live/'+(mkf?'opmkf.png':'comkf.png') " class="live-img"></image>
</view>
<text class="btn-name">{{mkfTxt}}</text>
</view>
<view class="btn" @click="back(min)">
<view class="btn-r cancel">
<image src="../../../static/live/phone.png" class="cancel-img"></image>
</view>
<text class="btn-name">挂断</text>
</view>
<view class="btn" @click="tapVioce">
<view class="btn-r" :style="{backgroundColor:(vioce?'#fff':'rgba(0,0,0,0.3)')}">
<image :src="'../../../static/live/'+(vioce?'opvoice.png':'covoice.png')" class="live-img">
</image>
</view>
<text class="btn-name">{{vioceTxt}}</text>
</view>
</view>
</view>
</view>
注意:
视频通话状态liveStyle: 0:发起视频 1:接收视频 2:视频中 3:发起语音 4:接收语音 5语音中
功能:
<script>
export default {
data() {
return {
uuid: '',
ffid: '',
uid: '',
fid: '',
fimg: '',
fname: '',
url: '', // 推流地址
src: 'rtmp://192.168.137.1:1935/live/id', // 拉流地址
windowW: '', //设备宽度
windowH: '', //设备高度
statusBarH: '', //状态栏高度
bottomH: '', //底部安全区域
timer: '00:00', // 通话时间
liveStyle: 0, //视频通话状态 0:发起视频 1:接收视频 2:视频中 3:发起语音 4:接收语音 5语音中
vioce: false, //是否开启扬声器
mkf: false, //是否开启麦克风
mkfTxt: '麦克风已关',
vioceTxt: '扬声器已关',
min: 2, //发起方 接收方判断
}
},
onLoad(e) {
this.url = 'rtmp://192.168.137.1:1935/live/' + e.ffid + e.uuid
this.src = 'rtmp://192.168.137.1:1935/live/' + e.uuid + e.ffid
// 接收跳转的数据
this.gainInformation(e)
this.getPhoneStyle();
this.liveCancel();
this.liveing();
this.changeVoice();
},
onReady() {
// 注意:需要在onReady中 或 onLoad 延时
this.context = uni.createLivePusherContext("livePusher", this);
setTimeout(() => {
this.loadState(this.liveStyle)
}, 10)
// this.testsx();
},
computed: {
// 视频窗口大小
livePusherStyle() {
if (this.liveStyle == 2) {
return [100, 168, this.statusBarH + 4, 4]
} else {
return [this.windowW, this.windowH, 0, 0]
}
},
// 头部提示信息
title() {
if (this.liveStyle == 0) {
return '等待对方接受邀请...';
} else if (this.liveStyle == 1) {
return '邀请你视频通话...';
} else if (this.liveStyle == 3) {
return '等待对方接受邀请...';
} else if (this.liveStyle == 4) {
return '邀请你语音通话...';
}
}
},
methods: {
// 获取基本信息
gainInformation(e) {
this.liveStyle = e.style
// 判断跳回层
if (e.style == 0 || e.style == 3) {
this.min = 1;
}
// 时间开始
if (e.style == 2 || e.style == 5) {
this.timess();
}
this.uid = e.uid
this.fid = e.fid
this.ffid = e.ffid
this.uuid = e.uuid
this.fimg = e.fimg
this.fname = e.fname
console.log('推流',this.url);
console.log('拉流',this.src);
},
//测试顺序
testxx() {
new Promise(() => {
this.context = uni.createLivePusherContext("livePusher", this);
console.log('a')
})
},
testsx() {
async () => {
await this.testxx();
console.log('b')
this.loadState(this.liveStyle);
}
},
// 进入该页面到一些预先需要处理到的判断
loadState(e) {
// 判断发起方还是接收方
// if (e == 0 || e == 3) {
// this.changeRinging(1)
// this.start();
// } else if (e == 2 || e == 5) {
// this.changeRinging(0)
// }
if (e == 2 || e == 3) {
this.changeRinging(1)
this.start();
} else if (e == 1 || e == 5) {
this.changeRinging(0)
}
},
// 取消或者拒绝
back(e) {
// 返回上层
uni.navigateBack({
delta: e
})
// 关闭音乐
this.changeRinging(0);
// 关闭推流
// this.stop()
},
changeRinging(e) {
// 控制音频播放与关闭
// e==0为关闭;e==1为开启
uni.$emit('ringing', {
msg: e
})
},
// 切换语音或者视频
changeState(e) {
// 切换各种状态
this.liveStyle = e
if (e == 2 || e == 5) {
// 关闭音乐
this.changeRinging(0);
// 时间开始
this.timess();
}
// 通知对方切换语音
if (e == 5 || e == 3) {
let data = {
uid: this.fid,
fid: this.uid,
e: e,
tip: 6 //已经进入语音
}
uni.$emit('voiceing', data)
}
},
// 获取设备信息
getPhoneStyle() {
uni.getSystemInfo({
success: (res) => {
// console.log(res.windowWidth)
// console.log(res.windowHeight)
this.windowW = res.screenWidth;
this.windowH = res.screenHeight;
this.statusBarH = res.statusBarHeight;
this.bottomH = res.screenHeight - res.safeArea.bottom
}
});
},
// 麦克风切换
tapMkf() {
this.mkf = !this.mkf
if (this.mkf) {
this.mkfTxt = '麦克风已关'
} else {
this.mkfTxt = '麦克风已开'
}
},
// 扬声器切换
tapVioce() {
this.vioce = !this.vioce
if (this.vioce) {
this.vioceTxt = '扬声器已关'
} else {
this.vioceTxt = '扬声器已开'
}
},
// 处理对方反馈
// 对方拒绝
liveCancel() {
uni.$on('liveCancels', (data) => {
this.back(1)
})
},
// 处理接收对方反馈
// 对方接收视频
liveing() {
uni.$on('liveings', (data) => {
this.liveStyle = data.e
// 关闭音乐
this.changeRinging(0)
this.timess();
})
},
// 处理接收对方反馈
// 对方接收语音
changeVoice() {
uni.$on('voiceings', (data) => {
if (data.e == 5) {
this.liveStyle = 5;
// 关闭音乐
this.changeRinging(0);
this.timess();
} else if (data.e == 3) {
this.liveStyle = 3;
}
})
},
// 拒绝对方
cancelLive() {
console.log('拒绝对方11111111');
// 通知对方我已经拒绝了
let data = {
uid: this.fid,
fid: this.uid,
tip: 4 //拒绝
}
uni.$emit('liveback', data)
this.back(2)
},
// 通知对方我已经进入视频
ingLive() {
let data = {
uid: this.fid,
fid: this.uid,
e: 2,
tip: 5 //已经进入视频
}
uni.$emit('liveing', data)
this.liveStyle = 2
// 关闭音乐
this.changeRinging(0)
this.timess();
},
// 计时器
timess() {
let n_s = 1;
let n_m = 0;
setInterval(() => {
let setn_s = n_s;
let setn_m = n_m;
if (n_s < 10) {
setn_s = '0' + n_s;
}
if (n_m < 10) {
setn_m = '0' + n_m;
}
this.timer = setn_m + ':' + setn_s;
n_s++;
if (n_s > 59) {
n_m++;
n_s = 0;
}
}, 1000);
},
statechange(e) {
console.log("statechange:" + JSON.stringify(e));
},
netstatus(e) {
console.log("netstatus:" + JSON.stringify(e));
},
error(e) {
console.log("error:" + JSON.stringify(e));
},
start: function() {
this.context.start({
success: (a) => {
console.log("livePusher.start:" + JSON.stringify(a));
}
});
},
close: function() {
this.context.close({
success: (a) => {
console.log("livePusher.close:" + JSON.stringify(a));
}
});
},
snapshot: function() {
this.context.snapshot({
success: (e) => {
console.log(JSON.stringify(e));
}
});
},
resume: function() {
this.context.resume({
success: (a) => {
console.log("livePusher.resume:" + JSON.stringify(a));
}
});
},
pause: function() {
this.context.pause({
success: (a) => {
console.log("livePusher.pause:" + JSON.stringify(a));
}
});
},
stop: function() {
this.context.stop({
success: (a) => {
console.log(JSON.stringify(a));
}
});
},
switchCamera: function() {
this.context.switchCamera({
success: (a) => {
console.log("livePusher.switchCamera:" + JSON.stringify(a));
}
});
},
startPreview: function() {
this.context.startPreview({
success: (a) => {
console.log("livePusher.startPreview:" + JSON.stringify(a));
}
});
},
stopPreview: function() {
this.context.stopPreview({
success: (a) => {
console.log("livePusher.stopPreview:" + JSON.stringify(a));
}
});
}
}
}
</script>
注意:
推流拉流地址一定要是一样的,需要加上你自己当前的ip地址(手机端无法检测到localhost)
视频接听页面功能+布局
<template>
<view class="call" :style="{paddingTop:statusBarH+'px'}">
<view class="call-live" :style="{width:windowW-16+'px'}">
<image :src="fimg" mode="aspectFill" class="bg-img" :style="{width:windowW-16+'px'}">
</image>
<view class="blur" :style="{width:windowW-16+'px'}"></view>
<view class="call-live-inner" :style="{width:windowW-16+'px'}">
<!-- 用户信息 -->
<view class="flexs" @click="pathPage(callNom)">
<image :src="fimg" mode="aspectFill" class="call-img"></image>
<view class="call-txt">
<text class="call-name">{{fname}}</text>
<text class="call-msg">{{msg}}</text>
</view>
</view>
<!-- 按钮 -->
<view class="flexs">
<!-- 拒绝 -->
<view class="call-refuse call-button" @click="back(1)">
<image src="../../../static/live/phone.png" class="refuse-img"></image>
</view>
<!-- 视频 -->
<view class="call-agree call-button" v-if="callNom==1" @click="pathPage(2)">
<image src="../../../static/live/lives.png" class="live-img"></image>
</view>
<!-- 语音 -->
<view class="call-agree call-button" v-if="callNom==4" @click="pathPage(5)">
<image src="../../../static/live/phone1.png" class="voice-img"></image>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
uid: '',
fid: '',
fimg: '',
fname: '',
windowW: '', //设备宽度
windowH: '', //设备高度
statusBarH: '', //状态栏高度
bottomH: '', //底部安全区域
msg: '邀请你视频通话', // 提示信息
callNom: 1, //判断视频还是语音;1视频 4语音
socketOpen: false,
socketMsgQueue: [],
uuid :'',
ffid:''
}
},
onLoad(e) {
// 接收跳转的数据
this.gainInformation(e);
this.getPhoneStyle();
this.changeRinging(1);
this.changeVoice();
},
onReady() {
},
computed: {
},
methods: {
// 获取基本信息
gainInformation(e) {
this.callNom = e.style
this.uid = e.uid
this.fid = e.fid
this.fimg = e.fimg
this.fname = e.fname
this.uuid = e.uuid
this.ffid = e.ffid
if (e.style == 1) {
this.msg = '邀请你视频通话';
} else {
this.msg = '邀请你语音通话';
}
},
changeRinging(e) {
// 控制音频播放与关闭
// e==0为关闭;e==1为开启
uni.$emit('ringing', {
msg: e
});
},
// 取消或者拒绝
back(e) {
// 返回上层
uni.navigateBack({
delta: e
});
// 关闭音乐
this.changeRinging(0);
// 通知对方我已经拒绝了
let data = {
uid: this.fid,
fid: this.uid,
tip: 4 //拒绝
};
uni.$emit('liveback', data);
},
// 接收视频/页面跳转
pathPage(e) {
if (e == 2) {
// 通知对方我已经进入视频
let data = {
uid: this.fid,
fid: this.uid,
e:2,
tip: 5 //已经进入视频
}
uni.$emit('liveing', data)
}else if(e == 5){
// 通知对方我已经进入语音
let data = {
uid: this.fid,
fid: this.uid,
e:5,
tip: 5 //已经进入语音
}
uni.$emit('liveing', data)
}
uni.navigateTo({
url: "/pages/buyCar/live/live?style=" + e + "&fid=" + this.fid + '&uid=' + this.uid +
'&fname=' + this.fname + '&fimg=' + this.fimg + '&ffid=' + this.ffid + '&uuid=' + this.uuid
});
},
// 对方接收视频或语音
changeVoice() {
uni.$on('voiceings', (data) => {
if (data.e == 3) {
this.callNom = 4;
}
});
},
// 获取设备信息
getPhoneStyle() {
uni.getSystemInfo({
success: (res) => {
this.windowW = res.windowWidth;
this.windowH = res.windowHeight;
this.statusBarH = res.statusBarHeight;
this.bottomH = res.windowHeight - res.safeArea.bottom
}
});
},
}
}
</script>
<style>
.call {
position: fixed;
left: 0;
padding: 16rpx;
}
.call-live {
height: 200rpx;
}
.call-live-inner {
position: absolute;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 200rpx;
align-items: center;
}
.bg-img {
border-radius: 20rpx;
height: 200rpx;
}
.blur {
position: absolute;
border-radius: 20rpx;
height: 200rpx;
top: 0;
left: 0;
background: rgba(0, 0, 0, .6);
backdrop-filter: blur(25rpx);
}
.call-img {
width: 84rpx;
height: 84rpx;
border-radius: 42rpx;
margin-left: 32rpx;
}
.call-txt {
padding-left: 24rpx;
}
.call-name {
font-size: 32rpx;
line-height: 44rpx;
color: #fff;
}
.call-msg {
font-size: 28rpx;
line-height: 40rpx;
color: rgba(255, 255, 255, 0.6);
}
.call-button {
width: 84rpx;
height: 84rpx;
border-radius: 42rpx;
display: flex;
align-items: center;
justify-content: center;
}
.call-refuse {
background-color: #ff5d5b;
margin-right: 64rpx;
}
.call-agree {
background-color: #30c74d;
margin-right: 32rpx;
}
.refuse-img {
width: 56rpx;
height: 56rpx;
}
.live-img {
width: 42rpx;
height: 42rpx;
}
.voice-img {
width: 42rpx;
height: 42rpx;
}
.flexs {
display: flex;
flex-direction: row;
}
</style>
注意:
uniapp路由需要加样式方法(可以让你收到视频通话消息的时候弹出消息页面)
{
"path": "pages/buyCar/live/call",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom", //默认头部去掉
"backgroundColor": "transparent", //页面内容除外的变透明 (微信小程序不支持)
"app-plus": {
"animationType": "slide-in-top", // 从顶部进入
"animationDuration": 300
}
}
},
总结:这些功能包括(发起视频、接收视频、视频中、发起语音、接收语音、语音中)都是需要websocket进行实时通信,这里就不细讲websocket了,需要的话可以看我之前博客
// 视频(跳转推流)
uni.$on('live', (e) => {
Live()
})
const Live = () => {
let data = {
name: user.uname,
img: user.uimgurl,
uid: user.uid,
fid: friend.fid,
tip: 3 //视频
}
uni.sendSocketMessage({
data: JSON.stringify(data)
});
uni.navigateTo({
url: "/pages/buyCar/live/live?style=2&fid=" + friend.fid + '&uid=' + user.uid + '&fname=' + friend
.fname + '&fimg=' + friend.fimgurl + '&ffid=' + friend.fid + '&uuid=' + user.uid
})
}
// 视频接收(socket)
const liveSocket = () => {
uni.onSocketMessage(function(res) {
const msggs = JSON.parse(res.data)
if (msggs.tip == 3) {
// uni.navigateTo({
// url: "/pages/buyCar/live/call?style=1&fid=" + msggs.fid + '&uid=' + msggs.uid +
// '&fname=' + msggs.name + '&fimg=' + msggs.img+ '&ffid=' + friend.fid + '&uuid=' + user.uid
// })
uni.navigateTo({
url: "/pages/buyCar/live/live?style=2&fid=" + friend.fid + '&uid=' + user.uid + '&fname=' + friend
.fname + '&fimg=' + friend.fimgurl + '&ffid=' + friend.fid + '&uuid=' + user.uid
})
}
});
}
// 拒绝视频
// // 对方拒绝发送服务器
uni.$on('liveback', (data) => {
uni.sendSocketMessage({
data: JSON.stringify(data)
});
})
// // 对方拒绝接收
const liveCancel = () => {
uni.onSocketMessage((res) => {
const msggs = JSON.parse(res.data)
if (msggs.tip == 4) {
console.log('拒绝接收222222', msggs);
uni.$emit('liveCancels', msggs)
}
});
}
// 接收视频
// // 对方接收视频发送服务器
uni.$on('liveing', (data) => {
uni.sendSocketMessage({
data: JSON.stringify(data)
});
})
// // 对方接收视频接收
const liveIng = () => {
uni.onSocketMessage((res) => {
const msggs = JSON.parse(res.data)
// console.log('接收视频接收', msggs);
if (msggs.tip == 5) {
uni.$emit('liveings', msggs)
}
});
}
// 接收语音
// // 对方接收语音发送服务器
uni.$on('voiceing', (data) => {
uni.sendSocketMessage({
data: JSON.stringify(data)
});
})
// // 对方接收语音接收
const voiceIng = () => {
uni.onSocketMessage((res) => {
const msggs = JSON.parse(res.data)
// console.log('接收语音接收', msggs);
if (msggs.tip == 6) {
uni.$emit('voiceings', msggs)
}
});
}
二、后端:
下载插件:npm i node-media-server
借助node-media-server实现推流拉流效果,具体操作可以查看官方文档https://github.com/illuspas/Node-Media-server/blob/master/README_CN.md
nodeJs代码
app.js编写
// 引入live
const NodeMediaServer = require('node-media-server');
const config = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 30,
ping_timeout: 60
},
http: {
port: 8000,
allow_origin: '*'
}
};
var nms = new NodeMediaServer(config)
nms.run();
我用的是一个RTMP因为uniapp组件的live-pusher推流支持RTMP:RTMP 的最大优点是可以在服务器和客户端服务器之间保持稳定的连接,无论用户的互联网连接质量如何,它都可以无缝低延迟进行流媒体传输。这个技术主要通过将数据流分成相等的小部分(音频数据默认为 64 字节,视频数据默认为 128 字节)并将它们顺序传输到接收设备,然后将它们重新组合成视频流来实现的。
效果:
技术细节
需要通过websocket来实现视频通话,推流拉流地址一定要相同,推流拉流需要借助后端node-media-server 来实现
小结
可以关注本人,会持续更新,感谢观看