Skip to content
此页目录
本文总阅读量

融云音视频会议与屏幕共享 Rtc 客户端

初始化 Rtc 客户端

js
import { installer, RCRTCCode } from '@rongcloud/plugin-rtc'
methods: {
  data(){
    return{
      rtcClient: null,// rtc客户端
    }
  },
  start(){
    /* 
      im 客户端操作...
     */
    this.rtcInit(); // 初始化
    if (!this.rtcClient)
      return this.$message.error("rtc初始化失败,请刷新重试!"), false;
  },
  // 初始化 RCRTCClient,初始化过程推荐放在建立连接之前
  rtcInit() {
    this.loadingText = '正在连接房间';
    this.rtcClient = this.im.install(installer);
  },
}

加入房间

js
import { installer, RCRTCCode } from '@rongcloud/plugin-rtc'
import {getUserInfoYJZH} from '@/api/index'; // '/user/info.json'
import qs from 'qs' // 引入qs模块,用来序列化post类型的数据

methods: {
  data(){
    remoteTracks: null, // 当前已发布至房间内的远端资源列表
    userIds: [], // 当前已加入房间的远端人员列表
    room: null, // 房间room实例
  },
  start(){
    const code = await this.joinRoom(); // 加入房间
    if (code) return ( this.$message.error("加入房间失败,请刷新后重试,错误代码:" + code),false );
  },
  // 加入普通音视频房间,从 5.0.7 开始增加返回 `tracks` 与 `userIds`
  async joinRoom() {
    // * userIds - 当前已加入房间的远端人员列表
    // * tracks  - 当前已发布至房间内的远端资源列表
    this.loadingText = '正在加入房间';
    const { code, room, userIds, tracks: remoteTracks } = await this.rtcClient.joinRTCRoom(this.form.roomId)

    // 若加入失败,则 room、userIds、tracks 值为 undefined
    if (code !== RCRTCCode.SUCCESS) {
      console.log('加入房间失败,错误代码:', code)
      return Promise.resolve(code)
    }

    this.room = room;

    /* 加入房间后返回房间内已存在的用户,获取用户信息 */
    userIds.forEach(async(userId) => {
      let userInfo = await this.getUserInfo(userId)
      // ...
    })
    this.remoteTracks = remoteTracks;
    return Promise.resolve(null); // 成功

  },
  // 获取用户信息
  async getUserInfo(userId){
    let {data:{data} } = await getUserInfoYJZH({
      headers: headers({
        appSecret: this.appSecret,
        appKey: this.appKey,
      }),
      data: qs.stringify({ userId })
    })
    let {code,...rest} = data;
    if(code === 200) return Promise.resolve(rest);
  },
}

注册房间监听

  • html 模板参考,视频播放需要标签,设置 track.getTrackId 返回的 id 绑定是谁共享的屏幕,音频则不需要,js 创建 audio 播放即可
  • 注意:浏览器限制,无法自动播放时开启声音,只有用户经常使用这个网站才能自动播放视频时开启声音。
html
<div class="video-content">
  <template v-for="track in localTracks">
    <div
      class="video-grid"
      :key="track.getTrackId()"
      :style="maxWidthAndHeight"
    >
      <video
        v-if="track.isVideoTrack()"
        class="video"
        :id="'rc-video-' + track.getTrackId()"
        :ref="'rc-video-' + track.getTrackId()"
        muted
        autoplay
      ></video>
    </div>
  </template>
  <template v-for="track in localVdeio">
    <div
      class="video-grid"
      :key="track.getTrackId()"
      :style="maxWidthAndHeight"
    >
      <video
        v-if="track.isVideoTrack()"
        class="video"
        :id="'rc-video-' + track.getTrackId()"
        :ref="'rc-video-' + track.getTrackId()"
        muted
        autoplay
      ></video>
    </div>
  </template>
  <template v-for="track in subscriptionTracks">
    <div
      class="video-grid"
      :style="maxWidthAndHeight"
      :key="track.getTrackId()"
    >
      <video
        v-if="track.isVideoTrack()"
        class="video"
        :id="'rc-video-' + track.getTrackId()"
        :ref="'rc-video-' + track.getTrackId()"
        muted
        autoplay
      ></video>
    </div>
  </template>
