腾讯云实时音视频(Tencent RTC,简称 TRTC)是一项低延时、高并发、稳定可靠的音视频 PaaS云服务,主要提供多人实时通话以及低延时互动直播能力。TRTC 将腾讯 21 年来在网络与音视频技术上的深度积累,通过 SDK 及云端 API的方式向开发者开放,为包括全民K歌、腾讯会议、陌陌、VIPKID 等腾讯内外客户提供底层音视频服务,覆盖了数以亿计的终端用户。
腾讯云TRTC的功能包括视频通话、语音通话、视频互动直播、互动连麦、屏幕分享、3A 处理、变声、美颜特效、视频内容审核等功能可以满足大部分企业或者个人对于实时音视频领域的需求,而且接入成本低,提供SDK、Demo、详细的产品文档提供给开发人员学习。
下面的例子是基于腾讯云实时音视频(TRTC)的无ui集成方案。 在web端简单实现多个视频会议功能,包含 多人音视频会议、屏幕共享、邀请成员、 关闭 开启 音视频 等基础功能。
下面介绍一下本次例子所使用到的一些基础概念
TRTC 通过应用的形式来管理不同的业务或项目。您可以在 TRTC 控制台 给不同的业务或项目分别创建不同的应用,从而实现业务或项目数据的隔离。每个腾讯云账号最多可以创建100个 TRTC 应用。
SDKAppID(应用标识/应用 ID)是腾讯云后台用来区分不同 TRTC 应用的唯一标识,在 TRTC 控制台 创建应用时自动生成。不同 SDKAppID 之间的数据不互通。
SDKSecretKey(应用密钥)是腾讯云后台用来验证TRTC 应用唯一标识(SDKAppID)的一个字符串,如果说SDKAppID是账号,那么SDKSecretKey就是密码。
UserID(用户标识)用于在一个 TRTC 应用中唯一标识一个用户。
用户标识是用户登录开发者业务系统的帐号在腾讯云上的映射。通常情况下,开发者可直接使用用户名作为 UserID。
取值范围长度建议不超过32字节。请使用英文字符、数字或下划线,不能全为数字,区分大小写。
RoomID(房间号/房间ID)用于在一个 TRTC 应用中唯一标识一个房间。RoomID 是由开发者自行维护和分配的一个 uint32 范围内的数字,取值区间:1 - 4294967295。
UserSig(用户签名)是腾讯云设计的一种安全保护签名,用于对一个用户进行登录鉴权认证,确认用户是否真实,阻止恶意攻击者盗用您的云服务使用权。详情请参见 UserSig 相关问题 文档。
推送指用户将本地的音视频数据上传给 TRTC 服务端的操作,对应“推流”。
订阅指用户向 TRTC 服务端请求拉取指定用户音视频数据的操作,对应“拉流”。
想了解更多概念可以查看官方文档
项目链接
项目所用框架 vite + vue3 + element-plus + uno.css
项目代码结构如下
获取 用户id 和房间id,设置进入房间前是否打开摄像头,麦克风等功能, 同时处理分享后的页面。
// 效验规则
const rules = reactive({
user: [{ required: true, message: "请输入用户id", trigger: "blur" }],
roomId: [{ required: true, message: "请输入房间号", trigger: "blur" }],
});
// 默认开启功能
const checkList = ref(["audio", "video"]);
// 表单信息和功能参数
const form = reactive({
user: "",
roomId: "",
audio: true,
video: true,
beauty: false,
});
const route = useRoute();
const router = useRouter();
const ruleFormRef = ref<FormInstance>();
// 加入房间对话框
const dialogVisible = ref(false);
let localStream: LocalStream;
// 创建本地流
const createCameraStream = async () => {
let localStream;
try {
// 初始化摄像头流
localStream = TRTC.createStream({
userId: "test",
audio: checkList.value.includes("audio"),
video: checkList.value.includes("video"),
});
await localStream.initialize();
} catch (e: any) {
throw new Error(e);
}
// 如果选择了美颜
if (checkList.value.includes("beauty")) {
// 用美颜插件处理视频流
const beautyPlugin = new RTCBeautyPlugin();
localStream = beautyPlugin.generateBeautyStream(localStream);
}
return localStream;
};
// 选择改变
async function handleStreamChange(value: string[]) {
localStream?.close();
localStream = await createCameraStream();
localStream.play("camera");
}
/**
* 表单确认
*/
async function handleCreateRoom() {
await ruleFormRef.value?.validate(async (valid, fields) => {
if (valid) {
dialogVisible.value = true;
// 创建流
localStream = await createCameraStream();
// 挂载到 id为camera的div中
localStream.play("camera");
}
});
}
// 关闭对话框
async function handleClose() {
dialogVisible.value = false;
localStream?.close();
}
// 确认进入房间
function handleStartRoom() {
form.beauty = checkList.value.includes("beauty");
form.video = checkList.value.includes("video");
form.audio = checkList.value.includes("audio");
router.push({ path: "page", query: { ...form } });
}
// 自动从url中获取到房间号 实现邀请功能
onMounted(() => {
if (route.query.roomId) form.roomId = route.query.roomId as string;
});
处理远端视频流和本地视频流的的页面
onMounted(async () => {
// 从路由中获取房间号和用户id
appConf.userId = route.query.user as string;
appConf.roomId = parseInt(route.query.roomId as string, 10);
// 生成用户签名
const { sdkAppId, userSig } = await getSign(appConf.userId);
appConf.sdkAppId = sdkAppId;
appConf.userSig = userSig;
//加入房间
handlerJoin();
});
/src/utils/sign/generateTestUserSig.ts
const generator = new (<any>window).LibGenerateTestUserSig(
// 应用id
SDKAPPID,
// 应用密钥
SECRETKEY,
// 签名过期时间时间单位:秒
EXPIRETIME
);
const userSig = generator.genTestUserSig(userId);
我们通过 createClient 创建 Client 对象, 并通过Client 对象 调用 join 方法 加入房间
文档:Client对象文档
const { userId, userSig, roomId, sdkAppId } = appConf;
try {
// 创建客户端
localClient = TRTC.createClient({
mode: "rtc",
sdkAppId,
userId,
userSig,
});
//
await localClient.join({ roomId });
} catch (e: any) {
console.log("加入房间失败", e);
}
// 事件监听
installEventHandlers();
// 创建视频流
await createStream();
// 推流
await publishHandler();
Client 生命周期如图所示:
我们可以用on off 监听和取消 各个Client的 生命周期活动
用法 on(eventName, handler, context)
主要监听的事件有
STREAM_ADDED
Default Value:‘stream-added’
远端流添加事件,当远端的 client 调用 publish() 发布流后,您会收到该通知。通过该事件您可以订阅指定的远端流。
STREAM_REMOVED
Default Value:‘stream-removed’
远端流移除事件,当远端的 client 调用 unpublish() 取消发布流后,您会收到该通知。通过该事件您可以知道哪一个远端流被移除了。
STREAM_SUBSCRIBED
Default Value:‘stream-subscribed’
远端流订阅成功事件,调用 subscribe() 成功后会触发该事件。
const installEventHandlers = () => {
if (!localClient) return;
localClient.on("stream-added", handleStreamAdded);
localClient.on("stream-subscribed", handleStreamSubscribed);
localClient.on("stream-removed", handleStreamRemoved);
localClient.on("stream-updated", nothingHandle);
};
const handleStreamAdded = (event: any) => {
// 远程流
const remoteStream = event.stream;
const id = remoteStream.getId();
const userId = remoteStream.getUserId();
if (remoteStream.getUserId() === `share_${userId}`) {
// 取消订阅
// 当收到远端流增加事件 'stream-added' 通知后,SDK 默认会立刻接收并解码该远端流所包含的音视频数据,如果你想拒绝接收 该远端流的任何音视频数据,请在 'stream-added' 事件处理回调中调用此接口
localClient.unsubscribe(remoteStream).catch((error: any) => {});
} else {
//订阅
localClient.subscribe(remoteStream).catch((error: any) => {});
}
};
// 开会人列表
const remoteStreamsList = ref<Array<RemoteStream>>([]);
const handleStreamSubscribed = async (event: any) => {
const remoteStream = event.stream;
const id = remoteStream.getId();
const userId = remoteStream.getUserId();
remoteStreamsList.value = [...remoteStreamsList.value, remoteStream];
// 播放
await nextTick();
await remoteStream.play(id);
};
//远端流移除事件监听函数
const handleStreamRemoved = (event: any) => {
const remoteStream = event.stream;
const id = remoteStream.getId();
// 移除开会人列表
remoteStreamsList.value = remoteStreamsList.value.filter(
(stream) => stream.getId() !== id
);
};
/**
* 流处理
*/
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);
}
};
// 麦克风
const microStatus = ref(true);
const microphoneHandler = () => {
if (microStatus.value) {
localStream.muteAudio();
} else {
localStream.unmuteAudio();
}
microStatus.value = !microStatus.value;
};
// 视频
const cameraStatus = ref(true);
const cameraHandler = async () => {
// 停止videoTrack并释放摄像头资源
if (cameraStatus.value) {
const videoTrack = localStream.getVideoTrack();
if (videoTrack) {
await localStream.removeTrack(videoTrack);
videoTrack.stop();
}
} else {
const stream = await createCameraStream();
const videoTrack = stream.getVideoTrack();
await localStream.addTrack(videoTrack as MediaStreamTrack);
}
cameraStatus.value = !cameraStatus.value;
};
// 结束群聊
const handleLeave = async () => {
// 取消推流
await unpublishHandler();
try {
// 关闭监听
uninstallEventHandlers();
// 离开房间
if (localClient) await localClient.leave();
// 关闭视频流
if (localStream) {
localStream.stop();
localStream.close();
localStream = null as any;
}
router.push({ path: "/" });
} catch (e: any) {
console.log(e, "报错");
}
};