记事-vue3项目整合Agora声网sdk实现RTC视频通话

vue3项目整合Agora声网sdk实现RTC视频通话

vu3项目安装依赖

注意vue3项目的node版本要大于node22,这是声网sdk限制的

cmd 复制代码
npm install agora-rtc-sdk-ng --save

web视频通话弹窗示例

以下案例是一个点击el-button按钮后,打开一个视频通话弹窗的vue示例

代码如下

html 复制代码
<template>
  <div class="about-container">
    <h1>测试 Element Plus</h1>
    <!-- 1. 默认只展示一个触发按钮 -->
    <el-button type="primary" size="large" @click="startCallWorkflow">
      发起视频通话
    </el-button>

    <!-- 2. 视频通话弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      title="视频通话"
      width="360px"
      :close-on-click-modal="false"
      :show-close="false"
      @close="handleDialogClose"
    >
      <!-- 顶部:通话时长 (仅在通话中显示) -->
      <div v-if="callStatus === 'connected'" class="call-timer">
        通话时长: {{ formattedTime }}
      </div>

      <!-- 视频区域 -->
      <div class="video-wrapper">
        <div id="video-container" class="video-container">
          <!-- 本地视频小窗 -->
          <div id="local-video" class="local-video-box"></div>
          <!-- 远端视频大窗 -->
          <div id="remote-video" class="remote-video-box"></div>
        </div>
        
        <!-- 状态提示文字 -->
        <div v-if="callStatus === 'calling'" class="status-overlay">
          正在呼叫对方...
        </div>
      </div>

      <!-- 底部操作按钮区 -->
      <template #footer>
        <div class="dialog-footer">
          <!-- 场景A: 呼叫中 -> 显示 [接听] 和 [挂断/忙] -->
          <!-- 注意:通常发起方不会看到"接听",这里假设你是接收方逻辑,或者模拟双向流程。
               如果是发起方,通常只有"取消呼叫"。
               根据题目要求:"弹窗展示...默认只有[接听]和[事忙挂断]",这通常是**被叫方**视角。
               为了演示完整,我保留这两个按钮,但实际业务中发起方和被叫方UI不同。
               这里我们简化为:点击主按钮后,进入"等待对方接听"或"模拟被叫"状态。
          -->
          
          <div v-if="callStatus === 'calling'" class="btn-group">
            <el-button type="success" @click="acceptCall">
              <el-icon><VideoCamera /></el-icon> 接听
            </el-button>
            <el-button type="danger" @click="rejectCall">
              <el-icon><PhoneFilled /></el-icon> 事忙挂断
            </el-button>
          </div>

          <!-- 场景B: 通话中 -> 显示 [中断通话] -->
          <div v-if="callStatus === 'connected'" class="btn-group">
            <el-button type="danger" size="large" circle @click="endCall">
              <el-icon><PhoneFilled /></el-icon>
            </el-button>
            <span class="hangup-text">中断通话</span>
          </div>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
// 添加 computed 到导入列表中
import { ref, computed, onUnmounted, nextTick } from 'vue';
import AgoraRTC from 'agora-rtc-sdk-ng';
import { ElMessage } from 'element-plus';
import { VideoCamera, PhoneFilled } from '@element-plus/icons-vue';

// --- 声网配置 ---
const appId = '此处填写声网控制台中的应用的appId'; //由声网控制台签发-表示rtc应用名称对应的appId
const channel = '此处填写RTC的通道'; //两个人通话时必须处于同一个RTC通道
const token = '此处填写声网控制台签发的临时token有效期24小时(后期可以使用java/python等后端返回的生产token)'; 
const userId = 12345; //此处应填写进入通话的用户id ,比如张三李四通话时,张三用张三的uid ,李四用李四的uid
// --- 状态管理 ---
const dialogVisible = ref(false);
const callStatus = ref('idle'); // idle | calling | connected
if (process.env.NODE_ENV === 'development') {
  AgoraRTC.enableLogUpload(); 
  // 如果需要更详细的控制台日志,可以设置日志级别
  // AgoraRTC.setLogLevel(4); // 4 = DEBUG
}
const client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
const localTracks = { videoTrack: null, audioTrack: null };
const remoteUsers = ref([]);

// --- 计时器相关 ---
const timerInterval = ref(null);
const seconds = ref(0);

