简介:声网 RTC SDK 是提供音视频实时互动云服务的产品,支持多平台多设备运行,可实现一对一单聊、多人群聊,同时具备纯语音通话、高清视频通话和互动直播功能。
功能: 实现音视频实时互动
官网:doc.shengwang.cn/doc/rtc/jav…
相关概念及流程:doc.shengwang.cn/doc/rtc/jav…
前端开发(快速入门):docs.agora.io/en/interact…
前端实现:vue3接入SDK(demo)
import { ref, reactive, onUnmounted } from 'vue';
import AgoraRTC from "agora-rtc-sdk-ng";
/**
* Agora RTC Composition API
* 提供音视频通话功能
*/
export function useAgoraRTC() {
// 响应式状态
const client = ref(null);
const localAudioTrack = ref(null);
const localVideoTrack = ref(null);
const remoteUsers = ref(new Map());
const isConnected = ref(false);
const isPublishing = ref(false);
const connectionState = ref('disconnected'); // disconnected, connecting, connected, reconnecting
const error = ref(null);
// 配置参数
const config = reactive({
appId: import.meta.env.VITE_AGORA_APP_ID || "<-- Insert app ID -->",
channel: "default-channel",
token: import.meta.env.VITE_AGORA_TOKEN || "<-- Insert token -->",
uid: Math.floor(Math.random() * 100000)
});
// 初始化客户端
const initializeClient = () => {
if (client.value) return;
try {
client.value = AgoraRTC.createClient({
mode: "live",
codec: "vp8"
});
setupEventListeners();
error.value = null;
} catch (err) {
error.value = `初始化客户端失败: ${err.message}`;
console.error("初始化客户端失败:", err);
}
};
// 设置事件监听器
const setupEventListeners = () => {
if (!client.value) return;
// 用户发布媒体流
client.value.on("user-published", async (user, mediaType) => {
try {
await client.value.subscribe(user, mediaType);
console.log("订阅用户成功:", user.uid);
if (mediaType === "video") {
remoteUsers.value.set(user.uid, user);
}
if (mediaType === "audio") {
user.audioTrack?.play();
}
} catch (err) {
error.value = `订阅用户失败: ${err.message}`;
console.error("订阅用户失败:", err);
}
});
// 用户取消发布
client.value.on("user-unpublished", (user) => {
remoteUsers.value.delete(user.uid);
});
// 用户离开
client.value.on("user-left", (user) => {
remoteUsers.value.delete(user.uid);
});
// 连接状态变化
client.value.on("connection-state-change", (state) => {
connectionState.value = state;
isConnected.value = state === 'CONNECTED';
console.log("连接状态变化:", state);
});
// 异常处理
client.value.on("exception", (event) => {
error.value = `Agora异常: ${event.code} - ${event.msg}`;
console.error("Agora异常:", event);
});
};
// 创建本地音视频轨道
const createLocalTracks = async () => {
try {
localAudioTrack.value = await AgoraRTC.createMicrophoneAudioTrack({
encoderConfig: "music_standard",
AEC: true, // 回声消除
ANS: true, // 噪声抑制
AGC: true // 自动增益控制
});
localVideoTrack.value = await AgoraRTC.createCameraVideoTrack({
encoderConfig: "1080p_2",
optimizationMode: "detail",
cameraId: await getCameraDeviceId()
});
error.value = null;
return true;
} catch (err) {
error.value = `创建本地轨道失败: ${err.message}`;
console.error("创建本地轨道失败:", err);
return false;
}
};
// 获取摄像头设备ID
const getCameraDeviceId = async () => {
try {
const devices = await AgoraRTC.getCameras();
return devices.length > 0 ? devices[0].deviceId : undefined;
} catch {
return undefined;
}
};
// 作为主持人加入频道
const joinAsHost = async (customConfig = {}) => {
try {
Object.assign(config, customConfig);
initializeClient();
if (!client.value) {
throw new Error("客户端未初始化");
}
connectionState.value = 'connecting';
await client.value.join(config.appId, config.channel, config.token, config.uid);
await client.value.setClientRole("host");
const tracksCreated = await createLocalTracks();
if (tracksCreated) {
await client.value.publish([localAudioTrack.value, localVideoTrack.value]);
isPublishing.value = true;
}
connectionState.value = 'connected';
error.value = null;
return true;
} catch (err) {
error.value = `主持人加入失败: ${err.message}`;
connectionState.value = 'disconnected';
console.error("主持人加入失败:", err);
return false;
}
};
// 作为观众加入频道
const joinAsAudience = async (customConfig = {}) => {
try {
Object.assign(config, customConfig);
initializeClient();
if (!client.value) {
throw new Error("客户端未初始化");
}
connectionState.value = 'connecting';
await client.value.join(config.appId, config.channel, config.token, config.uid);
await client.value.setClientRole("audience", { level: 2 });
connectionState.value = 'connected';
error.value = null;
return true;
} catch (err) {
error.value = `观众加入失败: ${err.message}`;
connectionState.value = 'disconnected';
console.error("观众加入失败:", err);
return false;
}
};
// 离开频道
const leaveChannel = async () => {
try {
// 停止发布
if (isPublishing.value && client.value) {
await client.value.unpublish();
isPublishing.value = false;
}
// 关闭本地轨道
if (localAudioTrack.value) {
localAudioTrack.value.close();
localAudioTrack.value = null;
}
if (localVideoTrack.value) {
localVideoTrack.value.close();
localVideoTrack.value = null;
}
// 离开频道
if (client.value) {
await client.value.leave();
client.value = null;
}
// 清理状态
remoteUsers.value.clear();
isConnected.value = false;
connectionState.value = 'disconnected';
error.value = null;
return true;
} catch (err) {
error.value = `离开频道失败: ${err.message}`;
console.error("离开频道失败:", err);
return false;
}
};
// 切换音频状态
const toggleAudio = () => {
if (localAudioTrack.value) {
const enabled = !localAudioTrack.value.enabled;
localAudioTrack.value.setEnabled(enabled);
return enabled;
}
return false;
};
// 切换视频状态
const toggleVideo = () => {
if (localVideoTrack.value) {
const enabled = !localVideoTrack.value.enabled;
localVideoTrack.value.setEnabled(enabled);
return enabled;
}
return false;
};
// 获取音频状态
const getAudioStatus = () => {
return localAudioTrack.value ? localAudioTrack.value.enabled : false;
};
// 获取视频状态
const getVideoStatus = () => {
return localVideoTrack.value ? localVideoTrack.value.enabled : false;
};
// 切换摄像头
const switchCamera = async (deviceId) => {
if (!localVideoTrack.value) return false;
try {
await localVideoTrack.value.setDevice(deviceId);
return true;
} catch (err) {
error.value = `切换摄像头失败: ${err.message}`;
console.error("切换摄像头失败:", err);
return false;
}
};
// 获取可用设备列表
const getDevices = async () => {
try {
const [microphones, cameras, speakers] = await Promise.all([
AgoraRTC.getMicrophones(),
AgoraRTC.getCameras(),
AgoraRTC.getPlaybackDevices()
]);
return {
microphones,
cameras,
speakers
};
} catch (err) {
error.value = `获取设备列表失败: ${err.message}`;
console.error("获取设备列表失败:", err);
return { microphones: [], cameras: [], speakers: [] };
}
};
// 组件卸载时清理资源
onUnmounted(() => {
leaveChannel();
});
return {
// 状态
client,
localAudioTrack,
localVideoTrack,
remoteUsers,
isConnected,
isPublishing,
connectionState,
config,
error,
// 方法
initializeClient,
joinAsHost,
joinAsAudience,
leaveChannel,
toggleAudio,
toggleVideo,
getAudioStatus,
getVideoStatus,
switchCamera,
getDevices,
createLocalTracks
};
}
<template>
<div class="video-container">
<!-- 头部标题 -->
<div class="video-header">
<h1>互动直播 Agora Web SDK</h1>
<div class="connection-status" :class="connectionState">
状态: {{ getStatusText(connectionState) }}
</div>
</div>
<!-- 错误提示 -->
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- 视频区域 -->
<div class="video-content">
<!-- 本地视频 -->
<div class="video-section">
<h3>本地视频</h3>
<div class="video-player">
<div
ref="localVideoContainer"
class="video-element"
:class="{ 'video-disabled': !getVideoStatus() }"
></div>
<div v-if="!isConnected" class="video-placeholder">
未连接
</div>
</div>
<div class="video-controls" v-if="isPublishing">
<el-button
:type="getAudioStatus() ? 'primary' : 'danger'"
@click="toggleAudio"
:disabled="!isConnected"
>
{{ getAudioStatus() ? '静音' : '取消静音' }}
</el-button>
<el-button
:type="getVideoStatus() ? 'primary' : 'danger'"
@click="toggleVideo"
:disabled="!isConnected"
>
{{ getVideoStatus() ? '关闭视频' : '开启视频' }}
</el-button>
</div>
</div>
<!-- 远程视频 -->
<div class="video-section">
<h3>远程用户 ({{ remoteUsers.size }})</h3>
<div class="remote-videos">
<div
v-for="[uid, user] in remoteUsers"
:key="uid"
class="remote-video-item"
>
<div
:ref="el => setRemoteVideoRef(uid, el)"
class="video-element remote"
></div>
<div class="user-info">用户 {{ uid }}</div>
</div>
<div v-if="remoteUsers.size === 0" class="no-remote-users">
暂无远程用户
</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-buttons">
<el-button
type="primary"
@click="handleJoinAsHost"
:disabled="isConnected"
:loading="connectionState === 'connecting'"
>
作为主持人加入
</el-button>
<el-button
type="success"
@click="handleJoinAsAudience"
:disabled="isConnected"
:loading="connectionState === 'connecting'"
>
作为观众加入
</el-button>
<el-button
type="danger"
@click="handleLeave"
:disabled="!isConnected"
>
离开频道
</el-button>
<el-button
type="info"
@click="handleRefresh"
>
刷新页面
</el-button>
</div>
<!-- 配置面板 -->
<div class="config-panel">
<el-collapse>
<el-collapse-item title="频道配置">
<div class="config-form">
<el-form :model="customConfig" label-width="120px">
<el-form-item label="App ID">
<el-input v-model="customConfig.appId" placeholder="请输入App ID"></el-input>
</el-form-item>
<el-form-item label="频道名称">
<el-input v-model="customConfig.channel" placeholder="请输入频道名称"></el-input>
</el-form-item>
<el-form-item label="Token">
<el-input v-model="customConfig.token" placeholder="请输入Token"></el-input>
</el-form-item>
<el-form-item label="用户ID">
<el-input v-model="customConfig.uid" type="number" placeholder="请输入用户ID"></el-input>
</el-form-item>
</el-form>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { useAgoraRTC } from '@/composables/useAgoraRTC';
// 使用Agora RTC
const {
client,
localAudioTrack,
localVideoTrack,
remoteUsers,
isConnected,
isPublishing,
connectionState,
config,
error,
joinAsHost,
joinAsAudience,
leaveChannel,
toggleAudio,
toggleVideo,
getAudioStatus,
getVideoStatus
} = useAgoraRTC();
// 本地视频容器引用
const localVideoContainer = ref(null);
// 远程视频容器引用映射
const remoteVideoRefs = ref(new Map());
// 自定义配置
const customConfig = reactive({
appId: '',
channel: '',
token: '',
uid: null
});
// 设置远程视频引用
const setRemoteVideoRef = (uid, el) => {
if (el) {
remoteVideoRefs.value.set(uid, el);
playRemoteVideo(uid);
}
};
// 播放远程视频
const playRemoteVideo = (uid) => {
const user = remoteUsers.value.get(uid);
const container = remoteVideoRefs.value.get(uid);
if (user && user.videoTrack && container) {
user.videoTrack.play(container);
}
};
// 播放本地视频
const playLocalVideo = () => {
if (localVideoTrack.value && localVideoContainer.value) {
localVideoTrack.value.play(localVideoContainer.value);
}
};
// 获取状态文本
const getStatusText = (state) => {
const statusMap = {
'disconnected': '未连接',
'connecting': '连接中...',
'connected': '已连接',
'reconnecting': '重新连接中...',
'DISCONNECTED': '断开连接',
'CONNECTING': '连接中',
'CONNECTED': '已连接',
'RECONNECTING': '重新连接中',
'ABORTED': '连接中止'
};
return statusMap[state] || state;
};
// 作为主持人加入
const handleJoinAsHost = async () => {
const success = await joinAsHost({
...customConfig,
uid: customConfig.uid || config.uid
});
if (success) {
ElMessage.success('主持人加入成功');
await nextTick();
playLocalVideo();
} else {
ElMessage.error('主持人加入失败');
}
};
// 作为观众加入
const handleJoinAsAudience = async () => {
const success = await joinAsAudience({
...customConfig,
uid: customConfig.uid || config.uid
});
if (success) {
ElMessage.success('观众加入成功');
} else {
ElMessage.error('观众加入失败');
}
};
// 离开频道
const handleLeave = async () => {
const success = await leaveChannel();
if (success) {
ElMessage.success('离开频道成功');
} else {
ElMessage.error('离开频道失败');
}
};
// 刷新页面
const handleRefresh = () => {
window.location.reload();
};
// 监听远程用户变化,自动播放视频
import { watch } from 'vue';
watch(remoteUsers, (newUsers) => {
nextTick(() => {
newUsers.forEach((user, uid) => {
playRemoteVideo(uid);
});
});
}, { deep: true });
// 组件挂载时初始化
onMounted(() => {
// 设置默认配置
Object.assign(customConfig, {
appId: config.appId,
channel: config.channel,
token: config.token,
uid: config.uid
});
});
// 组件卸载时清理
onUnmounted(() => {
leaveChannel();
});
</script>
<style scoped>
.video-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.video-header h1 {
margin: 0;
color: #303133;
}
.connection-status {
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
.connection-status.connected,
.connection-status.CONNECTED {
background-color: #f0f9ff;
color: #1890ff;
}
.connection-status.connecting,
.connection-status.CONNECTING {
background-color: #fff7e6;
color: #fa8c16;
}
.connection-status.disconnected,
.connection-status.DISCONNECTED {
background-color: #fff2f0;
color: #ff4d4f;
}
.error-message {
background-color: #fff2f0;
color: #ff4d4f;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border: 1px solid #ffccc7;
}
.video-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.video-section {
background: #fafafa;
border-radius: 8px;
padding: 16px;
}
.video-section h3 {
margin: 0 0 16px 0;
color: #606266;
}
.video-player {
position: relative;
background: #000;
border-radius: 4px;
overflow: hidden;
min-height: 300px;
}
.video-element {
width: 100%;
height: 300px;
object-fit: cover;
}
.video-element.video-disabled {
opacity: 0.3;
}
.video-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 18px;
}
.video-controls {
margin-top: 12px;
display: flex;
gap: 8px;
}
.remote-videos {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.remote-video-item {
position: relative;
background: #000;
border-radius: 4px;
overflow: hidden;
min-height: 150px;
}
.remote-video-item .video-element {
height: 150px;
}
.user-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 4px 8px;
font-size: 12px;
}
.no-remote-users {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #909399;
font-style: italic;
}
.control-buttons {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
}
.config-panel {
background: #fafafa;
border-radius: 8px;
padding: 16px;
}
.config-form {
max-width: 500px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.video-content {
grid-template-columns: 1fr;
}
.control-buttons {
flex-direction: column;
align-items: center;
}
.control-buttons .el-button {
width: 100%;
max-width: 200px;
}
}
</style>