vue集成zego即构实现实时音视频
2021-01-05 本文已影响0人
风中凌乱的男子
主播端
image.png
观众端
image.png
第一步,肯定是要先实现这样的一个三分屏直播间样式,直接贴代码了
<template>
<div class="main">
<div class="row">
<div class="left" :style="obj">
<!-- top -->
<div class="top">
<span>频道号:2066909 <i>无直播</i> </span>
<span>经典儿童言语康复案例解析</span>
<span>
<i>网络延时:10ms</i>
<i style="padding: 0 2px;">丢包率:{{videoPacketsLostRate}}%</i>
<i> <span></span><span class="span"></span><span></span>网络状态</i>
</span>
</div>
<!-- bottom -->
<div class="bottom">
<div class="bottom_l">
<div class="t">
<!-- tab -->
<div class="baiban">
<i class="el-icon-edit-outline" style="font-size:24px;"></i>
<p>白板</p>
</div>
<div class="baiban">
<i class="el-icon-document" style="font-size:24px;"></i>
<p>课件</p>
</div>
<div class="baiban">
<i class="el-icon-video-camera" style="font-size:24px;"></i>
<p>多媒体</p>
</div>
<div class="baiban">
<i class="el-icon-monitor" style="font-size:24px;"></i>
<p>屏幕共享</p>
</div>
<div class="baiban yingyong" @mouseenter="onMousehoverYing($event)" @mouseleave="onMouseleaveYing($event)">
<i class="el-icon-copy-document" style="font-size:24px;"></i>
<p>应用</p>
</div>
</div>
<div class="b">
<span>
<el-button class="oneBtn" type="primary" @click="startPublishingStream" circle>开始</el-button>
</span>
<span>
<el-button type="primary" class="btn two" size="small" icon="el-icon-share" circle></el-button>
</span>
<span>
<el-button type="primary" class="btn" size="small" icon="el-icon-setting" circle></el-button>
</span>
</div>
</div>
<div class="bottom_r">
<!-- 应用bar -->
<div class="bar" v-show="showBar" @mouseenter="onMousehoverYing($event)" @mouseleave="onMouseleaveYing($event)" style="display: none;">
<!-- tab -->
<div class="baiban">
<i class="el-icon-location-outline" style="font-size:24px;"></i>
<p>签到</p>
</div>
<div class="baiban">
<i class="el-icon-date" style="font-size:24px;"></i>
<p>公告</p>
</div>
<div class="baiban">
<i class="el-icon-document-copy" style="font-size:24px;"></i>
<p>问卷</p>
</div>
<div class="baiban">
<i class="el-icon-document-checked" style="font-size:24px;"></i>
<p>答题卡</p>
</div>
<div class="baiban">
<i class="el-icon-trophy" style="font-size:24px;"></i>
<p>抽奖</p>
</div>
</div>
<!-- 白板区域 -->
白板区域 <br>
白板区域 <br>
白板区域 <br>
白板区域 <br>
白板区域 <br>
白板区域 <br>
</div>
</div>
</div>
<div class="right" :style="obj">
<!-- avatar -->
<div class="avatar" id="local_stream" @mouseenter="onMousehoverEnv($event)" @mouseleave="onMouseleaveEnv($event)">
<!-- toobar -->
<div class="toobar" style="display: none;">
<span>
<img :src="getItemIcon1()" alt="" v-show='showTagVideo' @click="closeVideo" class="imgIcon" width="30">
<img :src="getItemIcon2()" alt="" v-show='!showTagVideo' @click="openVideo" class="imgIcon" width="30">
</span>
<span style="margin:0 15px;">
<img :src="getItemIcon3()" class="imgIcon" alt="" width="30">
</span>
<span>
<img :src="getItemIcon4()" class="imgIcon" alt="" width="30">
</span>
</div>
<!-- 底部状态栏 -->
<div class="b_toobar">
<span>(我)2076282</span>
<span>
<i class="el-icon-bell" style="font-size:20px;margin-right:5px;"></i>
<i class="el-icon-microphone" style="font-size:20px;"></i>
</span>
</div>
<!-- avatar_live.png -->
<div class="live" v-show="!showTagVideo">
<img src="http://erkong.ybc365.com/b4576202012231731231009.png" alt="">
</div>
<!-- 视频区域 -->
<video v-show="showTagVideo" autoplay muted id='video' :src-object.prop="stream" width="100%" height="100%"></video>
</div>
<!-- tabs -->
<ul class="tabs">
<li class="li-tab" v-for="(item,index) in tabsParam" @click="toggleTabs(index)" :class="{active:index==nowIndex}">
{{item =='成员'?item+`(${index})`:item}}</li>
</ul>
<div class="divTab1" v-show="nowIndex===0">
<div class="content">
content
</div>
<div class="send">
<div class="check">
<span>
<i class="el-icon-picture-outline-round biaoqingicon"></i>
</span>
<span>
<el-checkbox v-model="checked">屏蔽打赏</el-checkbox>
</span>
</div>
<div class="input">
<el-input class="textarea" type="textarea" placeholder="我也来参与一下互动" v-model="textarea" maxlength="200" show-word-limit rows="3"
resize='none'>
</el-input>
<div class="sendBtn">
<div>聊天室已开启</div>
<div>
<span style="margin-right:15px;">0/200</span>
<span class="senBtntxt">
发送
</span>
</div>
</div>
</div>
</div>
</div>
<div class="divTab2" v-show="nowIndex===1">
22
</div>
</div>
</div>
</div>
</template>
<script>
const controlBtnList = [
{
name: 'camera',
cnName: '摄像头',
imgSrc: {
open: require('../assets/icons/room/mumber_camer.svg'),
close: require('../assets/icons/room/mumber_camer_close.svg')
}
},
{
name: 'mic',
cnName: '麦克风',
imgSrc: {
open: require('../assets/icons/room/mumber_micophone.svg'),
close: require('../assets/icons/room/mumber_micophone_close.svg')
}
},
]
export default {
data() {
return {
showTagVideo: true,
showBar: false,
checked: false,
textarea: "",
tabsParam: ['聊天', '成员'],
nowIndex: 0,//默认第一个tab为激活状态
obj: {
height: (document.documentElement.clientHeight || document.body.clientHeight) + 'px',
},
controlBtnList,
videoPacketsLostRate: 0
}
},
methods: {
getItemIcon1() {
return this.controlBtnList[0].imgSrc.open
},
getItemIcon2() {
return this.controlBtnList[0].imgSrc.close
},
getItemIcon3() {
return this.controlBtnList[1].imgSrc.open
},
getItemIcon4() {
return this.controlBtnList[1].imgSrc.close
},
onMousehoverEnv(e) {
e.target.parentElement.querySelector(".toobar").style.display = "block"
},
onMouseleaveEnv(e) {
e.target.parentElement.querySelector(".toobar").style.display = "none"
},
onMousehoverYing(e) {
this.showBar = true
},
onMouseleaveYing(e) {
this.showBar = false
},
toggleTabs(index) {
this.nowIndex = index;
},
// 关闭摄像头
closeVideo() {
this.showTagVideo = !this.showTagVideo
this.$message.error('摄像头已关闭,观众将看不到您的画面')
},
// 启用摄像头
openVideo() {
this.showTagVideo = !this.showTagVideo
this.$message.success('摄像头已打开')
},
},
mounted() {
window.onresize = () => {
return (() => {
this.obj.height = (document.documentElement.clientHeight
|| document.body.clientHeight) + 'px'
})()
}
},
watch: {
showBar() {
if (this.showBar) {
document.querySelector(".yingyong").style.color = "#3595fb"
document.querySelector(".yingyong").style.background = "#363644"
} else {
document.querySelector(".yingyong").style.color = ""
document.querySelector(".yingyong").style.background = ""
}
}
}
}
</script>
<style lang="scss" scoped>
.main {
.row {
display: flex;
.left {
background: #eee;
min-height: 650px;
flex: 1;
min-width: 1000px;
.top {
height: 49px;
background: #191a1c;
display: flex;
justify-content: space-between;
align-items: center;
color: #adadc0;
font-size: 14px;
padding: 0 15px;
span {
&:first-child {
i {
font-style: normal;
border: 1px solid #adadc0;
border-radius: 2px;
padding: 0 8px;
margin-left: 16px;
color: #fff;
}
}
&:nth-child(2) {
color: #fff;
font-size: 16px;
}
&:nth-child(3) {
i {
font-style: normal;
&:nth-child(2) {
margin: 0 12px;
}
&:nth-child(3) {
span {
display: inline-block;
width: 1px;
background: #5fb430;
&:first-child {
height: 4px;
}
&:nth-child(2) {
height: 7px;
}
&:nth-child(3) {
height: 10px;
margin-right: 5px;
}
}
.span {
margin: 0 3px;
}
}
}
}
}
}
.bottom {
display: flex;
height: calc(100% - 49px);
width: 100%;
font-size: 13.5px;
.bottom_l {
background: #191a1c;
height: 100%;
width: 80px;
color: #adadc0;
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: center;
.t {
width: 100%;
height: 50%;
p {
margin: 0;
}
.baiban {
width: 100%;
height: 75px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
margin-top: 6px;
}
&:hover {
background: #363644;
cursor: pointer;
color: #3595fb;
}
}
}
.b {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 15px 0;
.oneBtn {
width: 55px;
height: 55px;
}
.btn {
background: #26272e;
border-color: #26272e;
font-size: 16px;
}
.two {
margin: 15px 0;
}
}
}
.bottom_r {
flex: 1;
background: #f5f7ff;
height: 100%;
position: relative;
.bar {
width: 80px;
background: #363644;
height: 100%;
color: #adadc0;
position: absolute;
top: 0px;
p {
margin: 0;
}
.baiban {
width: 100%;
height: 75px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
margin-top: 6px;
}
&:hover {
background: #363644;
cursor: pointer;
color: #fff;
}
}
}
}
}
}
.right {
background: #191a1c;
min-height: 650px;
min-width: 256px;
width: 256px;
.avatar {
width: 100%;
height: 168px;
background: #363636;
color: #fff;
position: relative;
box-shadow: inset 0px -6px 18px -10px #000;
.toobar {
position: absolute;
bottom: 0;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
text-align: center;
line-height: 150px;
z-index: 99;
.imgIcon {
cursor: pointer;
}
}
.b_toobar {
width: 100%;
position: absolute;
bottom: 0;
font-size: 12px;
padding: 2px 4px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 98;
}
.live {
position: absolute;
bottom: 0;
top: 25%;
left: 0;
right: 0;
text-align: center;
img {
width: 66px;
}
}
}
.tabs {
padding: 0;
margin: 0;
width: 100%;
display: flex;
justify-content: space-around;
background: #363644;
color: #fff;
font-size: 13px;
.li-tab {
height: 100%;
display: inline-block;
text-align: center;
padding: 10px 0 5px 0;
&:hover {
cursor: pointer;
}
}
.active {
border-bottom: 2px solid #fff;
}
}
.divTab1 {
color: #fff;
font-size: 12px;
height: calc(100% - 341px);
.content {
padding: 10px 10px 0 10px;
height: 100%;
}
.send {
height: 142px;
// background: red;
.check {
height: 30px;
background: #363644;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
.biaoqingicon {
cursor: pointer;
font-size: 20px;
}
::v-deep .el-checkbox__input.is-checked + .el-checkbox__label {
color: #fff;
}
::v-deep .el-checkbox {
color: #fff;
}
::v-deep .el-checkbox__label {
margin-top: 1px;
}
}
.input {
height: calc(100% - 30px);
position: relative;
.textarea {
height: 85%;
::v-deep .el-textarea__inner {
border: none;
background-color: transparent;
color: #fff;
font-size: 12px;
&::placeholder {
font-size: 12px;
}
}
}
.sendBtn {
position: absolute;
bottom: 0;
background: #333;
width: 100%;
padding: 12px 10px;
display: flex;
justify-content: space-between;
align-items: center;
.senBtntxt {
background: #3595fb;
padding: 4px 14px;
border-radius: 30px;
cursor: pointer;
}
}
}
}
}
}
#video {
object-fit: cover;
}
}
}
</style>
附上svg图片
//mumber_camer_close.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#737680" d="M9,1A5.9,5.9,0,0,0,5.52,2.12l1,1A4.53,4.53,0,0,1,9,2.4,4.6,4.6,0,0,1,13.6,7a4.53,4.53,0,0,1-.73,2.47l1,1A5.9,5.9,0,0,0,15,7,6,6,0,0,0,9,1Zm4.8,14.2-.6-.6H4.62l1.8-1.2h5.16l1.26.84-2.77-2.77A4.18,4.18,0,0,1,9,11.6,4.6,4.6,0,0,1,4.4,7a4.18,4.18,0,0,1,.13-1.07L3.41,4.81A6.13,6.13,0,0,0,3,7a6,6,0,0,0,2.85,5.1L3,14v2H15v-.3A1.7,1.7,0,0,1,13.8,15.2Z"/>
<path class="hover-fill" fill="#737680" d="M6,7.44A3,3,0,0,0,8.56,10ZM9,4a3,3,0,0,0-1.3.3L8.82,5.42A.55.55,0,0,1,9,5.4,1.6,1.6,0,0,1,10.6,7a.55.55,0,0,1,0,.18L11.7,8.3A3,3,0,0,0,12,7,3,3,0,0,0,9,4Z"/>
<path class="hover-fill" fill="#737680" d="M15,14.7a.74.74,0,0,1-.5-.2L3,3A.71.71,0,0,1,4,2L15.5,13.5a.72.72,0,0,1,0,1A.74.74,0,0,1,15,14.7Z"/>
</svg>
//mumber_camer.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="hover-fill" fill="none" width="18" height="18"/>
<path class="hover-fill" fill="#0044ff" d="M9,2.4A4.6,4.6,0,1,1,4.4,7,4.6,4.6,0,0,1,9,2.4M9,1a6,6,0,1,0,6,6A6,6,0,0,0,9,1Z"/>
<path class="hover-fill" fill="#0044ff" d="M9,5.4A1.6,1.6,0,1,1,7.4,7,1.6,1.6,0,0,1,9,5.4M9,4a3,3,0,1,0,3,3A3,3,0,0,0,9,4Z"/>
<path class="hover-fill" fill="#0044ff" d="M11.58,13.4l1.8,1.2H4.62l1.8-1.2h5.16M12,12H6L3,14v2H15V14l-3-2Z"/>
</svg>
//mumber_micophone_close.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#737680" d="M9,2A3,3,0,0,0,6.6,3.2l1,1A1.55,1.55,0,0,1,9,3.4,1.6,1.6,0,0,1,10.6,5V7.2L12,8.55A3.31,3.31,0,0,0,12,8V5A3,3,0,0,0,9,2ZM6,8a3,3,0,0,0,3,3A3.31,3.31,0,0,0,9.55,11L6,7.4Z"/>
<path class="hover-fill" fill="#737680" d="M10.58,12l1.05,1A5.46,5.46,0,0,1,3.55,8.25a.7.7,0,0,1,1.4,0A4.06,4.06,0,0,0,9,12.3,4.17,4.17,0,0,0,10.58,12Z"/>
<path class="hover-fill" fill="#737680" d="M14.45,8.25a5.3,5.3,0,0,1-.51,2.29L12.86,9.46a3.74,3.74,0,0,0,.19-1.21.7.7,0,1,1,1.4,0Z"/>
<path class="hover-fill" fill="#737680" d="M11.5,14.8h-5a.7.7,0,0,0,0,1.4h5a.7.7,0,0,0,0-1.4Z"/>
<path class="hover-fill" fill="#737680" d="M15,14.7a.74.74,0,0,1-.5-.2L3,3A.71.71,0,0,1,4,2L15.5,13.5a.72.72,0,0,1,0,1A.74.74,0,0,1,15,14.7Z"/>
</svg>
//mumber_micophone.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<rect class="cls-1" width="18" height="18"/>
<path class="hover-fill" fill="#0044ff" d="M9,3.4A1.6,1.6,0,0,1,10.6,5V8A1.6,1.6,0,0,1,7.4,8V5A1.6,1.6,0,0,1,9,3.4M9,2A3,3,0,0,0,6,5V8a3,3,0,0,0,6,0V5A3,3,0,0,0,9,2Z"/>
<path class="hover-stroke" fill="none" stroke="#0044ff" d="M13.75,8.25A4.75,4.75,0,0,1,9,13H9A4.75,4.75,0,0,1,4.25,8.25"/>
<line class="hover-stroke" fill="none" stroke="#0044ff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" x1="6.5" y1="15.5" x2="11.5" y2="15.5"/>
</svg>
第二步,装zego即构的SDK,
npm install zego-express-engine-webrtc --save
然后在页面script内引入
import { ZegoExpressEngine } from 'zego-express-engine-webrtc'
第三步,在data内新增几个值
userID: '888',
AppID: 1974122008,
AppSign: '52db995b730066c45a659f8f4217676eb56346d621f98f97b50dca32882cd98a',
zg: {},
token1: "",
stream: {},
第四步,在mounted初始化实例
// 初始化实例
this.zg = new ZegoExpressEngine(this.AppID, 'wss://webliveroom-test.zego.im/ws')
// 监听zg连接状态
this.zg.on('roomStateUpdate', (roomID, state, errorCode, extendedData) => {
if (state == 'DISCONNECTED') {
// 与房间断开了连接
console.log('与房间断开了连接');
}
if (state == 'CONNECTING') {
// 与房间尝试连接中
console.log('与房间尝试连接中');
}
if (state == 'CONNECTED') {
// 与房间连接成功
this.$message.success('登陆成功')
}
})
// 监听推流状态
this.zg.on('publisherStateUpdate', result => {
// 推流状态变更通知
this.$message.success('推流成功')
})
this.zg.on('publishQualityUpdate', (streamID, stats) => {
// 推流质量
console.log(stats);
this.videoPacketsLostRate = (stats.video.videoTransferFPS).toFixed(2)
})
第五步,在methods里新写一个获取token的方法
// 获取token的方法
getTokenFun(appID, userID) {
return new Promise((resolve, reject) => {
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = e => {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
resolve(xmlhttp.response);
} else {
reject(e);
}
}
};
xmlhttp.open(
"GET",
`https://wsliveroom-alpha.zego.im:8282/token?app_id=${appID}&id_name=${userID}`,
true
);
xmlhttp.send(null);
});
},
第六步,继续写方法,获取token,进入房间
// 获取token
async getToken() {
this.token1 = await this.getTokenFun(this.AppID, this.userID)
this.loginRoom()
},
// 登陆房间
async loginRoom() {
const result = await this.zg.loginRoom('666', this.token1, { userID: this.userID, userName: 'aaa' });
this.createStr()
},
// 创建流和渲染
async createStr() {
this.stream = await this.zg.createStream();
},
// 开始推流、开始直播
startPublishingStream() {
this.zg.startPublishingStream('123', this.stream)
}
最后一步,在mounted里执行一个方法
// 获取token执行登陆房间操作
this.getToken()
主播端推流 就搞定了
下面是观众端拉流
比较简单,直接贴出来吧
<template>
<div>
<div id="remote_stream">
<video autoplay muted id='video' :src-object.prop="stream" width="100%" height="100%"></video>
</div>
</div>
</template>
<script>
import { ZegoExpressEngine } from 'zego-express-engine-webrtc'
export default {
data() {
return {
userID: '999',
AppID: 1974122008,
AppSign: '52db995b730066c45a659f8f4217676eb56346d621f98f97b50dca32882cd98a',
zg: {},
token1: "",
stream: {},
}
},
methods: {
// 获取token的方法
getTokenFun(appID, userID) {
return new Promise((resolve, reject) => {
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = e => {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
resolve(xmlhttp.response);
} else {
reject(e);
}
}
};
xmlhttp.open(
"GET",
`https://wsliveroom-alpha.zego.im:8282/token?app_id=${appID}&id_name=${userID}`,
true
);
xmlhttp.send(null);
});
},
// 获取token
async getToken() {
this.token1 = await this.getTokenFun(this.AppID, this.userID)
this.loginRoom()
},
// 登陆房间
async loginRoom() {
const result = await this.zg.loginRoom('666', this.token1, { userID: this.userID, userName: 'bbb' });
},
// 开始 拉流
async startPlayingStream() {
this.stream = await this.zg.startPlayingStream('123');
console.log(stream);
}
},
mounted() {
// 初始化实例
this.zg = new ZegoExpressEngine(this.AppID, 'wss://webliveroom-test.zego.im/ws')
// 获取token执行登陆房间操作
this.getToken()
// 监听zg连接状态
this.zg.on('roomStateUpdate', (roomID, state, errorCode, extendedData) => {
if (state == 'DISCONNECTED') {
// 与房间断开了连接
console.log('与房间断开了连接');
}
if (state == 'CONNECTING') {
// 与房间尝试连接中
console.log('与房间尝试连接中');
}
if (state == 'CONNECTED') {
// 与房间连接成功
this.$message.success('登陆成功')
}
})
this.zg.on('roomStreamUpdate', (roomID, updateType, streamList, extendedData) => {
if (updateType == 'DELETE') {
// 与房间断开了连接
console.log('与房间断开了连接');
this.$message.error('停止拉流!')
this.zg.stopPlayingStream('123')
}
if (updateType == 'ADD') {
// 与房间尝试连接中
this.startPlayingStream()
}
})
// 监听拉流状态
this.zg.on('playerStateUpdate', result => {
// 处理拉流状态
console.log(result);
})
this.zg.on('playQualityUpdate', (streamID, stats) => {
// 拉流质量回调
})
}
}
</script>
<style lang="scss" scoped>
#remote_stream {
width: 375px;
height: 180px;
background: #333;
}
</style>