const formattedTime = computed(() => {
  const mins = Math.floor(seconds.value / 60).toString().padStart(2, '0');
  const secs = (seconds.value % 60).toString().padStart(2, '0');
  return `${mins}:${secs}`;
});

// --- 核心逻辑 ---


// 【重要修改】封装监听器绑定逻辑,确保只绑定一次
const setupClientListeners = () => {
  console.log('[RTC] 设置客户端监听器...');
  // 防止重复绑定
  client.removeAllListeners();

  client.on('user-published', async (user, mediaType) => {
    console.log('[RTC] 事件: user-published', { uid: user.uid, mediaType });
    try {
      await client.subscribe(user, mediaType);
      console.log('[RTC] 订阅成功', { uid: user.uid, mediaType });
      
      if (mediaType === 'video') {
        console.log('[RTC] 播放远端视频到 #remote-video');
        user.videoTrack.play('remote-video');
        if (!remoteUsers.value.find(u => u.uid === user.uid)) {
          remoteUsers.value.push(user);
        }
      }
      
      if (mediaType === 'audio') {
        console.log('[RTC] 播放远端音频');
        user.audioTrack.play();
      }
    } catch (error) {
      console.error('[RTC] 订阅或播放失败:', error);
    }
  });

  client.on('user-unpublished', (user, mediaType) => {
    console.log('[RTC] 事件: user-unpublished', { uid: user.uid, mediaType });
    if (mediaType === 'video') {
      // 注意:unpublished 时 track 可能已经不可用,直接清理引用
      remoteUsers.value = remoteUsers.value.filter(u => u.uid !== user.uid);
      console.log('[RTC] 远端视频停止,更新远程用户列表');
    }
    if (mediaType === 'audio') {
      console.log('[RTC] 远端音频停止');
    }
  });
  
  client.on('user-joined', (user) => {
    console.log('[RTC] 事件: user-joined', { uid: user.uid });
  });

  client.on('user-left', (user) => {
    console.log('[RTC] 事件: user-left', { uid: user.uid });
  });
  
  // 监听连接状态变化
  client.on('connection-state-change', (curState, prevState, reason) => {
    console.log('[RTC] 连接状态变更', { curState, prevState, reason });
  });
};

// 1. 点击主按钮:打开弹窗,进入"呼叫中"状态,并初始化本地流(但不发布,或者发布取决于业务逻辑)
// 这里为了模拟"接听"前的状态,我们先打开摄像头预览,但不加入频道,或者加入频道但不推流?
// 声网逻辑通常是:Join Channel -> Publish。
// 为了符合题目"点击接听后才正式通话",我们可以这样设计:
// 点击主按钮 -> 打开弹窗 -> 创建本地流并预览(本地能看到自己)-> 状态为 calling
// 点击接听 -> 真正 Join Channel 并发布流 -> 状态为 connected
const startCallWorkflow = async () => {
  console.log('[RTC] 开始呼叫流程...');
  dialogVisible.value = true;
  callStatus.value = 'calling';
  seconds.value = 0;
  
  try {
    console.log('[RTC] 请求摄像头和麦克风权限...');
    localTracks.videoTrack = await AgoraRTC.createCameraVideoTrack();
    localTracks.audioTrack = await AgoraRTC.createMicrophoneAudioTrack();
    console.log('[RTC] 本地轨道创建成功');
    
    await nextTick();
    console.log('[RTC] 本地视频预览到 #local-video');
    localTracks.videoTrack.play('local-video');
    
  } catch (error) {
    console.error('[RTC] 获取媒体设备失败:', error);
    ElMessage.error('无法访问摄像头或麦克风: ' + error.message);
    dialogVisible.value = false;
  }
};

// 2. 点击接听:加入频道,发布流,开始计时
const acceptCall = async () => {
  console.log('[RTC] 点击接听,准备加入频道...');
  try {
    ElMessage.info('正在连接...');
    
    // 1. 设置监听器
    setupClientListeners();

    // 2. 加入频道
    console.log('[RTC] 正在加入频道...', { appId, channel, userId });
    const uid = await client.join(appId, channel, token, userId);
    console.log('[RTC] 加入频道成功! 本地 UID:', uid);
    
    // 3. 发布本地流
    console.log('[RTC] 正在发布本地流...', { hasVideo: !!localTracks.videoTrack, hasAudio: !!localTracks.audioTrack });
    await client.publish(Object.values(localTracks).filter(t => t)); // 过滤掉可能的 null
    console.log('[RTC] 本地流发布成功');
    
    callStatus.value = 'connected';
    startTimer();
    
    ElMessage.success('已接通');
  } catch (error) {
    console.error('[RTC] 加入频道或发布流失败:', error);
    // 打印更详细的错误信息以便调试
    if (error.code) {
      console.error('[RTC] 错误代码:', error.code);
      console.error('[RTC] 错误消息:', error.message);
    }
    ElMessage.error('连接失败: ' + (error.message || '未知错误'));
    endCall();
  }
};

