腾讯直播SDK前端实践指南

前言

远程会议、在线教育、游戏娱乐··· ···,直播技术的应用越来越广泛,直播功能也越来越成为前端绕不过的一道门槛。前段时间公司的某个产品引用腾讯直播SDK,接入了直播功能,在开发期间也踩了不少坑。在此将完整的开发流程记录下来,与诸君共勉。

需要提前说明的是,直播功能大多需要与后端进行交互,本文的侧重点在于前端,对于后端的实现逻辑不会做相关介绍。

基础概念

在正式进入前端开发指南之前,有些基础概念需要了解:

  1. WebRTC: 前端直播技术的底层都一般离开WebRTC(腾讯直播SDK也是一样)。WebRTC全称Web Real-Time Communications,也就是一项用于web的实时通讯技术。详细介绍WebRTC不是本文的主旨,诸位可参考MDN WebRTC
  2. 推流: 主播端将本地的音视频源推送到指定视频服务器,这个过程叫做推流。
  3. 拉流: 播放端将指定视频服务器中的音视频源拉取到本地进行播放,这个过程叫做拉流。
  4. TXLivePusher: TXLivePusher是腾讯提供的用于web推流的SDK。
  5. 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;

常见问题

以上是一个简单的推流、拉流的实践实例。在实际开发中肯定会碰到许许多多的问题,下面为我踩过的一些坑做个总结:

  1. 推流失败怎么办?

    推流前首先需要确定浏览器版本是否支持WebRTC,也可以用推流SDK提供的checkSupport() 方法检查一下。其次打开设备需要授权,届时浏览器右上角会弹出授权提醒,需要手动点击同意。

  2. 播放失败怎么办?

    多数播放失败其实是因为后端与腾讯云交互有问题导致的,如果想确定原因,可以拿后端返回的播放地址去腾讯官方demo中验证一下。

相关推荐
会发光的猪。4 分钟前
vue中el-select选择框带搜索和输入,根据用户输入的值显示下拉列表
前端·javascript·vue.js·elementui
旺旺大力包21 分钟前
【 Git 】git 的安装和使用
前端·笔记·git
PP东26 分钟前
ES学习class类用法(十一)
javascript·学习
海威的技术博客30 分钟前
JS中的原型与原型链
开发语言·javascript·原型模式
雪落满地香37 分钟前
前端:改变鼠标点击物体的颜色
前端
余生H1 小时前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿1 小时前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~1 小时前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫1 小时前
高德地图自定义折线矢量图形
前端·vue.js·vue
所以经济危机就是没有新技术拉动增长了1 小时前
二、javascript的进阶知识
开发语言·javascript·ecmascript