前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室

TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室

原创
作者头像
黑眼圈云豆
修改2020-06-24 17:52:13
3.8K3
修改2020-06-24 17:52:13
举报

根据上回学习了官方TRTC demo之后,已经了解了一个基础的多人会议室创建的流程,接下来我需要将自己学到的转换为自己能够运用的。

项目介绍

这个项目是使用的框架是vue2 + typescript,项目搭建工具使用的是vue-cli。vue-cli搭建项目比webpack方便快捷,还是比较推荐大家使用的。

TRTC是的web sdk是集成了npm的,也有ts版本,在项目中可以使用下面的命令进行安装部署

代码语言:javascript
复制
npm install trtc-js-sdk --save-dev

实现的功能有开闭音视频、监听音量、会议室登录登出等基础功能。

先给大家看看我实现的效果图。

登录页
登录页
会议室
会议室

功能是demo基本上差不多,项目ui使用的是element-ui,也是现在用的比较多的UI框架了,想必大家并不陌生。

项目基础目录
项目基础目录

view目录是我新建的目录,主要是用来存放一些视图展示的vue文件。

会议室主体代码
会议室主体代码

1.Client类是一个基础类,主要用于处理一些客户端初始化的方法(不过由于本人设计思想还不到位,这个类其实包含的逻辑就是RTCClient所需要的处理的逻辑);

2.RTCClient和ShareClient这两个类分别是处理音视频通话客户端和屏幕共享客户端的逻辑;

3.Generate类是用来加密生成用户签名的类,处理加密逻辑(在开发过程中,加密逻辑是放在服务器端,一定要注意!我这里为了方便,就放在本地处理了);

4.video-stream-item主要是用来处理单个音视频流的显示和样式逻辑,流的事件监听和播放也是放在这个组件里边;

5.video-stream-list主要功能是管理批量流的组件,控制多个视频的排列逻辑等;

6.trtc-room会议室客户端页面代码。

项目代码分析

Client类

代码语言:javascript
复制
import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';
import Generate from './Generate'

/**
 * trtc音视频客户端base类
 */
export default class Client {
    userId: string;  //用户id
    userSig: string;  //用户签名
    roomId: string;  //会议室id
    isPublished: boolean;  //是否已经发布过流
    isJoined: boolean;  //是否已经加入过房间
    localStream: any;  //本地流
    client: any;  //客户端
    cameraId: string; //摄像头id
    microphoneId: string;  //麦克风id
    
    /**
     * 用本我思考的是在实例化客户端的时候就把流的视图dom绑定好,但是后面发现这样并不灵活
     * 我把流的视图dom绑定放在了video-stream-item里面
     */
    // DOMContainer: HTMLDivElement | string;
    
    remoteStreams: Map<String, any>;  //远端流的map

    constructor(options: {
        userId: string, 
        roomId: string, 
        cameraId: string, 
        microphoneId: string, 
        // DOMContainer: string
    }) {
        this.userId = options.userId;
        this.roomId = options.roomId;

        this.cameraId = options.cameraId || '';
        this.microphoneId = options.microphoneId || '';
        // this.DOMContainer = options.DOMContainer;

        const generate = new Generate(this.userId);
        this.userSig = generate.getUserSig();

        // console.warn(this.userSig)

        this.isPublished = false;
        this.isJoined = false;
        this.localStream = null;
        this.remoteStreams = new Map();

        this.createClient();
    }

    createClient() {
        /**
         * 这里先暂时不实现切换视频设备和麦克风的逻辑,
         * 因为切换逻辑一般是在初始化之后再做
         */
        let params = {
            mode: <any>TRTCMode.VIDEOCALL,
            sdkAppId: AppConfig.SDKAPPID,
            userId: this.userId,
            userSig: this.userSig,
            // cameraId: this.cameraId,
            // microphoneId: this.microphoneId,
        }
        // console.warn(params)
        this.client = TRTC.createClient(params)
    }

