腾讯实时音视频(Tencent Real-Time Communication,TRTC)是腾讯云提供的一套低延时、高质量的音视频通信服务。本文将详细介绍如何在 uni-app 和 Web 端接入 TRTC,并提供完整的示例代码,包括多人视频通话的实现。
目录
准备工作
1. 开通腾讯云服务
- 注册腾讯云账号
- 开通实时音视频 TRTC 服务
- 创建应用,获取 SDKAppID 和密钥
2. 安装依赖
Web 端
npm install trtc-js-sdk
uni-app 端
npm install trtc-wx-sdk
Web 端接入
1. 基础配置
首先创建一个基础的 TRTC 配置类:
// trtc-config.js
export const TRTC_CONFIG = {
sdkAppID: 'your-sdk-app-id',
userID: 'user-id',
userSig: 'user-sig',
roomID: 'room-id',
template: 'grid', // 画面布局模式:grid(网格)或 float(悬浮)
role: 'anchor', // 角色:anchor(主播)或 audience(观众)
privateMapKey: '', // 房间加密密钥
businessInfo: '', // 业务信息
};
2. 创建 TRTC 实例
// trtc-client.js
import TRTC from 'trtc-js-sdk';
import { TRTC_CONFIG } from './trtc-config';
export class TRTCClient {
constructor() {
this.client = null;
this.localStream = null;
this.remoteStreams = new Map();
}
async init() {
try {
// 创建客户端实例
this.client = TRTC.createClient({
mode: 'rtc',
sdkAppID: TRTC_CONFIG.sdkAppID,
userID: TRTC_CONFIG.userID,
userSig: TRTC_CONFIG.userSig,
template: TRTC_CONFIG.template,
});
// 注册事件监听
this.registerEvents();
// 加入房间
await this.client.join({ roomID: TRTC_CONFIG.roomID });
console.log('加入房间成功');
// 创建本地流
await this.createLocalStream();
} catch (error) {
console.error('TRTC 初始化失败:', error);
throw error;
}
}
registerEvents() {
// 监听远端用户加入
this.client.on('peer-join', (event) => {
console.log('远端用户加入:', event.userID);
});
// 监听远端用户离开
this.client.on('peer-leave', (event) => {
console.log('远端用户离开:', event.userID);
this.remoteStreams.delete(event.userID);
});
// 监听远端流添加
this.client.on('stream-added', (event) => {
console.log('远端流添加:', event.stream);
this.client.subscribe(event.stream);
});
// 监听远端流订阅成功
this.client.on('stream-subscribed', (event) => {
console.log('远端流订阅成功:', event.stream);
this.remoteStreams.set(event.stream.getUserID(), event.stream);
this.playRemoteStream(event.stream);
});
}
async createLocalStream() {
try {
this.localStream = TRTC.createStream({
userID: TRTC_CONFIG.userID,
audio: true,
video: true,
});
await this.localStream.initialize();
await this.client.publish(this.localStream);
this.playLocalStream();
} catch (error) {
console.error('创建本地流失败:', error);
throw error;
}
}
playLocalStream() {
this.localStream.play('local-video');
}
playRemoteStream(stream) {
stream.play(`remote-video-${stream.getUserID()}`);
}
async leave() {
if (this.localStream) {
this.localStream.stop();
this.localStream.close();
}
if (this.client) {
await this.client.leave();
}
}
}
3. 在 Vue 组件中使用
<!-- TRTCVideo.vue -->
<template>
<div class="trtc-container">
<div id="local-video" class="video-container"></div>
<div v-for="stream in remoteStreams"
:key="stream.getUserID()"
:id="`remote-video-${stream.getUserID()}`"
class="video-container">
</div>
</div>
</template>
<script>
import { TRTCClient } from './trtc-client';
export default {
name: 'TRTCVideo',
data() {
return {
trtcClient: null,
remoteStreams: [],
};
},
async mounted() {
this.trtcClient = new TRTCClient();
try {
await this.trtcClient.init();
} catch (error) {
console.error('TRTC 初始化失败:', error);
}
},
beforeDestroy() {
if (this.trtcClient) {
this.trtcClient.leave();
}
},
};
</script>
<style scoped>
.trtc-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
padding: 10px;
}
.video-container {
width: 100%;
height: 300px;
background: #000;
border-radius: 8px;
overflow: hidden;
}
</style>
uni-app 接入
1. 创建 TRTC 配置
// trtc-config.js
export const TRTC_CONFIG = {
sdkAppID: 'your-sdk-app-id',
userID: 'user-id',
userSig: 'user-sig',
roomID: 'room-id',
template: 'grid',
role: 'anchor',
};
2. 创建 TRTC 实例
// trtc-client.js
import { TRTC_CONFIG } from './trtc-config';
export class TRTCClient {
constructor() {
this.client = null;
this.localStream = null;
this.remoteStreams = new Map();
}
async init() {
try {
// 创建客户端实例
this.client = uni.createTRTCContext({
sdkAppID: TRTC_CONFIG.sdkAppID,
userID: TRTC_CONFIG.userID,
userSig: TRTC_CONFIG.userSig,
template: TRTC_CONFIG.template,
});
// 注册事件监听
this.registerEvents();
// 加入房间
await this.client.join({ roomID: TRTC_CONFIG.roomID });
console.log('加入房间成功');
// 创建本地流
await this.createLocalStream();
} catch (error) {
console.error('TRTC 初始化失败:', error);
throw error;
}
}
registerEvents() {
// 监听远端用户加入
this.client.on('peer-join', (event) => {
console.log('远端用户加入:', event.userID);
});
// 监听远端用户离开
this.client.on('peer-leave', (event) => {
console.log('远端用户离开:', event.userID);
this.remoteStreams.delete(event.userID);
});
// 监听远端流添加
this.client.on('stream-added', (event) => {
console.log('远端流添加:', event.stream);
this.client.subscribe(event.stream);
});
// 监听远端流订阅成功
this.client.on('stream-subscribed', (event) => {
console.log('远端流订阅成功:', event.stream);
this.remoteStreams.set(event.stream.getUserID(), event.stream);
this.playRemoteStream(event.stream);
});
}
async createLocalStream() {
try {
this.localStream = this.client.createStream({
userID: TRTC_CONFIG.userID,
audio: true,
video: true,
});
await this.localStream.initialize();
await this.client.publish(this.localStream);
this.playLocalStream();
} catch (error) {
console.error('创建本地流失败:', error);
throw error;
}
}
playLocalStream() {
this.localStream.play({
elementID: 'local-video',
});
}
playRemoteStream(stream) {
stream.play({
elementID: `remote-video-${stream.getUserID()}`,
});
}
async leave() {
if (this.localStream) {
this.localStream.stop();
this.localStream.close();
}
if (this.client) {
await this.client.leave();
}
}
}
3. 在 uni-app 页面中使用
<!-- pages/trtc/index.vue -->
<template>
<view class="trtc-container">
<live-pusher
id="local-video"
class="video-container"
mode="RTC"
:enable-camera="true"
:enable-mic="true"
:enable-agc="true"
:enable-ans="true"
:enable-echo-cancel="true"
:auto-focus="true"
:beauty="0"
:whiteness="0"
:device-position="'front'"
:muted="false"
:enable-background-mute="false"
:audio-quality="'high'"
:video-quality="'high'"
:min-bitrate="200"
:max-bitrate="1000"
:audio-volume-type="'auto'"
:enable-remote-mirror="false"
:local-mirror="'auto'"
@statechange="onPusherStateChange"
@error="onPusherError"
/>
<live-player
v-for="stream in remoteStreams"
:key="stream.getUserID()"
:id="`remote-video-${stream.getUserID()}`"
class="video-container"
mode="RTC"
:min-cache="0.1"
:max-cache="0.3"
:enable-background-mute="false"
:object-fit="'contain'"
@statechange="onPlayerStateChange"
@error="onPlayerError"
/>
</view>
</template>
<script>
import { TRTCClient } from './trtc-client';
export default {
data() {
return {
trtcClient: null,
remoteStreams: [],
};
},
async onLoad() {
this.trtcClient = new TRTCClient();
try {
await this.trtcClient.init();
} catch (error) {
console.error('TRTC 初始化失败:', error);
uni.showToast({
title: '视频初始化失败',
icon: 'none',
});
}
},
onUnload() {
if (this.trtcClient) {
this.trtcClient.leave();
}
},
methods: {
onPusherStateChange(e) {
console.log('推流状态变化:', e.detail);
},
onPusherError(e) {
console.error('推流错误:', e.detail);
},
onPlayerStateChange(e) {
console.log('播放状态变化:', e.detail);
},
onPlayerError(e) {
console.error('播放错误:', e.detail);
},
},
};
</script>
<style>
.trtc-container {
display: flex;
flex-wrap: wrap;
padding: 10px;
}
.video-container {
width: 100%;
height: 300px;
margin-bottom: 10px;
background: #000;
border-radius: 8px;
overflow: hidden;
}
</style>
多人视频通话实现
1. 房间管理
首先,我们需要创建一个房间管理类来处理多人视频通话的逻辑:
// room-manager.js
export class RoomManager {
constructor(trtcClient) {
this.trtcClient = trtcClient;
this.roomMembers = new Map(); // 存储房间成员信息
this.maxMembers = 9; // 最大支持9人同时视频
}
// 加入房间
async joinRoom(roomId, userId, userSig) {
try {
await this.trtcClient.join({
roomId,
userId,
userSig,
});
console.log('加入房间成功');
return true;
} catch (error) {
console.error('加入房间失败:', error);
return false;
}
}
// 离开房间
async leaveRoom() {
try {
await this.trtcClient.leave();
this.roomMembers.clear();
console.log('离开房间成功');
} catch (error) {
console.error('离开房间失败:', error);
}
}
// 处理新成员加入
handleUserEnter(userId, userInfo) {
if (this.roomMembers.size >= this.maxMembers) {
console.warn('房间已满');
return false;
}
this.roomMembers.set(userId, userInfo);
return true;
}
// 处理成员离开
handleUserLeave(userId) {
this.roomMembers.delete(userId);
}
// 获取房间成员列表
getRoomMembers() {
return Array.from(this.roomMembers.values());
}
}
2. 多人视频布局
创建一个视频布局管理类来处理多人视频的显示:
// video-layout.js
export class VideoLayout {
constructor(container) {
this.container = container;
this.layouts = {
'1': 'single', // 1人布局
'2': 'double', // 2人布局
'3': 'triple', // 3人布局
'4': 'quad', // 4人布局
'5-9': 'grid' // 5-9人网格布局
};
}
// 更新视频布局
updateLayout(memberCount) {
let layoutType;
if (memberCount <= 4) {
layoutType = this.layouts[memberCount];
} else {
layoutType = this.layouts['5-9'];
}
this.container.className = `video-container layout-${layoutType}`;
}
// 添加视频元素
addVideoElement(userId, isLocal = false) {
const videoElement = document.createElement('div');
videoElement.id = isLocal ? 'local-video' : `remote-video-${userId}`;
videoElement.className = 'video-item';
this.container.appendChild(videoElement);
return videoElement;
}
// 移除视频元素
removeVideoElement(userId) {
const element = document.getElementById(`remote-video-${userId}`);
if (element) {
element.remove();
}
}
}
3. 多人视频组件实现
<!-- MultiVideoCall.vue -->
<template>
<div class="multi-video-container">
<div class="video-grid" ref="videoGrid">
<!-- 本地视频 -->
<div id="local-video" class="video-item">
<div class="user-info">
<span>{{ localUserInfo.userName }}</span>
<span class="status" :class="{ 'muted': isAudioMuted }">
{{ isAudioMuted ? '已静音' : '未静音' }}
</span>
</div>
</div>
<!-- 远程视频 -->
<div v-for="member in remoteMembers"
:key="member.userId"
:id="`remote-video-${member.userId}`"
class="video-item">
<div class="user-info">
<span>{{ member.userName }}</span>
<span class="status" :class="{ 'muted': member.isAudioMuted }">
{{ member.isAudioMuted ? '已静音' : '未静音' }}
</span>
</div>
</div>
</div>
<!-- 控制栏 -->
<div class="control-bar">
<button @click="toggleAudio" :class="{ 'active': !isAudioMuted }">
{{ isAudioMuted ? '开启麦克风' : '关闭麦克风' }}
</button>
<button @click="toggleVideo" :class="{ 'active': !isVideoMuted }">
{{ isVideoMuted ? '开启摄像头' : '关闭摄像头' }}
</button>
<button @click="switchCamera">
切换摄像头
</button>
<button @click="leaveRoom" class="leave-btn">
离开房间
</button>
</div>
</div>
</template>
<script>
import { TRTCClient } from './trtc-client';
import { RoomManager } from './room-manager';
import { VideoLayout } from './video-layout';
export default {
name: 'MultiVideoCall',
data() {
return {
trtcClient: null,
roomManager: null,
videoLayout: null,
localUserInfo: {
userId: '',
userName: '',
},
remoteMembers: [],
isAudioMuted: false,
isVideoMuted: false,
};
},
async mounted() {
this.trtcClient = new TRTCClient();
this.roomManager = new RoomManager(this.trtcClient);
this.videoLayout = new VideoLayout(this.$refs.videoGrid);
// 初始化本地用户信息
this.localUserInfo = {
userId: 'user-' + Math.random().toString(36).substr(2, 9),
userName: '我',
};
// 注册事件监听
this.registerEvents();
// 加入房间
await this.joinRoom();
},
methods: {
async joinRoom() {
const roomId = this.$route.query.roomId;
const success = await this.roomManager.joinRoom(
roomId,
this.localUserInfo.userId,
'your-user-sig'
);
if (success) {
await this.startLocalStream();
}
},
async startLocalStream() {
try {
await this.trtcClient.createLocalStream({
audio: true,
video: true,
});
this.trtcClient.playLocalStream();
} catch (error) {
console.error('启动本地流失败:', error);
}
},
registerEvents() {
// 监听用户加入
this.trtcClient.on('user-enter', (event) => {
const { userId, userInfo } = event;
if (this.roomManager.handleUserEnter(userId, userInfo)) {
this.remoteMembers = this.roomManager.getRoomMembers();
this.videoLayout.updateLayout(this.remoteMembers.length + 1);
}
});
// 监听用户离开
this.trtcClient.on('user-leave', (event) => {
const { userId } = event;
this.roomManager.handleUserLeave(userId);
this.remoteMembers = this.roomManager.getRoomMembers();
this.videoLayout.updateLayout(this.remoteMembers.length + 1);
this.videoLayout.removeVideoElement(userId);
});
// 监听远程流添加
this.trtcClient.on('stream-added', (event) => {
const { stream } = event;
this.trtcClient.subscribe(stream);
});
// 监听远程流订阅成功
this.trtcClient.on('stream-subscribed', (event) => {
const { stream } = event;
this.videoLayout.addVideoElement(stream.getUserId());
stream.play(`remote-video-${stream.getUserId()}`);
});
},
async toggleAudio() {
this.isAudioMuted = !this.isAudioMuted;
await this.trtcClient.muteLocalAudio(this.isAudioMuted);
},
async toggleVideo() {
this.isVideoMuted = !this.isVideoMuted;
await this.trtcClient.muteLocalVideo(this.isVideoMuted);
},
async switchCamera() {
await this.trtcClient.switchDevice('video');
},
async leaveRoom() {
await this.roomManager.leaveRoom();
this.$router.push('/');
},
},
beforeDestroy() {
if (this.trtcClient) {
this.roomManager.leaveRoom();
}
},
};
</script>
<style scoped>
.multi-video-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.video-grid {
flex: 1;
display: grid;
gap: 10px;
padding: 10px;
background: #1a1a1a;
}
/* 不同布局的样式 */
.layout-single {
grid-template-columns: 1fr;
}
.layout-double {
grid-template-columns: repeat(2, 1fr);
}
.layout-triple {
grid-template-columns: repeat(3, 1fr);
}
.layout-quad {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.layout-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.video-item {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.user-info {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
}
.status {
margin-left: 10px;
}
.status.muted::before {
content: '🔇';
}
.control-bar {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
background: #2a2a2a;
padding: 0 20px;
}
.control-bar button {
padding: 8px 16px;
border-radius: 20px;
border: none;
background: #3a3a3a;
color: white;
cursor: pointer;
transition: all 0.3s;
}
.control-bar button.active {
background: #4CAF50;
}
.control-bar .leave-btn {
background: #f44336;
}
.control-bar button:hover {
opacity: 0.8;
}
</style>
4. 多人视频通话的主要功能
房间管理
- 创建/加入房间
- 离开房间
- 房间成员管理
- 最大支持9人同时视频
视频控制
- 开启/关闭摄像头
- 开启/关闭麦克风
- 切换摄像头
- 调整视频质量
布局管理
- 自适应布局(1-9人)
- 网格布局
- 视频窗口大小调整
状态显示
- 用户在线状态
- 音频状态
- 视频状态
- 网络质量
5. 性能优化建议
- 视频质量控制
// 根据网络状况动态调整视频质量
async function adjustVideoQuality(networkQuality) {
const qualityMap = {
excellent: { resolution: '720p', bitrate: 1500 },
good: { resolution: '540p', bitrate: 1000 },
poor: { resolution: '360p', bitrate: 600 },
};
const quality = qualityMap[networkQuality] || qualityMap.poor;
await trtcClient.setVideoQuality(quality);
}
- 内存管理
// 及时清理不需要的资源
function cleanupResources() {
// 停止所有视频流
remoteStreams.forEach(stream => {
stream.stop();
stream.close();
});
// 清空成员列表
roomMembers.clear();
// 重置布局
videoLayout.reset();
}
- 自动重连机制
// 实现自动重连
async function handleDisconnection() {
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
await trtcClient.reconnect();
console.log('重连成功');
return true;
} catch (error) {
retryCount++;
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
}
}
console.error('重连失败');
return false;
}
常见问题
1. 权限问题
Web 端
- 确保在 HTTPS 环境下运行
- 确保用户授予了摄像头和麦克风权限
- 检查浏览器是否支持 WebRTC
uni-app 端
- 在 manifest.json 中配置相应权限
{
"permission": {
"scope.camera": {
"desc": "用于视频通话"
},
"scope.record": {
"desc": "用于语音通话"
}
}
}
2. 音视频质量问题
- 检查网络连接质量
- 调整视频分辨率和码率
- 使用 TRTC 提供的网络质量监控功能
3. 调试技巧
- 使用 TRTC 控制台查看房间状态
- 使用 TRTC 提供的日志功能
- 在开发环境中使用 TRTC 提供的测试工具
4. 性能优化
- 使用适当的视频分辨率和码率
- 实现自动重连机制
- 优化内存使用,及时释放资源
参考资源
踩坑记录
1. Web 端常见问题
1.1 浏览器兼容性
- Chrome 浏览器在 HTTPS 环境下才能正常使用摄像头和麦克风
- Safari 浏览器需要用户手动触发才能获取媒体权限
- Firefox 浏览器在某些版本可能存在音频采集问题
解决方案:
// 检查浏览器兼容性
function checkBrowserCompatibility() {
const isHttps = window.location.protocol === 'https:';
const isChrome = /Chrome/.test(navigator.userAgent);
const isSafari = /Safari/.test(navigator.userAgent);
if (!isHttps && isChrome) {
console.warn('请使用 HTTPS 环境');
return false;
}
return true;
}
1.2 设备权限问题
- 用户拒绝授权后,无法再次弹出权限请求
- 设备被其他应用占用时无法获取权限
解决方案:
// 检查设备权限状态
async function checkDevicePermissions() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const hasPermission = devices.some(device => device.label);
if (!hasPermission) {
// 引导用户手动开启权限
console.log('请在浏览器设置中手动开启摄像头和麦克风权限');
}
return hasPermission;
} catch (error) {
console.error('检查设备权限失败:', error);
return false;
}
}
2. uni-app 端常见问题
2.1 小程序环境限制
- 小程序端无法同时使用多个摄像头
- 部分机型可能存在音频采集问题
- 小程序端不支持自定义视频分辨率
解决方案:
// 检查小程序环境
function checkMiniProgramEnv() {
// #ifdef MP-WEIXIN
const systemInfo = uni.getSystemInfoSync();
if (systemInfo.platform === 'ios') {
// iOS 设备特殊处理
return {
maxVideoBitrate: 1000,
maxAudioBitrate: 48,
};
}
// #endif
return {
maxVideoBitrate: 1500,
maxAudioBitrate: 64,
};
}
2.2 页面生命周期问题
- 页面隐藏时视频流可能被系统回收
- 切换页面时可能出现黑屏
解决方案:
// 处理页面生命周期
export default {
onHide() {
// 页面隐藏时暂停视频流
this.trtcClient.pauseLocalStream();
},
onShow() {
// 页面显示时恢复视频流
this.trtcClient.resumeLocalStream();
},
onUnload() {
// 页面卸载时清理资源
this.cleanupResources();
}
}
3. 多人视频通话问题
3.1 网络带宽问题
- 多人同时视频时可能出现卡顿
- 网络波动时视频质量下降
解决方案:
// 动态调整视频质量
async function handleNetworkQuality(quality) {
const memberCount = this.roomMembers.size;
let targetQuality;
if (memberCount <= 2) {
targetQuality = { resolution: '720p', bitrate: 1500 };
} else if (memberCount <= 4) {
targetQuality = { resolution: '540p', bitrate: 1000 };
} else {
targetQuality = { resolution: '360p', bitrate: 600 };
}
if (quality === 'poor') {
targetQuality.bitrate *= 0.7;
}
await this.trtcClient.setVideoQuality(targetQuality);
}
3.2 内存泄漏问题
- 频繁加入退出房间可能导致内存泄漏
- 视频流未及时释放
解决方案:
// 资源清理
function cleanupResources() {
// 停止所有视频流
this.remoteStreams.forEach(stream => {
stream.stop();
stream.close();
});
// 清空成员列表
this.roomMembers.clear();
// 重置布局
this.videoLayout.reset();
// 清理事件监听
this.trtcClient.removeAllListeners();
}
4. 其他常见问题
4.1 签名问题
- userSig 过期导致无法加入房间
- 签名生成错误
解决方案:
// 签名过期处理
async function handleUserSigExpired() {
try {
// 重新获取 userSig
const newUserSig = await this.getNewUserSig();
// 更新 client 配置
await this.trtcClient.updateUserSig(newUserSig);
// 重新加入房间
await this.roomManager.joinRoom(this.roomId, this.userId, newUserSig);
} catch (error) {
console.error('更新签名失败:', error);
}
}
4.2 设备切换问题
- 切换设备时可能出现黑屏
- 设备被占用时无法切换
解决方案:
// 设备切换处理
async function handleDeviceSwitch(deviceType) {
try {
// 先停止当前流
await this.trtcClient.stopLocalStream();
// 切换设备
await this.trtcClient.switchDevice(deviceType);
// 重新启动流
await this.trtcClient.startLocalStream();
} catch (error) {
console.error('切换设备失败:', error);
// 恢复原设备
await this.trtcClient.switchDevice(deviceType);
}
}
这些踩坑记录都是实际开发中遇到的问题和解决方案,希望能帮助大家避免类似的问题。如果遇到其他问题,建议查看 TRTC 官方文档或联系技术支持。