// 3. 点击事忙挂断/中断通话:清理资源
const rejectCall = () => {
  console.log('[RTC] 用户拒绝呼叫');
  endCall();
  ElMessage.info('已挂断');
};

const endCall = async () => {
  console.log('[RTC] 开始结束通话清理工作...');
  stopTimer();
  
  // 清理本地轨道
  for (const trackName in localTracks) {
    const track = localTracks[trackName];
    if (track) {
      console.log(`[RTC] 关闭本地轨道: ${trackName}`);
      track.stop();
      track.close();
    }
  }
  localTracks.videoTrack = null;
  localTracks.audioTrack = null;

  // 清理远端轨道
  console.log('[RTC] 清理远端用户轨道...', remoteUsers.value.length);
  remoteUsers.value.forEach(user => {
    if (user.videoTrack) {
      user.videoTrack.stop();
      user.videoTrack.close();
    }
    if (user.audioTrack) {
      user.audioTrack.stop();
      user.audioTrack.close();
    }
  });
  remoteUsers.value = [];

  // 离开频道
  if (client.connectionState === 'CONNECTED') {
    console.log('[RTC] 正在离开频道...');
    try {
      await client.leave();
      console.log('[RTC] 已离开频道');
    } catch (e) {
      console.error('[RTC] 离开频道时出错:', e);
    }
  } else {
    console.log('[RTC] 客户端未连接,无需离开频道');
  }
  
  // 移除监听器
  client.removeAllListeners();
  console.log('[RTC] 监听器已移除');

  callStatus.value = 'idle';
  dialogVisible.value = false;
  console.log('[RTC] 通话流程结束');
};

// 处理弹窗关闭(比如点击遮罩层,虽然设置了false,但以防万一)
const handleDialogClose = () => {
  console.log('[RTC] 弹窗关闭触发');
  endCall();
};

// --- 辅助函数 ---

// const handleUserPublished = async (user, mediaType) => {
//   await client.subscribe(user, mediaType);
//   if (mediaType === 'video') {
//     user.videoTrack.play('remote-video');
//     remoteUsers.value.push(user);
//   }
//   if (mediaType === 'audio') {
//     user.audioTrack.play();
//   }
// };

// const handleUserUnpublished = (user, mediaType) => {
//   if (mediaType === 'video') {
//     user.videoTrack.stop();
//     user.videoTrack.close();
//     remoteUsers.value = remoteUsers.value.filter(u => u.uid !== user.uid);
//   }
//   if (mediaType === 'audio') {
//     user.audioTrack.stop();
//     user.audioTrack.close();
//   }
// };

const startTimer = () => {
  stopTimer();
  timerInterval.value = setInterval(() => {
    seconds.value++;
  }, 1000);
};

const stopTimer = () => {
  if (timerInterval.value) {
    clearInterval(timerInterval.value);
    timerInterval.value = null;
  }
};

// 组件卸载时确保清理
onUnmounted(() => {
  console.log('[RTC] 组件卸载,强制清理');
  endCall();
});
</script>

<style scoped>
.about-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f7fa;
}



.video-wrapper {
  position: relative;
  width: 100%;
  height: 400px; /* 固定高度供弹窗使用 */
  background-color: #000;
}

.video-container {
  position: relative;
  width: 100%;
  height: 100%;
}

/* 远端视频全屏 */
.remote-video-box {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 本地视频小窗 */
.local-video-box {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 90px;
  height: 120px;
  border: 2px solid #fff;
  z-index: 10;
  background-color: #333;
  box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}

.status-overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 18px;
  background-color: rgba(0, 0, 0, 0.5);
  padding: 10px 20px;
  border-radius: 4px;
  z-index: 5;
}

.call-timer {
  text-align: center;
  font-size: 16px;
  color: #409eff;
  margin-bottom: 10px;
  font-weight: bold;
}