</div>
<div v-if="this.rtcClient && this.remoteTracks" class="btn-box">
  <div>
    <el-button
      type="primary"
      @click="
        !publishScreenTrack
          ? publishScreenShare()
          : unPublish('publishScreenTrack', publishScreenTrack)
      "
      class="bgc"
    >
      {{ !publishScreenTrack ? "共享屏幕" : "停止共享" }}
    </el-button>

    <el-button
      class="bgc"
      type="primary"
      @click="
        !publishMicrophoneAudioTrack
          ? publishMicrophoneAudio()
          : unPublish(
              'publishMicrophoneAudioTrack',
              publishMicrophoneAudioTrack
            )
      "
    >
      {{ !publishMicrophoneAudioTrack ? "打开麦克风" : "关闭麦克风" }}
    </el-button>

    <el-button
      class="bgc"
      type="primary"
      @click="
        !publishCameraVideoTrack
          ? publishCameraVideo()
          : unPublish('publishCameraVideoTrack', publishCameraVideoTrack)
      "
    >
      {{ !publishCameraVideoTrack ? "打开摄像头" : "关闭摄像头" }}
    </el-button>
  </div>
  <div>
    <el-button
      class="bgc"
      type="primary"
      @click.native="showUserIds = !showUserIds"
      id="userIds"
    >
      人员列表
    </el-button>
    <el-button class="bgc" type="primary" @click.native="exit">
      退出
    </el-button>
  </div>
</div>
js
data(){
  return{
    subscriptionTracks: [], // 订阅的视频流
    localTracks: [], // 本地资源 共享屏幕
    localVdeio: [],  // 本地资源 视频摄像头
    publishScreenTrack: null, // 当前共享屏幕的资源
    publishMicrophoneAudioTrack: null, // 当前已发布麦克风资源
    publishCameraVideoTrack: null, // 当前已发布的摄像头资源
  }
},
methods:{
  start(){
    this.roomWatch(); // 房间监听
  },
  // 注册房间事件监听器,重复注册时,仅最后一次注册有效
  roomWatch() {
    this.loadingText = '加入成功,建立房间监听!';
    this.room.registerRoomEventListener({
      /**
       * 本端被踢出房间时触发
       * @description 被踢出房间可能是由于服务端超出一定时间未能收到 rtcPing 消息,所以认为己方离线。
       * 另一种可能是己方 rtcPing 失败次数超出上限,故而主动断线
       * @param byServer
       * 当值为 false 时,说明本端 rtcPing 超时
       * 当值为 true 时,说明本端收到被踢出房间通知
       */
      onKickOff(byServer) {
        if (byServer) {
          console.log('被踢出房间');
          this.exit();
        } else {
          console.log('房间连接超时');
        }
      },
      /**
       * 接收到房间信令时回调,用户可通过房间实例的 `sendMessage(name, content)` 接口发送信令
       * @param name 信令名 string
       * @param content 信令内容 any
       * @param senderUserId 发送者 Id string
       * @param messageUId 消息唯一标识 string
       */
      onMessageReceive(name, content, senderUserId, messageUId) {
      },
      /**
       * 监听房间属性变更通知
       * @param name string
       * @param content string
       */
      onRoomAttributeChange(name, content) {
        console.log(name, content, '监听房间属性变更通知');
      },
      /**
       * 发布者禁用/启用音频
       * @param audioTrack RCRemoteAudioTrack 类实例
       */
      onAudioMuteChange(audioTrack) {
        console.log(audioTrack, '发布者禁用/启用音频');
      },
      /**
       * 发布者禁用/启用视频
       * @param videoTrack RCRemoteVideoTrack 类实例对象
       */
      onVideoMuteChange(videoTrack) {
        console.log(videoTrack, '发布者禁用/启用视频');
      },
      /**
       * 房间内其他用户新发布资源时触发
       * 如需获取加入房间之前房间内某个用户发布的资源列表,可使用 room.getRemoteTracksByUserId('userId') 获取
       * @param tracks 新发布的音轨与视轨数据列表,包含新发布的 RCRemoteAudioTrack 与 RCRemoteVideoTrack 实例
       */
        onTrackPublish: async (tracks) => {
        // 按业务需求选择需要订阅资源,通过 room.subscribe 接口进行订阅
        console.log(tracks, 'tracks');
        if (!tracks?.length) return;
        const { code } = await this.room.subscribe(tracks)
        if (code !== RCRTCCode.SUCCESS) {
          console.log('资源订阅失败 ->', code)
        }
        console.log('资源订阅', code);
      },
      /**
       * 房间用户取消发布资源
       * @param tracks 被取消发布的音轨与视轨数据列表
       * @description 当资源被取消发布时,SDK 内部会取消对相关资源的订阅,业务层仅需处理 UI 业务
       */
      onTrackUnpublish: ([{_id:id}]) => {
        this.subscriptionTracks.forEach(({_id},index) => {

          console.log(id,_id);
          if(id === _id)
          this.subscriptionTracks.splice(index,1);
        })

        console.log('房间用户取消发布资源', id);
      },
      /**
       * 订阅的音视频流通道已建立, track 已可以进行播放
       * @param track RCRemoteTrack 类实例
       */
      onTrackReady: async (track) => {
        console.log(track, '订阅的音视频流通道已建立,track 已可以进行播放');
        // this.isSubscriptionTrack = true;
        await this.$nextTick();
        console.log(track, '已可以进行播放');
        if (track.isAudioTrack()) {
          // 音轨不需要传递播放控件
          track.play()
        } else {

          // 视轨需要一个 video 标签才可进行播放
          this.subscriptionTracks.push(track);
          await this.$nextTick();

          track.play(this.$refs['rc-video-' + track.getTrackId()][0])
        }
      },
      /**
       * 人员加入
       * @param userIds 加入的人员 id 列表
       */
      onUserJoin: async ([userId]) => {
        console.log('人员加入', userId);
      },
      /**
       * 人员退出
       * @param userIds
       */
      onUserLeave: ([userIds]) => {
        console.log('人员退出', userIds);
        console.log(this.subscriptionTracks);
      }
    })

    /**
     * 添加音量变化通知
     * @param handler 音量变化通知执行事件
     * @param gap 时间间隔,有效值为 300ms-1000ms,默认为 1000ms
     */
    this.room.onAudioLevelChange((audioLevelReportList) => {
      /* audioLevelReportList : { track: RCLocalAudioTrack | RCRemoteAudioTrack, audioLevel: number }[] */
      // console.log('添加音量变化通知', audioLevelReportList);

    }, 1000)
    this.loadingText = '完成';
    this.loading = false;
    console.log(' 监听完成');
  },
}