    /**
     * 绑定监听事件
     * @param error 客户端报错
     * @param clientBanned 客户端被踢出房间
     * @param peerJoin 客户端加入房间
     * @param peerLeave 客户端离开房间
     * @param streamAdded 远程流加入房间,返回boolean,代表要不要订阅
     * @param streamSubscribed 订阅远程流
     * @param streamRemoved 移除远程流
     * @param streamUpdated 远程流更新
     */
    handleEvents({ 
        error = _ => {},
        clientBanned = _ => {},
        peerJoin = _ => {},
        peerLeave = _ => {},
        streamAdded = _ => { return true },
        streamSubscribed = _ => {},
        streamRemoved = _ => {},
        streamUpdated = _ => {},
        unmuteVideo = _ => {},
        muteVideo = _ => {},
        unmuteAudio = _ => {},
        muteAudio = _ => {},
    }) {
        this.client.on('error', (err: any) => {
            console.error(err);
            error(err);
            location.reload();
        })

        this.client.on('client-banned', (err: any) => {
            alert('您被提出了房间');
            clientBanned(err);
            location.reload();
        })

        this.client.on('peer-join', (e: any) => {
            peerJoin(e);
            console.log(`用户${e.userId}加入了房间`);
        })

        this.client.on('peer-leave', (e: any) => {
            peerLeave(e);
            console.log(`用户${e.userId}离开了房间`);
        })

        this.client.on('stream-added', (e: any) => {
            if (!streamAdded(e)) {
                return;
            }
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();

            console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})接入`);
            
            //订阅过的远端流不再订阅
            if (this.remoteStreams.has(userId)) return;

            this.client.subscribe(remoteStream);
        })

        this.client.on('stream-subscribed', (e: any) => {
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();

            console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})订阅成功`);
            
            
            this.remoteStreams.set(userId, remoteStream);
            
            streamSubscribed(e);

            /**
             * 订阅成功之后,不再直接进行play和监听状态操作
             */
            // remoteStream.on('player-state-change', e => {
            //     console.log(`用户${userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
            // });
            // remoteStream.play(streamSubscribed(e) || id, { objectFit: 'contain' });
        })

        this.client.on('stream-removed', (e: any) => {
            const remoteStream = e.stream;
            const userId = remoteStream.getUserId();
            const id = remoteStream.getId();

            if (this.remoteStreams.has(userId)) this.remoteStreams.delete(userId);

            /**
             * 移除操作我们还是可以直接进行的
             */
            remoteStream.stop();
            streamRemoved(e);
            console.log(`stream-removed ID: ${id}  type: ${remoteStream.getType()}`);
        })

        this.client.on('stream-updated', (e: any) => {
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();
            
            console.log('远端流状态更新')
            // this.remoteStreams.set(userId, remoteStream);

            streamUpdated(e);
        })
        
        //有远端流禁用了音频
        this.client.on('mute-audio', (e: any) => {
            console.log('mute-audio', e)
            muteAudio(e)
        })
        
        //有远端流开启了音频
        this.client.on('unmute-audio', (e: any) => {
            console.log('unmute-audio', e)
            unmuteAudio(e)
        })
        
        //有远端流禁用了视频
        this.client.on('mute-video', (e: any) => {
            console.log('mute-video', e)
            muteVideo(e)
        })
        
        //有远端流开启了视频
        this.client.on('unmute-video', (e: any) => {
            console.log('unmute-video', e)
            unmuteVideo(e)
        })
    }

    /**
     * 初始化房间
     */
    async initRoom() {
        if (this.isJoined) {
            console.log('客户端已经加入过房间');
            return;
        }

        try {
            await this.client.join({
                roomId: this.roomId
            });
            console.log('成功加入聊天室');
            this.isJoined = true;
            
            await this.initStream(); 
        } catch(err) {
            console.log('加入房间失败', err)
        }
    }

    getLocalStream() {
        return this.localStream;
    }
    
    /**
     * 初始化流
     */
    async initStream() {
        this.localStream = TRTC.createStream({
            audio: true,
            video: true,
            // cameraId: this.cameraId,
            // microphoneId: this.microphoneId,
            mirror: true
        })

        try {
            await this.localStream.initialize();
            console.log('本地流初始化成功');

            this.localStream.on('player-state-change', e => {
                console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
            })

            await this.publishStream();
            
            // this.localStream.play(this.DOMContainer);

        } catch (err) {
            console.error('初始化本地流错误', err);
        }
    }

    async publishStream() {
        if (!this.isJoined) {
            console.warn('本地客户端还没有加入任何聊天室');
            return
        }

        if (this.isPublished) {
            console.warn('本地客户端已经发布过音视频流了');
            return
        }
        try {
            await this.client.publish(this.localStream);
            console.log('本地流发布成功');
            this.isPublished = true;
        } catch (err) {
            this.isPublished = false;
            console.error('发布本地流出现错误', err);
        }
    }

    async unpublishStream() {
        if (!this.isJoined) {
            console.warn('尚未加入房间,无法实行取消流发布');
            return;
        }

        if (!this.isPublished) {
            console.warn('尚未发布任何本地流,无法取消发布');
            return;
        }

        await this.client.unpublish(this.localStream);
        this.isPublished = false;
    }
    
    async leave() {
        if (!this.isJoined) {
            console.warn('未加入房间,无法执行离开操作')
            return;
        }

        await this.unpublishStream();

        await this.client.leave();

        this.localStream.stop();
        this.localStream.close();
        this.localStream = null;
        this.isJoined = false;
    }

    /**
     * 恢复播放所有流
     * 该方法主要是解决视频无法自动播放的问题
     */
    // resumeStreams() {
    //     this.localStream.resume();
    //     for (let [userId, stream] of this.remoteStreams) {
    //     stream.resume();
    //     }
    // }

    /**
     * 禁止本地音频
     */
    muteLocalAudio() {
        return this.localStream.muteAudio();
    }

    /**
     * 打开本地音频
     */
    unmuteLocalAudio() {
        return this.localStream.unmuteAudio();
    }

    /**
     * 禁止本地视频
     */
    muteLocalVideo() {
        return this.localStream.muteVideo();
    }

    /**
     * 打开本地视频
     */
    unmuteLocalVideo() {
        return this.localStream.unmuteVideo();
    }
}