.dialog-footer {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.btn-group {
  display: flex;
  gap: 20px;
  align-items: center;
}

.hangup-text {
  margin-top: 5px;
  font-size: 12px;
  color: #f56c6c;
}
</style>

注意事项

声网web视频通话,只支持localhost 或公网https访问 ,这是声网sdk限制的;

举例: 这个vue3项目部署到公网https服务器上 ;然后2个用户在浏览器访问这个https的前端页面地址后,才能正确初始化sdk,然后用户浏览器提示是否授权打开摄像头和麦克风,用户授权后才能正常打开视频通话界面,从而看到彼此的摄像头;

本地开发调试时,可以自己用localhost访问,让对方用https访问 (可以使用花生壳提前做好https穿透到本机vue3项目)来调试画面;没问题之后再发布生产环境比如阿里云配置域名和https就行;

画中画界面,需要自己前端代码绘制,如图,我是绘制的主画面显示对方摄像头,右上角的小框画面显示自己的摄像头;

整个webRTC通话的流程如下,app客户端可以是WEB 或安卓apk 或IOS app;服务端是java/python等服务端(用于签发token) ;RTC-SDK是终端app集成的声网sdk ;

服务端签发token的示例如下

java示例代码 官方githug代码

java 复制代码
package io.agora.sample;
import io.agora.media.RtcTokenBuilder2;
import io.agora.media.RtcTokenBuilder2.Role;

public class RtcTokenBuilder2Sample {
    // 获取环境变量 AGORA_APP_ID 的值。请确保你将该变量设为你在声网控制台获取的 App ID
    static String appId = System.getenv("AGORA_APP_ID");
    // 获取环境变量 AGORA_APP_CERTIFICATE 的值。请确保你将该变量设为你在声网控制台获取的 App 证书
    static String appCertificate = System.getenv("AGORA_APP_CERTIFICATE");
    // 将 channelName 替换为需要加入的频道名
    static String channelName = "channelName";
    // 填入你实际的用户 ID
    static int uid = 2082341273;
    // Token 的有效时间,单位秒
    static int tokenExpirationInSeconds = 3600;
    // 所有的权限的有效时间,单位秒,声网建议你将该参数和 Token 的有效时间设为一致
    static int privilegeExpirationInSeconds = 3600;

    public static void main(String[] args) {
        System.out.printf("App Id: %s\n", appId);
        System.out.printf("App Certificate: %s\n", appCertificate);
        if (appId == null || appId.isEmpty() || appCertificate == null || appCertificate.isEmpty()) {
            System.out.printf("Need to set environment variable AGORA_APP_ID and AGORA_APP_CERTIFICATE\n");
            return;
        }
        // 生成 Token
        RtcTokenBuilder2 token = new RtcTokenBuilder2();
        String result = token.buildTokenWithUid(appId, appCertificate, channelName, uid, Role.ROLE_PUBLISHER, tokenExpirationInSeconds, privilegeExpirationInSeconds);
        System.out.printf("Token with uid: %s\n", result);
    }
}

python示例代码 官方python3代码

python 复制代码
import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from src.RtcTokenBuilder2 import *


def main():
    # 获取环境变量 AGORA_APP_ID 的值。请确保你将该变量设为你在声网控制台获取的 App ID
    app_id = os.environ.get("AGORA_APP_ID")
    # 获取环境变量 AGORA_APP_CERTIFICATE 的值。请确保你将该变量设为你在声网控制台获取的 App 证书
    app_certificate = os.environ.get("AGORA_APP_CERTIFICATE")
    # 将 channel_name 替换为需要加入的频道名
    channel_name = "channel_name"
    # 填入你实际的用户 ID
    uid = "uid"
    # Token 的有效时间,单位秒
    token_expiration_in_seconds = 3600
    # 所有的权限的有效时间,单位秒,声网建议你将该参数和 Token 的有效时间设为一致
    privilege_expiration_in_seconds = 3600

    print("App Id: %s" % app_id)
    print("App Certificate: %s" % app_certificate)
    if not app_id or not app_certificate:
        print("Need to set environment variable AGORA_APP_ID and AGORA_APP_CERTIFICATE")
        return

    # 生成 Token
    token = RtcTokenBuilder.build_token_with_uid(app_id, app_certificate, channel_name, uid, Role_Publisher,
                                                 token_expiration_in_seconds, privilege_expiration_in_seconds)
    print("Token with int uid: {}".format(token))


if __name__ == "__main__":
    main()

golang示例代码

go 复制代码
package main

import (
    "fmt"
    "os"

    rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtctokenbuilder2"
)

func main() {
    // 获取环境变量 AGORA_APP_ID 的值。请确保你将该变量设为你在声网控制台获取的 App ID
    appId := os.Getenv("AGORA_APP_ID")
    // 获取环境变量 AGORA_APP_CERTIFICATE 的值。请确保你将该变量设为你在声网控制台获取的 App 证书
    appCertificate := os.Getenv("AGORA_APP_CERTIFICATE")
    // 将 channelName 替换为需要加入的频道名
    channelName := "channelName"
    // 填入你实际的用户 ID
    uid := uint32(uid)
    // Token 的有效时间,单位秒
    tokenExpirationInSeconds := uint32(3600)
    // 所有的权限的有效时间,单位秒,声网建议你将该参数和 Token 的有效时间设为一致
    privilegeExpirationInSeconds := uint32(3600)

    fmt.Println("App Id:", appId)
    fmt.Println("App Certificate:", appCertificate)
    if appId == "" || appCertificate == "" {
        fmt.Println("Need to set environment variable AGORA_APP_ID and AGORA_APP_CERTIFICATE")
        return
    }
    // 生成 Token
    result, err := rtctokenbuilder.BuildTokenWithUid(appId, appCertificate, channelName, uid, rtctokenbuilder.RolePublisher, tokenExpirationInSeconds, privilegeExpirationInSeconds)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("Token with int uid: %s\n", result)
    }
}