播放房间内已存在的音视频

remoteTracks:加入房间时返回的数组

js
methods:{
  // 订阅 加入房间后,房间内可能已经存在其他参会者发布的音视轨数据
  async subscribe() {
    if (!this.remoteTracks.length) return;
    const { code } = await this.room.subscribe(this.remoteTracks)
  
    if (code !== RCRTCCode.SUCCESS) {
      console.log(`资源订阅失败 -> code: ${code}`)
    }
    console.log('订阅成功');
  },
}

共享屏幕

js
// data 数据:publishScreenTrack: null, // 当前共享屏幕的资源
// 发布屏幕共享资源
async publishScreenShare() {
  if (!this.rtcClient || !this.room) {
    console.log('请确保已经初始化完 RTC,并已加入房间');
    return;
  }

  // 获取屏幕资源
  const { code, track } = await this.rtcClient.createScreenVideoTrack();
  if (code !== RCRTCCode.SUCCESS) {
    console.log(`获取资源失败: ${code}`);
    return this.$message.error(`获取资源失败: ${code}`);
  }

  // 发布(推流)
  const pubRes = await this.publishScreen([track]);
  if (pubRes !== RCRTCCode.SUCCESS) {
    console.log(`发布资源失败: ${code}`);
    return;
  }
  console.log('发布', track);
  this.publishScreenTrack = track;
  /* 当停止推流时触发方法 */
  track._msTrack.onended = () => {
    this.unPublish('publishScreenTrack',this.publishScreenTrack)
    console.log('本地推流停止');
  },
  // 发布屏幕共享
  async publishScreen(localTracks) {
    if (!localTracks.length) {
      return;
    }
    const { code } = await this.room.publish(localTracks);

    if (code === RCRTCCode.SUCCESS) {
      /**
       * 播放资源
       */
      this.localTracks = localTracks;
      // this.appendVideoEl(localTracks);

      console.log(localTracks, 'localTracks本地资源');
      await this.$nextTick();

      localTracks.forEach((track) => {
        this.playTrack(track, false);
      });

      return Promise.resolve(code);
    } else {
      console.log(`发布资源失败: ${code}`);
    }
  },
  /**
   * 播放资源
   * @param {window.RCRTC.RCTrack} track 音轨、视轨
   * @param {boolean} playAudio 是否播放音频,(本端发布的音频静音,值为 false)
   * @returns 
   */
  async playTrack(track, playAudio) {
    // 播放视频
    if (track.isVideoTrack()) {
      /* 传入 dom 元素 */
      track.play(this.$refs['rc-video-' + track.getTrackId()][0]);
      return;
    }
    // 播放音频
    if (playAudio) {
      track.play();
    }
  },
},

发布共享摄像头与麦克风

