近期公司承接了某高校智慧校园的项目建设工作,其中在家校协作的板块中需要进行视频教学,以及线上屏幕共享,为了完成这一需求,我在自研与第三方服务的选择之间选择了第三方,主要因为目前团队成员在音视频方向的深度不足以支撑完成整个高可用模块,而且现在受疫情影响,对于高校来说,直播教学板块尤为重要。
常用的第三方音视频服务:
在几番选择后,我采用了腾讯云作为我们音视频板块的服务支撑组件,主要是因为我们很多服务都与腾讯云有对接,在一起方便于管理,而且操作起来比较方便,并且也是业内知名大厂。
TRTC 是腾讯云主打全平台互通的多人音视频通话和低延时互动直播解决方案。TRTC服务有多种客户端的支持,对于IOS、Android、React native等都支持的比较好,我们主要在于 IOS、Android、Web三端进行处理,其中 TRTC Web SDK 基于 WebRTC 实现,基于腾讯云成熟的音视频积累,保证了视频流的低延时 高效传输等。
首先,在腾讯云搜索 TRTC,https://cloud.tencent.com/search/%E5%AE%9E%E6%97%B6%E9%9F%B3%E8%A7%86%E9%A2%91/1_1
,
然后点击立即使用:
然后到登录账户到达 TRTC 控制台,我首先需要建立一个 Demo 进行接入测试,这里我们就建立一个 aopppTRTC_Demo
实际上大家第一次使用他会有引导的,大家可以放心,这里我们要查看一下这个应用的配置,主要就是为了获得 SDKAppID 和 密钥
这样,我们就在腾讯云上建立好了 TRTC 应用了,接下来我们根据开始实现我们的直播教学业务。
我们先简单利用TRTC实现一下老师在教师端进行直播教学,这个需要能够共享老师屏幕,摄像头以及麦克风,老师登录教师端后在今天的课表上选择开始上课,然后启用屏幕、摄像头、麦克风共享即可
我们这里还是采用 Vue + Vite + Element-plus 进行 Web 页面设计,我最终的 demo 地址为 https://gitee.com/TestsLing/aoppp-trtc_-demo.git
,欢迎大家一起讨论交流
首先就是老师登录教师端后,去创建课程的直播间,这里我们需要构建一个开启课程直播的页面
我们只需要输入课程名称,然后点击开启教学直播,就会把课程名称发送给服务端,然后服务端帮我们初始化好房间,并返回对应的房间ID,相关代码如下:
<script lang="ts" setup>
import {onMounted, ref, reactive} from "vue";
import {useRoute, useRouter} from "vue-router";
import {FormInstance} from "element-plus";
const rules = reactive({
lesson: [{required: true, message: "请输入课程名称", trigger: "blur"}],
});
const form = reactive({
lesson: "",
});
const router = useRouter();
const ruleFormRef = ref<FormInstance>();
async function handleCreateRoom() {
await ruleFormRef.value?.validate((valid, fields) => {
fetch('http://localhost:3888/creatRoom', {
method: 'post',
headers: {
'Accept': 'application/json,text/plain,*/*',/* 格式限制:json、文本、其他格式 */
'Content-Type': 'application/json'/* 请求内容类型 */
},
body: JSON.stringify({
lesson: form.lesson
})
}).then((response) => {
return response.json()
}).then((data) => {
let {roomId, code} = data
if (code == 200 && valid) {
router.push({path: "page", query: {...form, roomId}})
}
}).catch(function (error) {
// 可以进行异常上报
console.log(error)
})
});
}
开启视频教学之后,我们就需要一个直播间,这个时候我们就需要使用到集成的 trtc-js-sdk,大致思路是这样,我们先通过trtc创建一个客户端,然后我们要对视频流进行处理
大致页面和代码如下:
然后流就会回显到我们的主内容区域
在代码中的实现:
要注意一下这个 userSig,签名的作用就是为了防篡改,需要确定你是使用方身份,我们需要使用 secretKey、sdkAppId、userId 来创造这个签名,secretKey是私密信息,不能放到客户端进行硬编码,应该由服务端处理完后将签名返回给客户端,但是官方也提供了前端加密的方法, 供我们调试使用,所以我们现在直接使用官方提供的demo中的加密方法来获取 UserSig。
/* eslint-disable */
/*
* Module: GenerateTestUserSig
*
* Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
* 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
*
* Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
*
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
*
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
*
* Reference:https://cloud.tencent.com/document/product/647/17275#Server
*/
interface Login {
sdkAppId: number;
userId: string;
secretKey: string;
}
interface UserSig {
userSig: string;
sdkAppId: number;
}
export function genTestUserSig({
sdkAppId,
userId,
secretKey,
}: Login): UserSig {
/**
* 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
*
* 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
* 它是腾讯云用于区分客户的唯一标识。
*/
const SDKAPPID = sdkAppId;
/**
* 签名过期时间,建议不要设置的过短
*
* 时间单位:秒
* 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
*/
const EXPIRETIME = 604800;
/**
* 计算签名用的加密密钥,获取步骤如下:
*
* step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
* step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
* step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
*
* 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
* 文档:https://cloud.tencent.com/document/product/647/17275#Server
*/
const SECRETKEY = secretKey;
// a soft reminder to guide developer to configure sdkAppId/secretKey
if (SDKAPPID == undefined || SECRETKEY === "") {
alert(
"请先配置好您的账号信息: SDKAPPID 及 SECRETKEY " +
"\r\n\r\nPlease configure your SDKAPPID/SECRETKEY in js/debug/GenerateTestUserSig.js"
);
}
const generator = new (<any>window).LibGenerateTestUserSig(
SDKAPPID,
SECRETKEY,
EXPIRETIME
);
const userSig = generator.genTestUserSig(userId);
return {
sdkAppId: SDKAPPID,
userSig: userSig,
};
}
官方的详细的介绍: https://cloud.tencent.com/document/product/647/17275
const router = useRouter();
const route = useRoute();
const appConf = reactive({
userId: "",
roomId: 0,
sdkAppId: 0,
userSig: "",
});
// 一般是服务端返回的,这里为了测试方便,先写死
const getUserId = () => "123"
// 在生命周期中进行初始化一下
onMounted(async () => {
appConf.userId = getUserId();
appConf.roomId = parseInt(route.query.roomId as string, 10);
const { sdkAppId, userSig } = await getSign(appConf.userId);
appConf.sdkAppId = sdkAppId;
appConf.userSig = userSig;
handlerJoin();
});
// 加入房间
const handlerJoin = async () => {
const { userId, userSig, roomId, sdkAppId } = appConf;
console.log({ userId, userSig, roomId, sdkAppId });
try {
// 创建客户端
localClient = TRTC.createClient({
mode: "rtc", // 采用rtc方式
sdkAppId,
userId,
userSig,
});
//
await localClient.join({ roomId });
} catch (e: any) {
console.log("加入房间失败", e);
}
// 对相关视频事件进行监听
installEventHandlers();
// 创建流
await createStream();
// 推送流
await publishHandler();
};
const installEventHandlers = () => {
if (!localClient) return;
localClient.on("error", nothingHandle);
localClient.on("client-banned", nothingHandle);
localClient.on("peer-join", nothingHandle);
localClient.on("peer-leave", nothingHandle);
localClient.on("stream-added", handleStreamAdded);
localClient.on("stream-subscribed", handleStreamSubscribed);
localClient.on("stream-removed", handleStreamRemoved);
localClient.on("stream-updated", nothingHandle);
localClient.on("mute-video", nothingHandle);
localClient.on("mute-audio", nothingHandle);
localClient.on("unmute-video", nothingHandle);
localClient.on("unmute-audio", nothingHandle);
};
/**
* 流处理
*/
const createStream = async () => {
try {
if (streamMode.value === "screenStream") localStream = await creatScreenStream();
if (streamMode.value === "cameraStream") localStream = await createCameraStream();
// 主屏幕显示流
await localStream.play("stream_main");
} catch (e: any) {
throw new Error(e);
}
};
// 摄像头流
const createCameraStream = async () => {
const { userId, userSig, roomId, sdkAppId } = appConf;
let localStream;
try {
// const cameraItems = await TRTC.getCameras();
// const microphoneItems = await TRTC.getMicrophones();
// console.log(cameraItems, "cameraItems");
localStream = TRTC.createStream({
userId: userId,
audio: true,
video: true,
// cameraId: cameraItems[0].deviceId,
// microphoneId: microphoneItems[0].deviceId,
});
await localStream.initialize();
} catch (e: any) {
throw new Error(e);
}
return localStream;
};
// 屏幕流
const creatScreenStream = async () => {
const { userId, userSig, roomId, sdkAppId } = appConf;
let localStream;
try {
localStream = TRTC.createStream({
audio: false,
screen: true,
userId,
});
await localStream.initialize();
} catch (e: any) {
throw new Error(e);
}
return localStream;
};
// 推流
const publishHandler = async () => {
if (!localClient || !localStream) return;
try {
await localClient.publish(localStream);
console.log("推流成功");
} catch (e: any) {
console.log("推流失败", e);
}
};
// 取消推流
const unpublishHandler = async () => {
if (!localClient || !localStream) return;
try {
await localClient.unpublish(localStream);
} catch (e: any) {
throw new Error(e);
}
};
// ....更多请看 gitee demo
如果推流失败,需要先检查当前环境,文档也有说明: https://web.sdk.qcloud.com/trtc/webrtc/doc/zh-cn/tutorial-23-advanced-support-detection.htm