前期准备
- 安装livekit(基于go语言,开源的框架)。下载地址:发布 v1.2.5 ·实时套件/实时套件 ·吉特哈布 (github.com)
运行livekit
使用 ./livekit-server.exe --dev 运行livekit。
本地开发使用 --dev 会使用默认的 API Key 和 API Secret
获取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 }) // 加入音视频会议