js
/**
 * 播放资源
 * @param {window.RCRTC.RCTrack} track 音轨、视轨
 * @param {boolean} playAudio 是否播放音频,(本端发布的音频静音,值为 false)
 * @returns 
 */
async playTrack(track, playAudio) {
  // 播放视频
  if (track.isVideoTrack()) {
    track.play(this.$refs['rc-video-' + track.getTrackId()][0]);
    return;
  }
  // 播放音频
  if (playAudio) {
    track.play();
  }
},
// 发布麦克风
async publishMicrophoneAudio() {
  // 仅当 `code === RCRTCCode.SUCCESS` 时 audioTrack 有值
  // audioTrack 为 RCMicphoneAudioTrack 类型实例
  const { code, track } = await this.rtcClient.createMicrophoneAudioTrack()
  if (code !== RCRTCCode.SUCCESS) return this.$message.error(`获取麦克风失败: ${code}`)

  this.publishAudioOrVideo('publishMicrophoneAudioTrack',track);

},
// 发布摄像头
async publishCameraVideo() {
  // 仅当 `code === RCRTCCode.SUCCESS` 时 videoTrack 有值
  // videoTrack 为 RCCameraVideoTrack 类型实例
  // const { code, track } = await this.rtcClient.createCameraVideoTrack();
  this.rtcClient.createCameraVideoTrack().then(({ code, track }) => {
    if (code !== RCRTCCode.SUCCESS) return this.$message.error(`获取摄像头失败: ${code}`)

    this.publishAudioOrVideo('publishCameraVideoTrack',track);
  })

},
// 发布摄像头或麦克风
async publishAudioOrVideo(proptype,track) {

  const { code } = await this.room.publish([track])
  // 若资源发布失败
  if (code !== RCRTCCode.SUCCESS) return this.$message.error(`资源发布失败: ${code}`);

  this[proptype] = track;
  // 播放视频
  if (track.isVideoTrack()) {
    this.localVdeio = [track];

    await this.$nextTick();
    this.playTrack(track, false);
  }

  // if (code === RCRTCCode.SUCCESS) {
  //   /**
  //    * 播放资源
  //    */
  //   this.localTracks = localTracks;

  //   console.log(localTracks, 'localTracks本地资源');

  //   localTracks.forEach((track) => {
  //     this.playTrack(track, false);
  //   });

  //   return Promise.resolve(code);
  // } else {
  //   console.log(`发布资源失败: ${code}`);
  // }
},

停止共享资源

js
// 停止发布本地资源
async unPublish(proptype,track) {
  /* track 为要停止的音视频轨道 */
  const { code } = await this.room.unpublish([track])

  if (code !== RCRTCCode.SUCCESS) {
    console.log('取消发布失败:', code)
  }
  track.destroy();

  /* 以下为销毁本地正在播放的音视频,参考 */
  this[proptype] = null;

  if(proptype === 'publishScreenTrack'){ //屏幕
    this.localTracks = [];
  }
  if(proptype === 'publishCameraVideoTrack'){ // 摄像头
    this.localVdeio = [];
  }
},

退出房间

unPublish:停止发布本地资源

js
// 退出房间
async exit(){
  this.loading = true;
  this.loadingText = '正在退出';
  const { code } = await this.rtcClient.leaveRoom(this.form.roomId);
  if (code !== RCRTCCode.SUCCESS) return this.$message.error('退出失败!')

  this.$message.success('操作成功!');

  // this.form.name = ''; // 房间名
  this.form.roomId = ''; // 房间id
  this.subscriptionTracks = []; // 订阅视频流
  if(this.publishScreenTrack) {
    this.loadingText = '正在关闭屏幕共享';
    this.unPublish('publishScreenTrack', this.publishScreenTrack); // 停止屏幕共享
  }
  if(this.publishMicrophoneAudioTrack) {
    this.loadingText = '正在关闭麦克风';
    this.unPublish('publishMicrophoneAudioTrack', this.publishMicrophoneAudioTrack); // 停止音频推流
  }
  if(this.publishCameraVideoTrack) {
    this.loadingText = '正在关闭摄像头';
    this.unPublish('publishCameraVideoTrack', this.publishCameraVideoTrack); // 停止摄像头
  }
  this.loading = false;
  this.showForm = true;
},

踢人

js
// 踢出别人
async kick(userId){
  let d = await kickYJZH({
    headers: headers({
      appSecret: this.appSecret,
      appKey: this.appKey,
    }),
    data: {
      roomId: this.form.roomId,
      userId,
    },
  })
},

评论

交流群