云录制

声网视频通话支持云录制,提供了rest接口、java sdk ,即要么前端录制,要么后端录制

云录制rest接口 (前端来录制)

需要前端调用3个接口

typescript 复制代码
// 声网云端录制 - 前端直连 Agora API(需在 .env 中配置以下变量)
// VITE_AGORA_CUSTOMER_KEY=your_customer_key
// VITE_AGORA_CUSTOMER_SECRET=your_customer_secret

const AGORA_CUSTOMER_KEY = import.meta.env.VITE_AGORA_CUSTOMER_KEY || '';
const AGORA_CUSTOMER_SECRET = import.meta.env.VITE_AGORA_CUSTOMER_SECRET || '';
const AGORA_RECORDING_API = 'https://api.sd-rtn.com/v1/apps';

function getAgoraBasicAuth(): string {
  return 'Basic ' + btoa(AGORA_CUSTOMER_KEY + ':' + AGORA_CUSTOMER_SECRET);
}

/**
 * 声网云端录制 - 获取 resourceId
 * POST https://api.sd-rtn.com/v1/apps/{appid}/cloud_recording/acquire
 * 返回数据结构
 * {
  "cname": "录制的频道名。",
  "uid": "字符串内容为云端录制服务在 RTC 频道内使用的 UID,用于标识频道内的录制服务。",
  "resourceId": "云端录制资源 Resource ID。使用这个 Resource ID 可以开始一段云端录制。这个 Resource ID 的有效期为 5 分钟,超时需要重新请求。"
}
 */
export async function acquireCloudRecording(
  appId: string,  // 应用 ID
   channel: string,  //设置待录制的频道名
    token: string,  //用于鉴权的动态密钥(Token)。如果你的项目已启用 App 证书,则务必在该字段中传入你项目的动态密钥
    uid:string,  //字符串内容为云端录制服务在频道内使用的 UID,用于标识频道内的录制服务
     sn: string,  // 设备编码
     ymd: string, // yyyyMMDD 如20230705
     hms: string, // 时分秒 如 102133
  ): Promise<any> {
  const res = await fetch(`${AGORA_RECORDING_API}/${appId}/cloud_recording/acquire`, {
    method: 'POST',
    headers: {
      'Authorization': getAgoraBasicAuth(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      cname: channel,
      uid: uid,   
      clientRequest: {scene: 0, resourceExpiredHour: 24, startParameter: {
        token:token,
        recordingConfig: {
            channelType: 1,
            streamTypes: 2,
            audioProfile: 2,
            videoStreamType: 0,
            maxIdleTime: 30,
            transcodingConfig: {
              width: 640,
              height: 360,
              fps: 15,
              bitrate: 500,
              mixedVideoLayout: 0,
              backgroundColor: '#000000'
            },
            subscribeVideoUids: ['#allstream#'],
            subscribeAudioUids: ['#allstream#']
          },
          recordingFileConfig: {avFileType: ['hls', 'mp4']},
          storageConfig: { //需要配置录制到某个OSS云存储,这里以腾讯COS为例
            vendor: 3,  // 3: 腾讯云
            region: 3,  // 3: 广州
            bucket: '存储桶名称',
            accessKey: 'COS访问key',
            secretKey: 'COS访问密钥',
            fileNamePrefix: ['robot','OFFICE',sn,'SERVICE',ymd,hms],
          },
      }}
    }),
  });
  return res.json();
}