RTCClient类

代码语言:javascript
复制
import Client from './Client';

export default class RTCClient extends Client {
    constructor(options: any) {
        super(options);
    }
}

好尴尬呀,因为设计缺陷,我把逻辑都放在Client类了,还在思考能怎么拆分,有大佬有想法的,可以在评论告知我一些思路。因为我是感觉那些逻辑无论是哪个客户端都需要进行。

ShareClient类

代码语言:javascript
复制
import Client from './Client';
import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';

export class ShareClient extends Client {
    constructor(options: any) {
        super(options);
    }

    createClient() {
        this.client = TRTC.createClient({
            mode: TRTCMode.VIDEOCALL,
            sdkAppId: AppConfig.SDKAPPID,
            userId: this.userId,
            userSig: this.userSig
        })

        //于普通的客户端不同,共享屏幕的客户端不接收远端流,所以要重写创建客户端方法
        this.client.setDefaultMuteRemoteStreams(true);
    }

    /**
     * 共享屏幕的流也需要重新初始化流的方法
     */
    async initStream() {
        this.localStream = TRTC.createStream({
            audio: false,
            screen: true,
            video: true,
        })

        try {
            this.localStream.initialize();
            console.log('本地流初始化成功');

            this.localStream.on('player-state-change', e => {
                console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
            })

            await this.publishStream();
            
            //同理这里也不进行play操作
            // this.localStream.play(this.DOMContainer);

        } catch (err) {
            console.error('初始化本地流错误', err);
        }
    }
}

trtc-room/index.vue

