这个系列呢,主要给各位观众老爷看看目前有较大趋势的SaaS应用的SDK在各种主流Web终端的使用姿势和异常分析,如果想要纯粹了解开发的或者云原生,云开发的可以去往另一个系列——云开发系列。
引流链接:https://cloud.tencent.com/developer/article/1750264
今天给大家主要讲讲TRTC(Tencent RTC ,实时通讯),在4G时代下,直播,短视频,视频聊天等用手机看视频已经成为了如大家呼吸一般简单的事情。而5G时代的到来,虽然目前还并不知道5G下视频向产品的发展趋势,但总体而言,视频
这个目前也接入了云原生,如果后续有机会也给大家讲一讲传统RTC实现接入,和云原生接入的区别。
在我的另一篇文章 https://cloud.tencent.com/developer/article/1738182中,详细展开了整个官方Web Demo 的架构,官方的Demo用的是jquery,但是大家懂得都懂,目前jquery在大多数情况下已经不再是开发中的首选了,目前更多的业务场景中是要如何把 trtc-js-sdk 接入到 vue,react 等框架内,这里的话给大家做一个参考实例,并且给大家总结一下注意要点,下面会贴一些核心的代码。
这一篇文章是讲Vue的,为什么叫初始篇呢,因为目前做了trtc的最基础的功能,未来也许会更多的案例(又给自己挖坑)
Vue 作为目前最为成熟的MVVM框架之一,相较于jquery去写,减少了大量视图上的操作,配合element-UI可以减少许多布局上的问题
先贴核心代码。
PS. Vue 这里有用到Router,所以会展示Router的用法,之后的React那边会展示非Router的思路
<script> // @ is an alias to /src import TRTC from "trtc-js-sdk"; import axios from "axios"; import router from "../router"; export default { name: "Login", components: {}, data() { return { //这里请填入对应的sdkAppId login: { sdkAppId_: 1400****221, userSig_: "", userId_: "", password_: "", }, client_: null, isJoined_: false, isPublished_: false, isAudioMuted_: false, isVideoMuted_: false, localStream_: null, remoteStream_: null, members_: new Map(), mic: "", micOptions: [], camera: "", cameraOptions: [], }; }, created() {}, mounted() {}, methods: { async login_() { if (!(this.login.password_ && this.login.userId_)) { this.$alert("请确认用户名与密码已经填写", "注意!", { confirmButtonText: "确定", callback: (action) => { this.$message({ type: "info", message: `action: ${action}`, }); }, }); } else if (this.login.password_ != 888) { //做测试用,密码为888 this.$message.error("密码错误"); } else if (this.radio1 == "") { this.$message.error("请选择模式"); } else { //这里是服务端计算密钥 axios .post(`${填你自己的host}`, { userid: this.login.userId_, expire: 604800, }) .then((res) => { this.$alert("登陆成功", "提示", { confirmButtonText: "确定", callback: (action) => { this.login.userSig_ = res.data; router.push({ name: "BasicTrtc", params: { userSig: this.login.userSig_, userId: this.login.userId_, //这里的sdkAppId 更好的情况应该设置为一个全局变量然后引入,这里偷懒了 sdkAppId: this.login.sdkAppId_, }, }); }, }); }) .catch((err) => { console.log(err); }); } }, }, }; </script>
const tcb = require('@cloudbase/node-sdk') const TLSSigAPIv2 = require('tls-sig-api-v2'); const Koa = require('koa'); const Router = require('koa-router'); const cors = require('koa2-cors'); const bodyParser = require('koa-bodyparser'); const app = new Koa(); const router = new Router(); app.use(sslify()) app.use(bodyParser()); app.use(cors()); app.use(router.routes()).use(router.allowedMethods()); router.get("/", ctx => { ctx.body = 'Hello World'; }); router.post("/getUserSig",ctx => { if (ctx.method == 'OPTIONS') { ctx.body = 200; } else { console.log(ctx.request.body); let {userid,expire} = ctx.request.body; //在后端做计算,这里的sdkappid和key可以写死 let api = new TLSSigAPIv2.Api(1400349221, '9ef0430a010*********************************037c6237ad3cd7710087e'); let sig = api.genSig(userid, expire); ctx.body = sig; } }); app.listen(8081);
这里一定要非常非常注意,由于TRTC底层封装的是WebRTC,WebRTC在服务器上必须只能在https协议下运行,因此一定要去配证书
前端计算可以参考官方Demo
<script> // @ is an alias to /src import TRTC from "trtc-js-sdk"; import axios from "axios"; import Vue from "vue"; import router from "../router"; export default { name: "BasicTrtc", components: {}, data() { return { login: { sdkAppId_: "", userSig_: undefined, userId_: "", roomId_: "", }, client_: null, localStream_: null, remoteStreams_: [], mic: "", micOptions: [], camera: "", cameraOptions: [], radio1: "", }; }, created() { TRTC.checkSystemRequirements().then((result) => { if (!result) { alert("Your browser is not compatible with TRTC"); } }); }, async mounted() { this.login.roomId_ = 666888; this.login.userId_ = this.$route.params.userId; this.login.userSig_ = this.$route.params.userSig; this.login.sdkAppId_ = this.$route.params.sdkAppId; console.log(this.login); this.checkInfo(); }, methods: { async init() { const localStream = TRTC.createStream({ audio: true, video: true }); localStream .initialize() .catch((error) => { console.error("failed initialize localStream " + error); }) .then(() => { console.log("initialize localStream success"); TRTC.getMicrophones().then((res) => { for (let item in res) { this.micOptions.push(res[item]); } }); TRTC.getCameras().then((res) => { for (let item in res) { this.cameraOptions.push(res[item]); } }); // 本地流初始化成功,可通过Client.publish(localStream)发布本地音视频流 }); this.localStream_ = localStream; this.$message({ type: "info", message: `初始化完成!请选择输入输出设备`, }); }, async checkInfo() { if (this.login.userSig_ == undefined) { this.$alert("您未登录或登录态失效,请重新登录", "警告", { callback: (action) => { router.push({ name: "Login" }); }, }); } else { this.init(); } }, async joinRoom() { switch(this.radio1){ case '多人通话(会议)': router.push({ name: "PrivateChatRoom", params: { userSig: this.login.userSig_, userId: this.login.userId_, sdkAppId: this.login.sdkAppId_, roomId:this.login.roomId_, mic:this.mic, camera:this.camera, }, }); break; case '语音聊天室': router.push({ name:"IMChatRoom", params: { userSig: this.login.userSig_, userId: this.login.userId_, sdkAppId: this.login.sdkAppId_, roomId:this.login.roomId_, mic:this.mic, camera:this.camera, }, }) break; case '互动直播': router.push({ name:"interactLive", params:{ login:this.login, mic:this.mic, camera:this.camera, } }) break; case '互动课堂': router.push({ name:"interactClass", params:{ login:this.login, mic:this.mic, camera:this.camera, } }) break; } }, }, }; </script>
这里的一个需要注意的是,为什么初始化要创建Stream,我们知道流是要放Client里才能使用的,一般正常的思路是先createClient 然后在createSteam 最后再把stream push到client里面去。但是由于部分浏览器的限制(主要是firefox,safari和一些chrome),对设备权限要求非常严格。没有流的话是不能直接授权设备的,而没有授权就无法获取设备ID(会出现undefined),则后面创建client的就无法创建,因此在这个界面里创建流并获取设备授权,并通过路由的形式传给房间
房间的大多数逻辑部分与官方demo既没有什么差别,基本流程如下图
唯一一个需要非常注意是在这里的最后三行,是该SDK接入Vue与jquery最大的不同之处
this.client_.on("stream-subscribed", (evt) => { const uid = evt.userId; const remoteStream = evt.stream; const id = remoteStream.getId(); console.log(id); this.remoteStreams_.push(remoteStream); // objectFit 为播放的填充模式,详细参考:https://trtc-1252463788.file.myqcloud.com/web/docs/Stream.html#play Vue.nextTick().then(function () { remoteStream.play("remote_" + id, { objectFit: "contain" }); }); });
由于Vue的视图更新是自动监听有关视图的数据变化,数据一旦发生变化,视图随之变化,反之亦然,这是Vue的双向绑定机制,这里可以简单提一下:用Object.defineProperty( )的 set 与 get 来劫持属性的变化,然后告知Watcher,watcher中向所有订阅者更改视图。所以换句话说:更改视图这个行为什么时候完成,归属于底层,我们不能通过直接按顺序往下写代码就认为这是在视图更新完之后执行的
所以我们需要用到 Vue.nextTick().then(fn()) 这个全局函数
他可以让Vue在视图更新之后再执行后续代码
当然还有一种写法是在Vue的生命周期里的 updated 这里写,这时React的写法,后续如果出React的章节可以在这里完成。
以下是参考源码:
<script> import TRTC from "trtc-js-sdk"; import axios from "axios"; import Vue from "vue"; import router from "../router"; export default { data() { return { //这里请填入对应的sdkAppId login: { sdkAppId_: "", userSig_: undefined, userId_: "", roomId_: "", }, circleUrl: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png", squareUrl: "https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png", sizeList: ["large", "medium", "small"], client_: null, isJoined_: false, isPublished_: false, isAudioMuted_: false, isVideoMuted_: false, localStream_: null, client_: null, localStream_: null, remoteStreams_: [], yourView_:true, mic: "", micOptions: [], camera: "", cameraOptions: [], members_: new Map(), }; }, async mounted() { const routerParams = this.$route.params; console.log(routerParams); this.login.roomId_ = routerParams.roomId; this.login.userId_ = routerParams.userId; this.login.userSig_ = routerParams.userSig; this.login.sdkAppId_ = routerParams.sdkAppId; this.mic = routerParams.mic; this.camera = routerParams.camera; await this.checkInfo(); }, methods: { async joinRoom() { if (this.login.roomId_ == "" || this.login.userId_ == "") { this.$message.error("请填写房间号/用户ID"); return; } console.log(this.login); this.client_ = TRTC.createClient({ mode: "rtc", sdkAppId: this.login.sdkAppId_, userId: this.login.userId_, userSig: this.login.userSig_, }); this.handleEventsListen(); if (this.isJoined_) { this.$message.warn("duplicate RtcClient.join() observed"); } await this.client_ .join({ roomId: this.login.roomId_, }) .then((res) => { this.$message.success("进房成功"); this.isJoined_ = true; this.beginPushStream(); }) .catch((err) => { console.log(err); this.$message.error("进房失败!" + err); }); }, async leaveRoom() { if (!this.isJoined_) { this.$message.warn("leave() - please join() firstly"); return; } // ensure the local stream is unpublished before leaving. await this.stopPushStream(); // leave the room await this.client_.leave(); this.isJoined_ = false; router.push({ name: "BasicTrtc", params: { userSig: this.login.userSig_, userId: this.login.userId_, sdkAppId: this.login.sdkAppId_, roomId:this.login.roomId_ }, }); }, beginPushStream() { this.localStream_ = TRTC.createStream({ audio: true, video: true, mirror: true, microphoneId: this.mic, cameraId: this.camera, }); this.localStream_ .initialize() .catch((error) => { console.log(error); }) .then(() => { this.$message.success("initialize localStream success"); // 本地流初始化成功,可通过Client.publish(localStream)发布本地音视频流 if (!this.isJoined_) { console.warn("publish() - please join() firstly"); return; } if (this.isPublished_) { console.warn("duplicate RtcClient.publish() observed"); return; } this.client_.publish(this.localStream_).then(() => { // 本地流发布成功 this.localStream_ .play("local", { objectFit: "contain" }) .then(() => { this.isPublished_ = true; // autoplay success }) .catch((e) => { console.error("failed to publish local stream " + e); this.isPublished_ = false; const errorCode = e.getCode(); if (errorCode === 0x4043) { // PLAY_NOT_ALLOWED,引导用户手势操作恢复音视频播放 // stream.resume() } }); }); }); }, async muteVideo(){ if(this.localStream_.muteVideo()){ this.isVideoMuted_ = true; }else{ this.$message.error("muteVideo failed"); } }, async unmuteVideo(){ if(this.localStream_.unmuteVideo()){ this.isVideoMuted_ = false; }else{ this.$message.error("unmuteVideo failed"); } }, async muteAudio(){ if(this.localStream_.muteAudio()){ this.isAudioMuted_ = true; }else{ this.$message.error("unmuteVideo failed"); } }, async unmuteAudio(){ if(this.localStream_.unmuteAudio()){ this.isAudioMuted_ = false; }else{ this.$message.error("unmuteVideo failed"); } }, async setSlience(){ }, async unsetSlience(){ }, async stopPushStream() { this.localStream_.stop(); this.localStream_.close(); if (!this.isJoined_) { console.warn("unpublish() - please join() firstly"); return; } if (!this.isPublished_) { console.warn("RtcClient.unpublish() called but not published yet"); return; } await this.client_.unpublish(this.localStream_); this.isPublished_ = false; }, async checkInfo() { if (this.login.userSig_ == undefined) { this.$alert("您未登录或登录态失效,请重新登录", "警告", { callback: (action) => { router.push({ name: "Login" }); }, }); } else if (this.mic == "" || this.camera == "") { this.$alert("未检测到您的麦克风或摄像头授权信息", "警告", { callback: (action) => { router.push({ name: "BasicTrtc", params: { userSig: this.login.userSig_, userId: this.login.userId_, sdkAppId: this.login.sdkAppId_, mode: this.radio1, }, }); }, }); }else{ await this.joinRoom(); } }, handleEventsListen() { console.log(this.client_); this.client_.on("stream-added", (evt) => { const remoteStream = evt.stream; const id = remoteStream.getId(); const userId = remoteStream.getUserId(); this.members_.set(userId, remoteStream); console.log( `remote stream added: [${userId}] ID: ${id} type: ${remoteStream.getType()}` ); if (remoteStream.getUserId() === this.shareUserId_) { // don't need screen shared by us this.client_.unsubscribe(remoteStream); } else { console.log("subscribe to this remote stream"); this.client_.subscribe(remoteStream); } console.log(this.remoteStreams_) }); this.client_.on("stream-subscribed", (evt) => { const uid = evt.userId; const remoteStream = evt.stream; const id = remoteStream.getId(); console.log(id); this.remoteStreams_.push(remoteStream); // objectFit 为播放的填充模式,详细参考:https://trtc-1252463788.file.myqcloud.com/web/docs/Stream.html#play Vue.nextTick().then(function () { remoteStream.play("remote_" + id, { objectFit: "contain" }); }); }); this.client_.on("stream-removed", (evt) => { const remoteStream = evt.stream; const id = remoteStream.getId(); remoteStream.stop(); console.log(this.remoteStreams_); this.remoteStreams_ = this.remoteStreams_.filter((stream) => { return stream.getId() !== id; }); console.log( `stream-removed ID: ${id} type: ${remoteStream.getType()}` ); }); this.client_.on("mute-video",event => { console.log(this.remoteStreams_) }) this.client_.on("unmute-video",event => { console.log(this.remoteStreams_) }) }, }, }; </script>
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 yunjia_community@tencent.com 删除。
我来说两句