/**
 * 声网云端录制 - 开始录制
 * POST https://api.sd-rtn.com/v1/apps/{appid}/cloud_recording/resourceid/{resourceid}/mode/{mode}/start
 * 返回数据结构
 * {
  "cname": "录制的频道名。",
  "uid": "字符串内容为云端录制服务在 RTC 频道内使用的 UID,用于标识频道内的录制服务。",
  "resourceId": "云端录制资源 Resource ID。使用这个 Resource ID 可以开始一段云端录制。这个 Resource ID 的有效期为 5 分钟,超时需要重新请求。",
  "sid": "录制 ID。成功开始云端录制后,你会得到一个 Sid (录制 ID)。该 ID 是一次录制周期的唯一标识。"
}
 */
export async function startCloudRecording(
  appId: string, // 应用 ID
  resourceId: string, //通过 acquireCloudRecording 请求获取到的 Resource ID
  mode: string,  //录制模式:mix 合流录制
  sn: string,  // 设备编码
  ymd: string, // yyyyMMDD 如20230705
  hms: string, // 时分秒 如 102133
  channel: string, //录制服务所在频道的名称。需要和你在 acquireCloudRecording 请求中输入的 channel 相同
  token: string,  //用于鉴权的动态密钥(Token)。如果你的项目已启用 App 证书,则务必在该字段中传入你项目的动态密钥
  uid: number = 0,  //字符串内容为录制服务在 RTC 频道内使用的 UID,用于标识该录制服务,需要和你在 acquireCloudRecording 请求中输入的 uid 相同
): Promise<any> {
  const res = await fetch(
    `${AGORA_RECORDING_API}/${appId}/cloud_recording/resourceid/${resourceId}/mode/${mode}/start`,
    {
      method: 'POST',
      headers: {
        'Authorization': getAgoraBasicAuth(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        cname: channel,
        uid: String(uid),
        clientRequest: {
          token:token,
          recordingConfig: {
            channelType: 1,
            streamTypes: 2,
            audioProfile: 2,
            videoStreamType: 0,
            maxIdleTime: 30,
            transcodingConfig: {
              width: 640,
              height: 360,
              fps: 15,
              bitrate: 500,
              mixedVideoLayout: 0,
              backgroundColor: '#000000'
            },
            subscribeVideoUids: ['#allstream#'],
            subscribeAudioUids: ['#allstream#']
          },
          recordingFileConfig: {avFileType: ['hls', 'mp4']},
          storageConfig: { //需要配置录制到某个OSS云存储,这里以腾讯COS为例
            vendor: 3,  // 3: 腾讯云
            region: 3,  // 3: 广州
            bucket: '存储桶名称',
            accessKey: 'COS访问key',
            secretKey: 'COS访问密钥',
            fileNamePrefix: ['robot','OFFICE',sn,'SERVICE',ymd,hms],
          },
        },
      }),
    },
  );
  return res.json();
}

