前端使用liveKIt搭建音视频

3,567 阅读2分钟

前期准备

运行livekit

使用 ./livekit-server.exe --dev 运行livekit。 本地开发使用 --dev 会使用默认的 API Key 和 API Secret

1666682681623.png

获取token

获取token需要服务端配合,这里使用node。 依赖 express livekit-server-sdk

const express = require('express')
const livekitApi = require('livekit-server-sdk')
const AccessToken = livekitApi.AccessToken

const app = express()
// 跨域设置
app.all("*", function (req, res, next) {
    res.header('Access-Control-Allow-Origin','*');
    // 允许的header类型
    res.header('Access-Control-Allow-Headers','content-type');
    // 跨域允许的请求方式
    res.header('Access-Control-Allow-Methods','DELETE,PUT,POST,GET,OPTIONS');
    next();
})

app.get('/getToken', (req, res) => {
  const at = new AccessToken('devkey', 'secret', {
    identity: req.query.userName,
  })
  at.addGrant({ roomJoin: true, room: req.query.roomName })
  const token = at.toJwt()
  res.send({token})
})

app.listen(5000, (err) => {
  if (!err) console.log('服务器启动成功了!')
})

AccessToken 中的两个参数 第一个是 API Key,第二个是 API Secret。 devkey、secret为跑 ./livekit-server.exe --dev 默认值

前端实现音视频

安装依赖 livekit-client

  • live.ts
import { Room, Participant, RoomOptions, Track, RoomEvent, RemoteTrack, RemoteTrackPublication, RemoteParticipant } from 'livekit-client'
export class JcLive {
  /**
   * 本地客服端
   */
  public client: Room

  /**
   * 本地音频流
   */
  private localAudioTrack: any

  /**
   * 本地视频流
   */
  private localVideoTrack: any

  /**
   * 本地投屏流
   */
  private screenVideoTrack: any

  constructor(private options: {userId:string}) {
    const roomOptions: RoomOptions = {
      // automatically manage subscribed video quality
      adaptiveStream: true,
      // optimize publishing bandwidth and CPU for published tracks
      dynacast: true
    }
    // 创建房间
    this.client = new Room(roomOptions)
    this.init()
  }

  /**
   * 加入频道
   */
  async join({
    channel,
    audio,
    video,
  }: TLiveJoinParams) {
    const {token} = axios
        .get(`http://192.168.4.93:5000/getToken?userName=${this.options.userId}&roomName=${channel}`)
    this.client.connect('ws://192.168.4.93:7880', token)
    this.createTracks(video, audio)
  }

  /**
   * 创建本地音视频流
   * @param video 视频
   * @param audio 音频
   */
  async createTracks(video: boolean, audio: boolean) {
    if (video) {
      this.createVideoTrack()
    }
    if (audio) {
      this.createAudioTrack()
    }
  }

  /**
   * 创建本地音频流
   */
  async createAudioTrack() {
    if (this.localAudioTrack) {
      this.client.localParticipant.setMicrophoneEnabled(false)
      this.localAudioTrack = null
    }
    this.localAudioTrack = await this.client.localParticipant.setMicrophoneEnabled(true)
  }

  /**
   * 创建本地视频流
   */
  async createVideoTrack() {
    this.avState.v = true
    this.addAvList()
    if (this.localVideoTrack) {
      this.client.localParticipant.setCameraEnabled(false)
      this.localVideoTrack = null
    }
    this.localVideoTrack = await this.client.localParticipant.setCameraEnabled(true)
    //播放本地视频
    const element = this.localVideoTrack.track.attach();
    parentElement.appendChild(element);    
  }

  /**
   * 创建投屏
   */
  async createScreenVideoTrack(cd: Function) {
    try {
      this.screenVideoTrack = await this.client.localParticipant.setScreenShareEnabled(true, { audio: false })
      const element = this.screenVideoTrack.track.attach();
      parentElement.appendChild(element);    
      this.screenVideoTrack.track.mediaStreamTrack.onended = () => {
         // 点击停止共享触发
      }
    } catch (error) {
      console.log(error, '获取权限失败')
    }
  }

  /**
   * 关闭投屏
   */
  async closeScreenVideoTrack() {
    if (this.screenVideoTrack) {
      this.client.localParticipant.setScreenShareEnabled(false)
    }
  }

  /**
   * 关闭音频或者视频
   */
  closeAorV(type) {
    if (type === 'VIDEO') {
      if (this.localVideoTrack) {
        this.client.localParticipant.setCameraEnabled(false)
        this.localVideoTrack = null
        deleteChild(parentElement)
      }
    } else if (type === 'AUDIO') {
      if (this.localAudioTrack) {
        this.client.localParticipant.setMicrophoneEnabled(false)
        this.localAudioTrack = null
      }
    }
  }

  /**
   * 离开频道
   */
  leave() {
    if (this.localAudioTrack) {
      this.client.localParticipant.setMicrophoneEnabled(false)
      this.localAudioTrack = null
    }
    if (this.localVideoTrack) {
      this.client.localParticipant.setCameraEnabled(false)
      this.localVideoTrack = null
      deleteChild(parentElement)
    }
    if (this.screenVideoTrack) {
      this.client.localParticipant.setScreenShareEnabled(false)
    }
    this.client.disconnect()
  }
  // 初始化跟踪订阅
  init(){
    // 处理跟踪订阅  participant.identity 创建连接时userId
    this.client.on(
      RoomEvent.TrackSubscribed,
      (track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
        if (track.kind === 'video') {
          attachTrack(track.attach(), participant.identity)
        } else if (track.kind === 'audio') {
          track.attach().play()
        }
      }
    )
    // 监听用户离开
    this.client.on(
      RoomEvent.TrackUnsubscribed,
      (track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
        if (track.kind === 'video') {
          deleteChild(participant.identity)
        } else if (track.kind === 'audio') {
          track.attach().pause()
        }
      }
    )
    // 关闭音频或者视频 只监听 video
    this.client.on(RoomEvent.TrackMuted, (publication: RemoteTrackPublication, participant: RemoteParticipant) => {
      if (participant.identity !== this.options.userId && publication.kind === 'video') {
        deleteChild(participant.identity)
      }
    })
    // 打开音频或者视频
    this.client.on(RoomEvent.TrackUnmuted, (publication: RemoteTrackPublication, participant: RemoteParticipant) => {
    // 这里过滤掉自己的音视频变化。也可以不过滤在上面删除挂载dom操作
      if (participant.identity !== this.options.userId && publication.kind === 'video') {
        if (publication.track) {
          attachTrack(publication.track.attach(), participant.identity)
        }
      }
    })
  }
  
}
// dom 结构建设使用userId 作为id
function attachTrack(element: HTMLElement, id: string) {
  // creates a new audio or video element
  // find the target element for participant
  const domEL = document.getElementById(id)
  element.style.width = '100%'
  element.style.height = '100%'
  element.style.position = 'absolute'
  element.style.backgroundColor = '#000'
  if (domEL) {
    domEL.appendChild(element)
  } else {
    let time: any = setInterval(() => {
      const domELC = document.getElementById(id)
      if (domELC) {
        domELC.appendChild(element)
        clearInterval(time)
        time = null
      }
    }, 500)
  }
}
  • 使用
this.live = new JcLive({ userId}) // 创建音视频实例
this.live.join({ channel, video, audio }) // 加入音视频会议