代码语言:javascript
复制
<template>
    <el-container class="trtc-room">
        <el-header class="trtc-room_header" height="44px">
            会议室{{roomId}}
            <el-button 
                class="float-right close-btn" 
                type="primary" 
                size="small"
                @click="closeMeeting">退出会议室
            </el-button>
        </el-header>
        <el-container>
            <el-aside class="trtc-room_aside" width="240px">
                <video-stream-item
                    ref="videoStreamItem"
                    v-if="hasInit"
                    :user-id="userId" 
                    :show-user-id="false"
                    :stream='rtcClient.localStream'>
                </video-stream-item>
                <div class="header-info">
                    <p>{{userId}}</p>
                </div>
                <div class="device-control">
                    <div class="icon-container" @click="changeAudio">
                        <icon :icon="isMuteAudio ? 'audio' : 'audio-off'" size="large"></icon>
                    </div>
                    <div class="icon-container" @click="changeVideo">
                        <icon :icon="isMuteVideo ? 'video' : 'video-off'" size="large"></icon>
                    </div>
                </div>
            </el-aside>
            <el-main>
                <video-stream-list :streams="remoteStreams" ref="videoStreamList"></video-stream-list>
            </el-main>
        </el-container>
    </el-container>
</template>

<script>
    import RTCClient from './js/client/class/RTCClient'
    import VideoStreamItem from '@/components/trtc-room/components/video-stream-item'
    import VideoStreamList from '@/components/trtc-room/components/video-stream-list'
    import Icon from '@/components/icon'
    import { Component, Vue, Prop }  from 'vue-property-decorator';
    @Component({
        components: {
            RTCClient,
            VideoStreamItem,
            Icon,
            VideoStreamList
        }
    })
    export default class TrtcRoom extends Vue {
        rtcClient = null; //客户端
        hasInit = false; //是否已经完成初始化
        remoteStreams = [];  //远端流
        isMuteVideo = false;  //是否禁用视频
        isMuteAudio = false;  //是否禁用音频

        @Prop() roomId;
        @Prop() userId;

        created() {
            console.log('trtc-room created')
        }
        async mounted() {
            const options = {
                userId: this.userId,
                roomId: this.roomId,
            };

            this.rtcClient = new RTCClient(options);

            await this.init();
            this.hasInit = true;
        }

        /**
         * 切换音频状态
         */
        changeAudio() {
            const actionFunc = () => {
                return this.isMuteAudio 
                    ? this.rtcClient.unmuteLocalAudio()
                    : this.rtcClient.muteLocalAudio();
            }
                
            actionFunc()
                ? this.isMuteAudio = !this.isMuteAudio
                : this.$message({
                    type: 'warning',
                    message: '没有视频设备'
                })
            
            //调用组件修改状态
            this.$refs.videoStreamItem.changeAudio(this.isMuteAudio);
        }

        /**
         * 切换视频状态
         */
        changeVideo() {
            const actionFunc = () => {
                return this.isMuteVideo 
                    ? this.rtcClient.unmuteLocalVideo()
                    : this.rtcClient.muteLocalVideo();
            }
                
            actionFunc()
                ? this.isMuteVideo = !this.isMuteVideo
                : this.$message({
                    type: 'warning',
                    message: '没有视频设备'
                })
            
            //调用组件修改状态
            this.$refs.videoStreamItem.changeVideo(this.isMuteVideo);
        }

        /**
         * 初始化
         */
        async init() {

            //监听事件绑定
            const events = {
                error: this.error,
                clientBanned: this.clientBanned,
                peerJoin: this.peerJoin,
                peerLeave: this.peerLeave,
                streamAdded: this.streamAdded,
                streamSubscribed: this.streamSubscribed,
                streamRemoved: this.streamRemoved,
                streamUpdated: this.streamUpdated,
                unmuteAudio: this.unmuteAudio,
                muteAudio: this.muteAudio,
                unmuteVideo: this.unmuteVideo,
                muteVideo: this.muteVideo,
            }
            this.rtcClient.handleEvents(events);
            try {
                await this.rtcClient.initRoom();
            } catch (error) {
                console.error(error)
            }
        }

        /**
         * 退出会议室
         */
        async closeMeeting() {
            try {
                await this.rtcClient.leave();
                this.$router.push({
                    name: 'Login'
                })
            } catch (error) {
                console.error(error)
            }
        }

        /**
         * 更新远端流
         */
        updateRemoteStreams() {
            let streams = Array.from(this.rtcClient.remoteStreams.values());
            this.remoteStreams = streams;
        }

        error(e) {
            console.log(e)
        }
        clientBanned(e) {
            console.log(e)
        }
        peerJoin(e) {
            console.log(e)
        }
        peerLeave(e) {
            console.log(e)
        }
        streamAdded(e) { 
            console.log(e);
            return true;
        }
        streamSubscribed(e) { 
            console.log('订阅到新远端流了', e.userId)
            this.updateRemoteStreams();
        }
        streamRemoved(e) {
            console.log('有一位远端流离开了房间', e)
            this.updateRemoteStreams();
        }
        streamUpdated(e) {
            console.log(e)
        }
        muteAudio(e) {
            console.log('远端禁用音频', e)
            //调用组件修改状态
            this.$refs.videoStreamList.changeAudio(e.userId, true);
        }
        unmuteAudio(e) {
            console.log(e)
            //调用组件修改状态
            this.$refs.videoStreamList.changeAudio(e.userId, false);
        }
        muteVideo(e) {
            //调用组件修改状态
            this.$refs.videoStreamList.changeVideo(e.userId, true);
            console.log('远端禁用视频', e)
        }
        unmuteVideo(e) {
            //调用组件修改状态
            this.$refs.videoStreamList.changeVideo(e.userId, false);
            console.log(e)
        }
    }