/**
 * 声网云端录制 - 停止录制
 * 开始录制后,你可以调用 stop 方法离开频道,停止录制。录制停止后如需再次录制,必须再调用 acquire 方法请求一个新的 Resource ID
 * POST https://api.sd-rtn.com/v1/apps/{appid}/cloud_recording/resourceid/{resourceid}/sid/{sid}/mode/{mode}/stop
 * 返回数据结构
 * 
{
    "cname": "device-00412345_channel",
    "resourceId": "QZW-Qc9EfWbTtlx7e7xq5bsPxh7gH7sevqyWDjF59OIuNTkVccEUq9w7YLfSUqsIpP0uf9H5QTpbuwFIJlJL9A2FbDol42ne3o-8g__pUGzkd3siZ9jLR34DfGMhdN0BOpHrYHIddjXej-uc3QrDK0H4VJgeVef4J4-HTrBKVzLA83K9T73nuWfY3hBDTyeCmzfbyq5xy7qm-U5fF-l_AE8V6PzB0W61f_GDchNr34ZJeOjXdeiQDXFesFGZyljyfM4-tDoMSwKc4wi7VaRD6Q",
    "serverResponse": {
        "fileList": [
            {
                "fileName": "robot/OFFICE/device-004/SERVICE/20260520/164833/175973946d407df68985a59a7c1ae3fc_device-00412345_channel.m3u8",
                "isPlayable": true,
                "mixedAllUser": true,
                "sliceStartTime": 1779266914981,
                "trackType": "audio_and_video",
                "uid": "0"
            },
            {
                "fileName": "robot/OFFICE/device-004/SERVICE/20260520/164833/175973946d407df68985a59a7c1ae3fc_device-00412345_channel_0.mp4",
                "isPlayable": true,
                "mixedAllUser": true,
                "sliceStartTime": 1779266914981,
                "trackType": "audio_and_video",
                "uid": "0"
            }
        ],
        "fileListMode": "json",
        "uploadingStatus": "uploaded"
    },
    "sid": "175973946d407df68985a59a7c1ae3fc",
    "uid": "18"
}

 */
export async function stopCloudRecording(
  appId: string,   // 应用 ID
  resourceId: string, //通过 acquireCloudRecording 请求获取到的 Resource ID
  sid: string,   //通过 startCloudRecording 获取的录制 ID
  mode: string,  //录制模式:mix 合流录制
  channel: string, //录制服务所在频道的名称。需要和你在 acquireCloudRecording 请求中输入的 channel 相同
  uid: string  //字符串内容为录制服务在 RTC 频道内使用的 UID,用于标识该录制服务,需要和你在 acquireCloudRecording 请求中输入的 uid 相同
): Promise<any> {
  const res = await fetch(
    `${AGORA_RECORDING_API}/${appId}/cloud_recording/resourceid/${resourceId}/sid/${sid}/mode/${mode}/stop`,
    {
      method: 'POST',
      headers: {
        'Authorization': getAgoraBasicAuth(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        cname: channel,
        uid: uid,
        clientRequest: {async_stop: false}
      }),
    },
  );
  return res.json();
}
云录制java sdk (后端来录制)
xml 复制代码
<dependencies>
    <dependency>
        <groupId>io.agora</groupId>
        <artifactId>agora-rest-client-core</artifactId>
        <version>0.5.0</version>
    </dependency>
</dependencies>

云录制java代码

java 复制代码
public class Main {
    private static String appId = "<your appId>";

    private static String cname = "<your cname>";
    // 录制服务加入频道的用户 ID
    private static String uid = "<your uid>";
    // 客户 ID
    private static String username = "<the username of basic auth credential>";
    // 客户密钥
    private static String password = "<the password of basic auth credential>";

    private static String token = "<your token>";

    private static String accessKey = "<your accessKey>";

    private static String secretKey = "<your secretKey>";

    private static Integer region = 1; // <your region>

    private static String bucket = "<your bucket>";

    private static Integer vendor = 2; // <your vendor>

