前言
远程会议、在线教育、游戏娱乐··· ···,直播技术的应用越来越广泛,直播功能也越来越成为前端绕不过的一道门槛。前段时间公司的某个产品引用腾讯直播SDK,接入了直播功能,在开发期间也踩了不少坑。在此将完整的开发流程记录下来,与诸君共勉。
需要提前说明的是,直播功能大多需要与后端进行交互,本文的侧重点在于前端,对于后端的实现逻辑不会做相关介绍。
基础概念
在正式进入前端开发指南之前,有些基础概念需要了解:
- WebRTC: 前端直播技术的底层都一般离开WebRTC(腾讯直播SDK也是一样)。WebRTC全称Web Real-Time Communications,也就是一项用于web的实时通讯技术。详细介绍WebRTC不是本文的主旨,诸位可参考MDN WebRTC。
- 推流: 主播端将本地的音视频源推送到指定视频服务器,这个过程叫做推流。
- 拉流: 播放端将指定视频服务器中的音视频源拉取到本地进行播放,这个过程叫做拉流。
- TXLivePusher: TXLivePusher是腾讯提供的用于web推流的SDK。
- TCPlayer: TCPlayer是腾讯提供的用于web拉流播放的播放器SDK。
开发流程
SDK引入
umi项目直接在 config
文件中配置全局脚本:
typescript
// LIVE_RESOURCE_URL是存放SDK资源的地址。
// 这里我们建议将腾讯SDK下载存放到公司自己的资源服务器,以避免不可控问题的出现。
export default defineConfig({
// ...
externals: {
TXLivePusher: 'window.TXLivePusher',
TCPlayer: 'window.TCPlayer',
},
scripts: [
`${LIVE_RESOURCE_URL}/TXLivePusher/TXLivePusher-2.0.2.min.js`,
`${LIVE_RESOURCE_URL}/TcPlayer/libs/TXLivePlayer-1.2.4.min.js`,
`${LIVE_RESOURCE_URL}/TcPlayer/libs/hls.min.1.1.5.js`,
`${LIVE_RESOURCE_URL}/TcPlayer/libs/flv.min.1.6.3.js`,
`${LIVE_RESOURCE_URL}/TcPlayer/tcplayer.v4.7.0.min.js`,
],
});
简单封装推流方法
typescript
// LocalLivePusher.ts
interface Props {
id?: string;
videoQuality?: string;
audioQuality?: string;
fps?: number;
}
const codeDescMap = {
'0': '与服务器断开连接',
'1': '正在连接服务器',
'2': '连接服务器成功',
'3': '重连服务器中',
'-1': 'WebRTC 接口调用失败',
'-2': '请求服务器推流接口返回报错',
'-1001': '打开摄像头失败',
'-1002': '打开麦克风失败',
'-1003': '打开屏幕分享失败',
'-1004': '打开本地媒体文件失败',
'-1005': '摄像头被中断(设备被拔出或者权限被用户取消)',
'-1006': '麦克风被中断(设备被拔出或者权限被用户取消)',
'-1007': '屏幕分享被中断(chrome浏览器点击自带的停止共享按钮)',
};
type CodeDescMap = typeof codeDescMap;
export interface DeviceInfoItem {
deviceId: string;
deviceName: string;
type: 'video' | 'audio';
}
export class LocalLivePusher {
private carrier: any;
constructor({ id, videoQuality, audioQuality, fps }: Props) {
// 生成推流实例
const livePusher = new TXLivePusher();
// 指定本地画面容器
if (id) livePusher.setRenderView(id);
// 设置视频质量
livePusher.setVideoQuality(videoQuality || '720p');
// 设置音频质量
livePusher.setAudioQuality(audioQuality || 'standard');
// 自定义设置帧率
livePusher.setProperty('setVideoFPS', fps || 25);
this.carrier = livePusher;
}
// 是否在推流中
isPushing() {
return this.carrier.isPushing();
}
// 获取本地可用设备(麦克风、摄像头、共享屏幕等)
getDevices() {
return new Promise((resolve) => {
this.carrier
.getDeviceManager()
.getDevicesList()
.then((devices: DeviceInfoItem[]) => {
resolve(devices);
})
.catch(() => {
resolve([]);
});
});
}
// 各状态监听
observe() {
this.carrier.setObserver({
// warning message
onWarning: (code: keyof CodeDescMap, msg: CodeDescMap[keyof CodeDescMap]) => {
console.log('onWarning-msg: ', msg);
console.log('onWarning: ', codeDescMap[code]);
},
// error message
onError: (code: keyof CodeDescMap, msg: CodeDescMap[keyof CodeDescMap]) => {
console.log('onError-msg: ', msg);
console.log('onError: ', codeDescMap[code]);
},
// 推流连接状态回调通知
onPushStatusUpdate: (code: keyof CodeDescMap, msg: CodeDescMap[keyof CodeDescMap]) => {
console.log('onPushStatusUpdate-msg: ', msg);
console.log('onPushStatusUpdate: ', codeDescMap[code]);
},
});
}
// 打开指定设备
openDevice(list: ('audio' | 'video' | 'screen')[]): Promise<boolean> {
const promiseList: any[] = [];
if (list.includes('audio')) promiseList.push(this.carrier.startMicrophone());
if (list.includes('video')) promiseList.push(this.carrier.startCamera());
if (list.includes('screen')) promiseList.push(this.carrier.startScreenCapture(true));
return new Promise((resolve) => {
Promise.all(promiseList)
.then(() => {
resolve(true);
})
.catch((e) => {
console.log('err', e);
resolve(false);
});
});
}
// 关闭所有设备
closeDevice() {
this.carrier.stopCamera();
this.carrier.stopMicrophone();
this.carrier.stopScreenCapture();
}
// 开始推流
startPush(pushUrl: string) {
this.carrier.startPush(pushUrl);
}
// 停止推流
stopPush() {
this.carrier.stopPush();
}
}
推流
tsx
import { useEffect, useRef, useState } from 'react';
import LocalLivePusher from './LocalLivePusher.ts';
function LivePush() {
const livePusherRef = useRef(null);
const [isLiving, setIsLiving] = useState(false);
const checkRef = useRef(null);
// 获取推流状态(推流状态不等于直播状态,前端检查并不靠谱,建议从后端获取直播状态)
function checkLive() {
setIsLiving(livePusherRef.current.isPushing());
checkRef.current = setInterval(() => {
setIsLiving(livePusherRef.current.isPushing());
}, 500);
}
function initLivePusher() {
if (livePusherRef.current) return;
const tempPusher = new LocalLivePusher({});
livePusherRef.current = tempPusher;
}
//开始直播
function startLive() {
if (!livePusherRef.current) return;
livePusherRef.current.openDevice(['screen']); // 共享当前屏幕,也可以是'audio'、'video'
livePusherRef.current.startPush('webrtc://domain/AppName/StreamName?txSecret=xxx&txTime=xxx'); // 推流地址,需要后端返回
checkLive();
}
//停止直播
async function stopLive() {
if (!livePusherRef.current) return;
livePusherRef.current.stopPush();
livePusherRef.current.closeDevice();
}
useEffect(() => {
initLivePusher();
return () => {
if (!checkRef.current) return;
clearInterval(checkRef.current);
checkRef.current = null;
};
}, []);
return (
<div>
<div>
{!isLiving ? (
<button type='primary' onClick={startLive}>
开始直播
</button>
) : (
<button type='primary' onClick={stopLive}>
停止直播
</button>
)}
</div>
</div>
);
}
export default LivePush;
拉流
tsx
import { useLayoutEffect, useRef } from 'react';
function LivePlay() {
const playRef = useRef<any>();
//初始化播放器实例并进行播放
function initPlayRefAndPlay() {
if (!playRef.current) {
playRef.current = TCPlayer('playerMiniBox', {
autoplay: true,
});
}
playRef.current.src(playUrl); // playUrl需要从后端获取
}
//销毁播放器实例
function destroyPlayer() {
if (!playRef.current) return;
playRef.current.dispose();
playRef.current = null;
}
useLayoutEffect(() => {
initPlayRefAndPlay();
return destroyPlayer;
}, []);
return (
<div>
<h1>直播画面</h1>
<video id='playerMiniBox' width='480' height='270' preload='auto' playsInline />
</div>
);
}
export default LivePlay;
常见问题
以上是一个简单的推流、拉流的实践实例。在实际开发中肯定会碰到许许多多的问题,下面为我踩过的一些坑做个总结:
-
推流失败怎么办?
推流前首先需要确定浏览器版本是否支持WebRTC,也可以用推流SDK提供的checkSupport() 方法检查一下。其次打开设备需要授权,届时浏览器右上角会弹出授权提醒,需要手动点击同意。
-
播放失败怎么办?
多数播放失败其实是因为后端与腾讯云交互有问题导致的,如果想确定原因,可以拿后端返回的播放地址去腾讯官方demo中验证一下。