</script>

<style lang="less" scoped>
.trtc-room_header {
    line-height: 44px;
    box-shadow: 0 5px 5px #ddd;
    margin-bottom: 10px;
}

.trtc-room_aside {
    box-shadow: 5px 0px 5px #ddd;
}

.header-info {
    text-align: center;
    height: 48px;
    line-height: 48px;
    border-bottom: 1px solid #ddd;
}

.device-control {
    display: flex;
    .icon-container {
        flex: 1 1 50%;
        text-align: center;
        padding: 15px;
        cursor: pointer;
        transition: all .3s ease 0s;
        &:hover {
            background-color: rgba(221, 221, 221, .3);
        }
        &:first-child {
            border-right: 1px solid #ddd;
        }
    }
}
.close-btn {
    margin-top: 6px;
}
</style>
<style lang="less">

</style>

video-stream-item.vue

代码语言:javascript
复制
<template>
    <div class="video-play-plane">
        <div class="video-play-view" :id="videoId"></div>
        <div class="video-play-desc">
            <span class="user-name" v-if="showUserId">{{userId}}</span>
            <icon 
                :icon="isMuteAudio ? 'audio-off' : 'audio'"
                size="normal"
                class="audio-icon">
            </icon>
            <div class="audio-level" :style="levelStyle"></div>
        </div>
    </div>
</template>