    public static void main(String[] args) throws Exception {

        Credential credential = new BasicAuthCredential(username, password);

        // Initialize CloudRecordingConfig
        CloudRecordingConfig config = CloudRecordingConfig.builder()
                .appId(appId)
                .credential(credential)
                // Specify the region where the server is located.
                // Optional values are CN, US, EU, AP, and the client will automatically
                // switch to use the best domain name according to the configured region
                .domainArea(DomainArea.CN)
                .build();

        // Initialize CloudRecordingClient
        CloudRecordingClient cloudRecordingClient = CloudRecordingClient.create(config);

        AcquireResourceRes acquireResourceRes;

        // Acquire resource
        try {
            acquireResourceRes = cloudRecordingClient
                    .mixScenario()
                    .acquire(cname, uid, AcquireMixRecordingResourceClientReq.builder().build())
                    .block();
        } catch (AgoraException e) {
            System.out.printf("agora error:%s", e.getMessage());
            return;
        } catch (Exception e) {
            System.out.printf("unknown error:%s", e.getMessage());
            return;
        }

        // Check if the response is null
        if (acquireResourceRes == null || acquireResourceRes.getResourceId() == null) {
            System.out.println("failed to get resource");
            return;
        }

        System.out.printf("resourceId:%s", acquireResourceRes.getResourceId());

        System.out.println("acquire resource success");

        // Define storage config
        StartResourceReq.StorageConfig storageConfig = StartResourceReq.StorageConfig.builder()
                .accessKey(accessKey)
                .secretKey(secretKey)
                .bucket(bucket)
                .vendor(vendor)
                .region(region)
                .build();

        // Define start resource request
        StartMixRecordingResourceClientReq startResourceReq = StartMixRecordingResourceClientReq.builder()
                .token(token)
                .recordingConfig(StartResourceReq.RecordingConfig.builder()
                        .channelType(1)
                        .build())
                .recordingFileConfig(StartResourceReq.RecordingFileConfig.builder()
                        .avFileType(Arrays.asList("hls", "mp4"))
                        .build())
                .storageConfig(storageConfig)
                .build();


        StartResourceRes startResourceRes;

        // Start resource
        try {
            startResourceRes = cloudRecordingClient
                    .mixScenario()
                    .start(cname, uid, acquireResourceRes.getResourceId(), startResourceReq)
                    .block();

        } catch (AgoraException e) {
            System.out.printf("agora error:%s", e.getMessage());
            return;
        } catch (Exception e) {
            System.out.printf("unknown error:%s", e.getMessage());
            return;
        }

        // Check if the response is null
        if (startResourceRes == null || startResourceRes.getSid() == null) {
            System.out.println("failed to start resource");
            return;
        }

        System.out.printf("sid:%s", startResourceRes.getSid());

        System.out.println("start resource success");

        Thread.sleep(3000);

        QueryMixHLSAndMP4RecordingResourceRes queryResourceRes;

        // Query resource
        try {
            queryResourceRes = cloudRecordingClient
                    .mixScenario()
                    .queryHLSAndMP4(startResourceRes.getResourceId(), startResourceRes.getSid())
                    .block();

        } catch (AgoraException e) {
            System.out.printf("agora error:%s", e.getMessage());
            return;
        } catch (Exception e) {
            System.out.printf("unknown error:%s", e.getMessage());
            return;
        }

        if (queryResourceRes == null || queryResourceRes.getServerResponse() == null) {
            System.out.println("failed to query resource");
            return;
        }

        System.out.println("query resource success");

        Thread.sleep(3000);

        StopResourceRes stopResourceRes;

        // Stop resource
        try {
            stopResourceRes = cloudRecordingClient
                    .mixScenario()
                    .stop(cname, uid, startResourceRes.getResourceId(), startResourceRes.getSid(), true)
                    .block();
        } catch (AgoraException e) {
            System.out.printf("agora error:%s", e.getMessage());
            return;
        } catch (Exception e) {
            System.out.printf("unknown error:%s", e.getMessage());
            return;
        }

        // Check if the response is null
        if (stopResourceRes == null || stopResourceRes.getSid() == null) {
            System.out.println("failed to stop resource");
        } else {
            System.out.println("stop resource success");
        }
    }
}
相关推荐
daols881 小时前
vxe-table 进阶:同时使用 formatter 与 cell-render 实现格式化与样式定制
前端·javascript·vue.js·vxe-table
前端张三1 小时前
ant design vue table 使用虚拟滚动
前端·javascript·vue.js
范什么特西2 小时前
狂神Vue
前端·javascript·vue.js
一 乐2 小时前
在线考试|基于Springboot的在线考试管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·毕设·在线考试管理系统
liyunlong-java2 小时前
Android 跳转系统相册选取图片/视频/音频/文档(适配全版本权限)
android·gitee·音视频
ACP广源盛139246256732 小时前
GSV2231@ACP#三屏扩展旗舰芯片,TRAE SOLO 多任务并行开发核心引擎
运维·网络·人工智能·嵌入式硬件·gpt·电脑·音视频
硅谷秋水3 小时前
τ0-WM:用于机器人操纵的统一视频-动作世界模型
人工智能·机器学习·计算机视觉·语言模型·机器人·音视频
喵了几个咪3 小时前
Headless 后端实践:基于Go的企业级多栈管理系统脚手架
开发语言·vue.js·后端·golang·reactjs·gowind
bestlanzi16 小时前
使用nvm管理node环境
前端·vue.js·npm