近期由于項(xiàng)目需要,本來(lái)集成的TRTC切換成MRTC
現(xiàn)在就MRTC集成做個(gè)簡(jiǎn)單整理铺峭,這里主要介紹web和H5的集成
1贩绕、MRTC簡(jiǎn)介
官網(wǎng)介紹
音視頻通話組件( Mobile Real-Time Communication,簡(jiǎn)稱 MRTC)是 mPaaS 提供的音頻董朝、視頻通話組件灰瞻。該組件功能豐富腥例,提供純語(yǔ)音通話和視頻通話功能,支持 PC酝润、移動(dòng)端燎竖、IoT 設(shè)備等多終端接入。音視頻通話可實(shí)現(xiàn)一對(duì)一通話及多人會(huì)議要销,通話過(guò)程中支持屏幕錄制构回、屏幕共享、截圖等功能疏咐,同時(shí)支持即時(shí)文字消息和文件傳輸纤掸。此外,支持實(shí)時(shí)語(yǔ)音識(shí)別浑塞,能夠識(shí)別對(duì)端的語(yǔ)音確認(rèn)借跪,輔助本端判斷對(duì)端的意向;點(diǎn)播功能可實(shí)現(xiàn)在視頻通話過(guò)程中酌壕,播放視頻掏愁、PPT 等多種提示畫(huà)面。
多種參與模式:支持一對(duì)一視頻通話及多人視頻通話卵牍。
多平臺(tái):支持 iOS果港、Android、PC Web糊昙、H5 以及小程序辛掠。
多端互通:支持手機(jī)、PC释牺、IoT 設(shè)備之間互聯(lián)互通萝衩。
會(huì)話保持:網(wǎng)絡(luò)短暫異常回挽、網(wǎng)絡(luò)切換時(shí),業(yè)務(wù)流程不中斷欠气,保持會(huì)話的持續(xù)性厅各。
自定義視頻規(guī)格镜撩、自適應(yīng)視頻規(guī)格:支持自定義寬预柒、高、最大幀率袁梗、最大碼率宜鸯,并能在上限范圍內(nèi)根據(jù)網(wǎng)絡(luò)狀況自適應(yīng)調(diào)整視頻規(guī)格。
2遮怜、MRTC的集成
在官網(wǎng)的集成上在詳細(xì)介紹下淋袖,做個(gè)二次封裝,相關(guān)接口可查看官方文檔
官方流程圖
思路:
1锯梁、封裝集成JS方法 (可使用mixins方式)
2即碗、封裝UI組件(呼叫組件,視頻通話組件陌凳,接聽(tīng)組件)
3剥懒、在業(yè)務(wù)的基礎(chǔ)上封裝相關(guān)業(yè)務(wù)音視頻SDK
3、具體實(shí)現(xiàn)(vue版)
1合敦、下載SDK初橘,引入項(xiàng)目
下載 artvc-web-sdk,把lib文件引入到項(xiàng)目中
項(xiàng)目按需引入對(duì)應(yīng)的js(在index.html里面)
<script src="./lib/adapter.js"></script>
<script src="./lib/meeting_api.js"></script>
<script src="./lib/mcu.js"></script>
<script src="./lib/meeting_camera_stream.js"></script>
<script src="./lib/meeting_invite.js"></script>
2、實(shí)例化 SDK
const test_controller = new McuController() // 實(shí)例化 SDK
this.test_controller = test_controller
3充岛、建立連接
init() {
const test_controller = this.test_controller
const config_param = {}
config_param.uid = '6189'
config_param.biz_name = 'demo'
config_param.sub_biz = 'default'
config_param.workspaceId = 'default'
config_param.room_server_url = 'wss://服務(wù)地址'
config_param.sign = this. getSign()
// 允許最大斷網(wǎng)時(shí)間 (超過(guò)未重連, 直接關(guān)閉)
config_param.network_check_timeout = 120 * 1000
test_controller.Connect(config_param)
}
// 注意:簽名應(yīng)該是后臺(tái)返回的保檐,這是demo可寫(xiě)死
// 簽名(通道建連/創(chuàng)建房間/加入房間需要)
getSign(uid, isRecord = false) {
const test_controller = this.test_controller
test_controller.trace(`GetSign uid=${uid}`)
return 'signature'
},
4、初始化回調(diào)方法
所有的回調(diào)方法都在這里監(jiān)聽(tīng)
initCallback() {
const test_controller = this.test_controller
// 建立連接成功回調(diào)
test_controller.OnConnectOK = () => {
// this.initRoom()
console.log('建立連接成功')
}
// 建立連接失敗回調(diào)
test_controller.OnConnectFailed = function(code, msg) {
console.log(code, msg)
console.log('建立連接失敗, 請(qǐng)嘗試https修復(fù)')
}
// 房間初始化成功
test_controller.OnInitRoomConfigOK = () => {
console.log('房間初始化成功')
if (this.role === 'created') {
this.createRoom()
} else if (this.role === 'join') {
this.joinRoom()
}
}
// 房間初始化失敗
test_controller.OnInitRoomConfigFail = function(err_code, err_msg) {
console.log(err_code, err_msg)
console.log('房間初始化失敗')
}
// 創(chuàng)建房間成功回調(diào)
test_controller.OnCreateRoomSucc = (room_id, rtoken) => {
console.log( room_id, rtoken)
this.isHiddenVideo = false
this.typeState = '0'
this.messageSend({
toUserId: '9232131735',
userId: '123',
type: '1',
roomNumber: room_id,
passWord: rtoken
})
// test_controller.JoinRoom(room_id, rtoken, this.getSign())
console.log('創(chuàng)建房間成功')
}
// 創(chuàng)建房間失敗回調(diào)
test_controller.OnCreateRoomFailed = function(err_code, err_msg) {
console.log(err_code, err_msg)
console.log('創(chuàng)建房間失敗')
}
// 加入房間成功
test_controller.OnJoinRoomSucc = () => {
console.log('加入房間成功')
this.isHiddenVideo = false
this.typeState = '0'
}
// 加入房間失敗
test_controller.OnJoinRoomFailed = function(err_code, err_msg) {
console.log(err_code, err_msg)
console.log('加入房間失敗')
}
test_controller.OnPublishSucc = (sid) => {
this.timeStart()
console.log('發(fā)布訂閱')
}
// 訂閱成功回調(diào)
test_controller.OnSubscribeSucc = function(feedId, sid) {
test_controller.trace(`~~~~~~~~~~~~~ OnSubscribeSuccess Response , sid=${sid},feedId=${feedId}`)
console.log('訂閱成功回調(diào)')
}
// 邀請(qǐng)成功
test_controller.OnInviteOK = function() {
console.log('邀請(qǐng)成功回調(diào)')
}
// 邀請(qǐng)失敗
test_controller.OnInviteFail = function(code, msg) {
console.log('邀請(qǐng)失敗回調(diào)')
}
test_controller.OnReplyInviteOK = () => {
console.log('回復(fù)邀請(qǐng)回調(diào)')
}
// 退出房間回調(diào)
test_controller.OnLeaveRoom = (leaveType) => {
test_controller.warning(`~~~~~~~~~~~~~ leave room! leaveType = ${leaveType}`)
console.log('退出房間成功')
this.onTimeReset()
this.isHiddenVideo = true
}
// 退出房間回調(diào)
test_controller.OnParticipantLeaveRoom = (participant, exitType) => {
test_controller.warning(`~~~~~~~~~~~~~ leave room! leaveType = ${participant}${exitType}`)
console.log('對(duì)方退出房間成功')
this.onQuit()
}
}
5崔梗、初始化房間
initRoom(type) {
// type 是區(qū)分是加入房間還是創(chuàng)建房間
if (type) {
this.role = type
}
const test_controller = this.test_controller
const config_param = {
auto_publish_subscribe: 3,
media_type: 1,
publish_device: 1,
initSubscribe: [
{
subscribe_video_id: 'video0',
subscribe_audio_id: 'audio0',
subscribe_streamId_id: 'subscribe_streamId0',
feedId_id: 'feedId0'
}, {
subscribe_video_id: 'video4',
subscribe_audio_id: 'audio4'
}],
initPublish: [
{
publish_video_id: 'publish_video1',
publish_streamId_id: 'publish_streamId1',
publish_tag: 'VIDEO_SOURCE_CAMERA_1'
}
]
}
test_controller.InitRoomConfig(config_param)
}
6夜只、創(chuàng)建房間
createRoom() {
const test_controller = this.test_controller
test_controller.CreateRoom(this.getSign())
},
7、發(fā)布訂閱
注意:如果初始化的時(shí)候是自動(dòng)發(fā)布訂閱蒜魄,則創(chuàng)建房間之后不需要手動(dòng)發(fā)布訂閱扔亥,否則需要手動(dòng)發(fā)布訂閱
onPublish() {
const test_controller = this.test_controller
const config_param = {
'media_type': 1,
'need_volume_analyser': true,
'publish_video_id': 'publish_video1',
'publish_streamId_id': 'publish_streamId1',
'aspectRatioStrongDepend': false,
'aspectRatio': '0',
'video_profile_type': '2',
'publish_tag': 'VIDEO_SOURCE_CAMERA',
'enableVideo': true,
'enableAudio': true,
'publish_device': 1,
'transport_': 'all',
'defaultTurnServer': '',
'degradationType': 1,
'scalabilityMode': 'NONE'
}
test_controller.Publish(config_param)
}
8、退出房間
onLeaveRoom() {
const test_controller = this.test_controller
test_controller.LeaveRoom()
},
9权悟、視頻UI組件
<template>
<div id="videos" v-drag class="video-div" :class="isHiddenVideo?'display-none':'display-block'">
<div class="publishVideo">
<video
id="publish_video1"
autoplay
muted="true"
webkit-playsinline="true"
playsinline="true"
width="100%"
height="100%"
style="object-fit: cover;"
/>
<div class="time">{{ time }}</div>
<div class="video-tool">
<img class="img-gd" src="@/assets/images/jj.png" alt="" @click="onQuit">
</div>
<div class="subscribeVideo">
<video
id="video0"
autoplay
muted
width="100%"
height="100%"
webkit-playsinline="true"
playsinline="true"
style="object-fit: cover;"
>
video
</video>
<audio id="audio0" autoplay>音頻</audio>
<video id="video99" autoplay muted="true" width="100%" height="480" hidden>
video
</video>
<audio id="audio99" autoplay hidden>音頻</audio>
<br>
<label id="subscribe_feedId_text0" type="text" class="hiddenForMobile" hidden> feedId:</label>
<label id="feedId0" class="css-text-color hiddenForMobile" type="text" />
<br>
<label id="subscribe_streamId_text0" type="text" hidden class="hiddenForMobile"> streamId:</label>
<label id="subscribe_streamId0" class="css-text-color hiddenForMobile" type="text" />
</div>
</div>
</template>
<script>
export default {
name: 'Index',
// 自定義指令
directives: {
drag: {
// 指令的定義
bind: function(el) {
const oDiv = el // 獲取當(dāng)前元素
oDiv.onmousedown = (e) => {
console.log('onmousedown')
// 算出鼠標(biāo)相對(duì)元素的位置
const disX = e.clientX - oDiv.offsetLeft
const disY = e.clientY - oDiv.offsetTop
document.onmousemove = (e) => {
// 用鼠標(biāo)的位置減去鼠標(biāo)相對(duì)元素的位置砸王,得到元素的位置
const left = e.clientX - disX
const top = e.clientY - disY
oDiv.style.left = left + 'px'
oDiv.style.top = top + 'px'
}
document.onmouseup = (e) => {
document.onmousemove = null
document.onmouseup = null
}
}
}
}
},
props: {
isHiddenVideo: {
type: Boolean,
default: true
},
time: {
type: String,
default: '00:00:00'
}
},
methods: {
onQuit() {
this.$emit('onQuit')
},
onInappropriate() {
this.$emit('onInappropriate')
},
onLooks() {
this.$emit('onLooks')
},
onOffer() {
this.$emit('onOffer')
}
}
}
</script>
<style lang="scss" scoped>
.display-none {
display: none;
}
.display-block {
display: block;
}
.video-div {
position: absolute;
top: 40px;
right: 10px;
width: 500px;
height: 600px;
overflow: hidden;
background: #001528;
border-radius: 20px;
.publishVideo {
width: 100%;
height: 100%;
.time {
position: absolute;
top: 10px;
right: 0;
width: 100px;
height: 40px;
line-height: 40px;
color: white;
}
.video-tool {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: rgba(0,0,0,.5);
.img-gd {
position: absolute;
top: -20px;
right: calc(50% - 20px);
z-index: 99;
width: 40px;
}
.btn-class {
position: relative;
width: 320px;
height: 100px;
margin: 0 auto;
//background: red;
}
.looks {
position: absolute;
left: 120px;
}
.offer {
position: absolute;
left: 220px;
}
.inappropriate {
position: absolute;
left: 20px;
}
.btn-base {
bottom: 25px;
width: 90px;
height: 40px;
font-size: 12px;
line-height: 40px;
color: white;
text-align: center;
background: #000;
border-radius: 20px;
}
}
}
.subscribeVideo {
position: absolute;
top: 50px;
right: 10px;
width: 200px;
height: 200px;
overflow: hidden;
//background-color: #409eff;
border-radius: 10px;
}
}
</style>
10、接聽(tīng)UI組件
<template>
<div class="invite-video">
<div class="user-info">
<img class="img-head" :src="callUserInfo.avatar?callUserInfo.avatar:'/company/static/images/system/user_avatar_default.png'" alt="">
<div class="info-name"><span class="name">{{ callUserInfo.name }}</span><span> {{callUserInfo.attrs}} </span></div>
<div class="info-text"><span>請(qǐng)求與你視頻面試</span></div>
</div>
<img class="img-jj" src="../../assets/image/jj.png" alt="" @click="onAction('1')">
<img class="img-splj" src="../../assets/image/splj.png" alt="" @click="onAction('2')">
</div>
</template>
<script>
export default {
name: 'Index',
props:{
callUserInfo:{
type:Object,
default:()=>{
return {
avatar:null,
name:null,
attrs:null
}
}
}
},
methods: {
onAction(type) {
this.$emit('onAction', type)
}
}
}
</script>
<style lang="scss" scoped>
.invite-video {
position: absolute;
top: 20px;
right: calc(50% - 200px);
width: 400px;
height: 150px;
overflow: hidden;
background: #001528;
border-radius: 10px;
.user-info {
position: absolute;
top: 0;
right: 0;
width: 400px;
height: 80px;
//background: red;
overflow: hidden;
.info-name {
position: absolute;
top: 15px;
left: 90px;
width: 300px;
height: 20px;
font-size: 14px;
color: #666;
.name {
font-size: 20px;
color: white
}
}
.info-text {
position: absolute;
bottom: 15px;
left: 90px;
width: 300px;
height: 20px;
font-size: 14px;
color: white;
}
.img-head {
position: absolute;
bottom: 15px;
left: 20px;
width: 50px;
border-radius: 10px;
}
}
.ckjl-btn {
position: absolute;
left: 110px;
}
.kshf-btn {
position: absolute;
left: 20px;
}
.btn-base {
bottom: 25px;
width: 80px;
height: 30px;
font-size: 12px;
line-height: 30px;
color: white;
text-align: center;
background: #000;
border-radius: 17px;
}
.img-jj {
position: absolute;
right: 80px;
bottom: 20px;
width: 40px;
}
.img-splj {
position: absolute;
right: 20px;
bottom: 20px;
width: 40px;
}
}
</style>
11峦阁、呼叫UI組件
<template>
<div class="call-class">
<img class="img-user" :src="userInfo.avatar?userInfo.avatar:'/company/static/images/system/user_avatar_default.png'" alt="">
<div class="name">{{ userInfo.userName }}</div>
<div class="name-call">正在呼叫...</div>
<img class="img-cancel" src="../../assets/image/cancel.png" alt="" @click="onCancel">
<div class="name-cancel" @click="onCancel">取消</div>
</div>
</template>
<script>
export default {
name: "index",
props:{
userInfo:{
type:Object,
default:()=>{
return {
avatar:null,
userName:null
}
}
}
},
methods:{
onCancel(){
this.$emit('onCancel')
}
}
}
</script>
<style scoped lang="scss">
.call-class{
position: absolute;
top: 100px;
right: 0;
width: 300px;
height: 350px;
background: rgba(0,0,0,0.8);
color: white;
border-radius: 10px 0 0 10px;
z-index: 99999;
.name{
text-align: center;
position: absolute;
top: 120px;
font-size: 20px;
font-weight: bold;
width: 100%;
}
.img-cancel{
position: absolute;
top: 230px;
right: calc(50% - 20px);
width: 40px;
z-index: 99;
}
.name-cancel{
text-align: center;
position: absolute;
top: 280px;
font-size: 12px;
width: 100%;
}
.name-call{
text-align: center;
position: absolute;
top: 160px;
font-size: 14px;
width: 100%;
}
}
.img-user{
position: absolute;
top: 30px;
right: calc(50% - 40px);
width: 80px;
z-index: 99;
}
</style>
12谦铃、app.vue 集成
<Video :is-hidden-video="isHiddenVideo" :time="timeStr" @onQuit="onQuit" />
<InviteVideo v-if="typeState === '0'" @onAction="onAction" />
<CallVideo v-if="isCallShow" @onCancel="onCancel"></CallVideo>
import InviteVideo from './components/InviteVideo'
import Video from './components/Video'
import mrtc from '@/mixins/mrtc'
components: {
InviteVideo,
Video
},
mixins: [mrtc, webSocket],
到這里基本上MRTC音視頻集成完成了。
4榔昔、WebSocket 使用
由于需求場(chǎng)景是pc和小程序互通驹闰,但由于小程序的局限性瘪菌,無(wú)法邀請(qǐng)好友加入房間,也無(wú)法監(jiān)聽(tīng)加入房間事件嘹朗。因此需要業(yè)務(wù)自行實(shí)現(xiàn)消息發(fā)送师妙。
websocket封裝,網(wǎng)上也有相應(yīng)的教程
export default {
components: { },
data() {
return {
websock: '',
lockReconnect: false, // 是否真正建立連接
timeout: 58 * 1000, // 58秒一次心跳
timeoutObj: null, // 心跳倒計(jì)時(shí)
serverTimeoutObj: null, // 心跳倒計(jì)時(shí)
timeoutnum: null, // 斷開(kāi) 重連倒計(jì)時(shí)
typeState: '0',
roomID: '6693563501',
roomKey: '123',
userID: ''
}
},
created() {
},
destroyed() {
this.websock.close() // 離開(kāi)路由之后斷開(kāi)websocket連接
},
methods: {
messageSend({ toUserId, userId, type, roomNumber, passWord }) {
const actions = {
toUserId: '9232131735487',
userId: '123',
type: type,
roomNumber: roomNumber,
passWord: passWord
}
this.websocketsend(JSON.stringify(actions))
},
currentTime() {
setInterval(this.formatDate, 500)
},
initWebSocket() {
// 初始化weosocket
const wsuri = 'ws://域名'
this.websock = new WebSocket(wsuri)
// 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā)
this.websock.onmessage = this.websocketonmessage
// 連接建立時(shí)觸發(fā)
this.websock.onopen = this.websocketonopen
// 通信發(fā)生錯(cuò)誤時(shí)觸發(fā)
this.websock.onerror = this.websocketonerror
// 連接關(guān)閉時(shí)觸發(fā)
this.websock.onclose = this.websocketclose
},
// 連接建立時(shí)觸發(fā)
websocketonopen() {
// 開(kāi)啟心跳
this.start()
// 連接建立之后執(zhí)行send方法發(fā)送數(shù)據(jù)
// this.websocketsend(actions)
},
// 通信發(fā)生錯(cuò)誤時(shí)觸發(fā)
websocketonerror() {
console.log('出現(xiàn)錯(cuò)誤')
this.reconnect()
},
// 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā)
websocketonmessage(e) {
console.log(e.data)
// 收到服務(wù)器信息屹培,心跳重置
//("1","呼叫"),
// ("2","被拒接"),
// ("3","不在線"),
// ("4","占線中"),
// ("999","心跳檢測(cè)");
// 業(yè)務(wù)邏輯自行處理
const data = JSON.parse(e.data)
this.typeState = data.type
switch (data.type) {
case '1':
this.roomID = data.roomNumber
this.roomKey = data.passWord
this.userID = data.userId
break
}
this.reset()
},
websocketsend(Data) {
// 數(shù)據(jù)發(fā)送
this.websock.send(Data)
},
// 連接關(guān)閉時(shí)觸發(fā)
websocketclose(e) {
// 關(guān)閉
console.log('斷開(kāi)連接', e)
// 重連
this.reconnect()
},
reconnect() {
// 重新連接
var that = this
if (that.lockReconnect) {
return
}
that.lockReconnect = true
// 沒(méi)連接上會(huì)一直重連默穴,設(shè)置延遲避免請(qǐng)求過(guò)多
that.timeoutnum && clearTimeout(that.timeoutnum)
that.timeoutnum = setTimeout(function() {
// 新連接
that.initWebSocket()
that.lockReconnect = false
}, 5000)
},
reset() {
// 重置心跳
var that = this
// 清除時(shí)間
clearTimeout(that.timeoutObj)
clearTimeout(that.serverTimeoutObj)
// 重啟心跳
that.start()
},
start() {
// 開(kāi)啟心跳
console.log('開(kāi)啟心跳')
var self = this
self.timeoutObj && clearTimeout(self.timeoutObj)
self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj)
self.timeoutObj = setTimeout(function() {
// 這里發(fā)送一個(gè)心跳,后端收到后褪秀,返回一個(gè)心跳消息蓄诽,
if (self.websock.readyState && Number(self.websock.readyState) === 1) {
// 如果連接正常
const actions = {
toUserId: '1592321317',
userId: '123',
type: '999',
roomNumber: '123456',
passWord: '123456'
}
self.websocketsend(JSON.stringify(actions)) // 這里可以自己跟后端約定
} else {
// 否則重連
self.reconnect()
}
self.serverTimeoutObj = setTimeout(function() {
// 超時(shí)關(guān)閉
self.websock.close()
}, self.timeout)
}, self.timeout)
}
},
mounted() {
this.currentTime()
},
// 銷毀定時(shí)器
beforeDestroy() {
if (this.formatDate) {
clearInterval(this.formatDate) // 在Vue實(shí)例銷毀前,清除時(shí)間定時(shí)器
}
}
}
到這MRTC就集成完成了媒吗,通信需要配置WebSocket實(shí)現(xiàn)仑氛。
附帶簡(jiǎn)單集成demo,不帶業(yè)務(wù)邏輯