<script>
    import { Vue, Prop, Component } from 'vue-property-decorator';
    import Icon from '@/components/icon'
    
    @Component({
        components: {
            Icon
        }
    })
    export default class VideoStreamItem extends Vue {
        @Prop({
            required: true,
        }) stream

        @Prop({
            required: true
        }) userId

        @Prop({
            type: Boolean,
            default: true
        }) showUserId

        videoId = 'trtcVideo'; //单个流的id
        isMuteVideo = false;  //是否禁用了视频
        isMuteAudio = false;  //是否禁用了音频
        audioLevel = 0;  //音量等级(0 - 1)
        interval = null;  //定时器(偶用来监听声音等级变化)

        /**
         * 计算当前音量样式
         */
        get levelStyle() {
            const baseHeight = 11;
            const baseWidth = 6;
            const baseTop = 21;
            
            let realHeight = 
                !this.isMuteAudio 
                ? baseHeight * this.audioLevel
                : 0;
            
            let realTop = baseTop - realHeight;

            return {
                height: realHeight + 'px',
                width: baseWidth + 'px',
                top: realTop + 'px'
            }
        }
        
        created() {
            this.videoId = this.videoId + this.stream.getId();
        }
        mounted() {

            //监听当前流的状态
            this.stream.on('player-state-changed', e => {
                console.log(`用户${this.userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
                
                //由于浏览器自动播放策略可能会导致视频不会自动播放,需要手动吊起
                if (event.state === 'PAUSED') {
                    this.stream.resume();
                }
            });

            //执行play,流的音视频在页面上展示
            this.stream.play(this.videoId);

            //设置定时器监听音量变化
            this.interval = setInterval(() => {
                this.audioLevel = this.stream.getAudioLevel();

                //对于等于0.1就相当于有在说话了
                if (this.audioLevel >= 0.1) {
                    console.log(`user ${this.stream.getUserId()} is speaking`);
                }
            }, 200);
        }
        destroyed() {
            //清楚定时器
            clearInterval(this.interval);   
        }

        changeVideo(sign) {
            this.isMuteVideo = sign;
        }
        changeAudio(sign) {
            this.isMuteAudio = sign;
        }
    }
</script>

<style lang="less" scoped>
.video-play-plane {
    position: relative;
    width: 240px;
    height: 180px;
}

.video-play-view {
    width: 240px;
    height: 180px;
    background-color: red;
}

.video-play-desc {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 37px;
    padding: 5px;
    box-sizing: border-box;
    background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8));
    .user-name {
        color: #fff;
    }
}
.audio-icon {
    float: right;
    color: #e8ff26;
    position: absolute;
    right: 0;
    bottom: 5px;
    z-index: 1;
}
.audio-level {
    background-color: #52c352;
    position: absolute;
    right: 9.5px;
    // top: 210px;
    z-index: 0;
}
</style>

video-stream-list.vue

代码语言:javascript
复制
<template>
    <el-card class="stream-list-container">
        <header class="header-title" slot="header">会议成员</header>
        <video-stream-item 
            v-for="stream in streams" 
            :key="stream.getId()"
            :ref="stream.getUserId()"
            :user-id="stream.getUserId()"
            :stream="stream">
        </video-stream-item>
        <div class="no-data" v-if="!streams.length">暂无其他参会人员</div>
    </el-card>
</template>

<script>
    import { Vue, Component, Prop } from 'vue-property-decorator'
    import VideoStreamItem from './video-stream-item'

    @Component({
        components: {
            VideoStreamItem
        }
    })
    export default class VideoStreamList extends Vue {
        @Prop({
            required: true,
            type: Array,
            default() {
                return []
            }
        }) streams;
        
        changeVideo(userId, sign) {
            const ref = this.$refs[userId][0];
            if (ref) ref.changeVideo(sign);
        }
        
        changeAudio(userId, sign) {
            const ref = this.$refs[userId][0];
            if (ref) ref.changeAudio(sign);
        }
    }
</script>

总结

从上手程度上来看,用trtc搭建一个简单的多人聊天室速度基本上比较快,难度不高,基本上能看懂官网的文档就可以直接上手使用了。但是里面有几点要注意一下:

1.在包裹了音视频流播放标签的父标签中设置text-align: center,会导致视频偏移;

视频发生了偏移
视频发生了偏移

2.在client监听的音视频切换(mute-audio、mute-video等四个),都是远端的流的切换事件,目前我还没找到怎么监听本地流;

3.stream上绑定player-state-changed,似乎无法监听到关闭和打开音视频流,不知道是不是我写的有问题,大家可以一起试试。

下一篇文章我会基于这次的项目更新直播模式,实现多人小课堂的模式,大概功能如下:

1.屏幕共享;

3.模拟房管功能;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目介绍
  • 项目代码分析
    • Client类
      • RTCClient类
        • ShareClient类
          • trtc-room/index.vue
            • video-stream-item.vue
              • video-stream-list.vue
              • 总结
              相关产品与服务
              实时音视频
              实时音视频(Tencent RTC)基于腾讯21年来在网络与音视频技术上的深度积累,以多人音视频通话和低延时互动直播两大场景化方案,通过腾讯云服务向开发者开放,致力于帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档