Tutorial: 开启视频合流插件

开启视频合流插件

功能描述

本文主要介绍如何使用合流插件将不同输入源的画面合流到一条轨道上。

前提条件

  • TRTC Web SDK version >= 5.12.0.

  • 兼容性:

    浏览器 兼容性
    桌面端 Chrome
    桌面端 Safari
    桌面端 Firefox
    桌面端 Edge
    移动端 ❌暂不支持合流功能

实现流程

引入并注册插件

import { VideoMixer } from 'trtc-sdk-v5/plugins/video-effect/video-mixer';
let trtc = TRTC.create({ plugins: [VideoMixer] });

开启合流

await trtc.startPlugin('VideoMixer', {
  view: 'local_preview_div', // 预览
  canvasInfo: {
    width: 1920,
    height: 1080
  },
  camera: [
    {
      id: 'camera1',
      layout: {
        x: 0,
        y: 0,
        width: 640,
        height: 480,
        zIndex: 1
      }
    }
  ]
  // ...
});

开启插件时,必须设置合流画布宽高,其余参数都是可选的。

合流各种输入源

摄像头

可以添加摄像头输入源,合流插件可以控制摄像头的采集,camera参数数组传入一个带有唯一 id 的对象代表采集摄像头并合流。

await trtc.updatePlugin('VideoMixer', {
  camera: [
    {
      id: 'camera1',
      profile: { width: 640, height: 480, frameRate: 15 },
      layout: {
        x: 100,
        y: 0,
        width: 640,
        height: 480,
        zIndex: 1
      }
    }
  ]
});

要更新摄像头参数,传入相同 id 并更新其他参数即可。

await trtc.updatePlugin('VideoMixer', {
  camera: [
    {
      id: 'camera1',
      layout: {
        x: 500,
        y: 500,
        width: 640,
        height: 480,
        zIndex: 1,
        rotation: 90,
        mirror: true
      }
    }
  ]
});

如果要关闭某个摄像头,只需传入的数组中去掉该对象。

await trtc.updatePlugin('VideoMixer', {
  camera: []
});

注:如果要临时隐藏摄像头而不是物理层面关闭,则通过 hidden 参数实现:

await trtc.updatePlugin('VideoMixer', {
  camera: [
    {
      id: 'camera1',
      layout: {
        // ...
        hidden: true,
      },
    },
  ],
});

其他输入源同理。

共享屏幕

可以添加共享屏幕输入源,合流插件可以控制共享屏幕的采集,screen参数数组中传入一个带有唯一 id 的对象代表采集共享屏幕并合流。

await trtc.updatePlugin('VideoMixer', {
  screen: [{
    id: 'screen1',
    profile: { width: 1920, height: 1080, frameRate: 15 },
    layout: {
      x: 0,
      y: 0,
      width: 1920,
      height: 1080,
      zIndex: 0,      
    }
  }]
});

更新屏幕合流参数:

await trtc.updatePlugin('VideoMixer', {
  screen: [{
    id: 'screen1',
    layout: {
      x: 100,
      y: 100,
      width: 1000,
      height: 500,
      zIndex: 2,
      rotation: 180,
      mirror: true
    }
  }]
});

关闭共享屏幕同理,传入的数组中去掉该对象即可。

await trtc.updatePlugin('VideoMixer', {
  screen: []
});

文字

可以添加文字输入源,text参数数组中传入一个带有唯一 id 的对象代表合流一个文字源:

await trtc.updatePlugin('VideoMixer', {
  text: [
    {
      id: 'text1',
      content: 'MultiLine\nTest',
      font: 'bold 60px SimHei',
      color: 'red',
      layout: {
        x: 200,
        y: 300,
        width: 300,
        height: 150,
        zIndex: 6,
      }
    }
  ]
});

注:文字内容过多时超出 layout 区域的部分会被裁剪,业务侧需根据文字内容自行调整 layout 宽高。

更新文字参数:

await trtc.updatePlugin('VideoMixer', {
  text: [
    {
      id: 'text1',
      content: 'TRTC🥳',
      font: 'bold 120px Georgia',
      color: 'blue',
      layout: {
        x: 100,
        y: 200,
        width: 600,
        height: 150,
        zIndex: 7,
        mirror: true
      }
    }
  ]
});

删除文字

await trtc.updatePlugin('VideoMixer', {
  text: []
});

图片

可以添加图片输入源,image参数数组中传入一个带有唯一 id 的对象代表合流一个图片源:

await trtc.updatePlugin('VideoMixer', {
  image: [{
    id: 'img1',
    url: './image.png',
    layout: {
      x: 0,
      y: 500,
      width: 800,
      height: 400,
      zIndex: 4,
    }
  }]
});

更新图片参数:

await trtc.updatePlugin('VideoMixer', {
  image: [{
    id: 'img1',
    url: './another-img.png',
    layout: {
      x: 100,
      y: 100,
      width: 600,
      height: 500,
      zIndex: 4,
      fillMode: 'fill'
    }
  }]
});

删除图片:

await trtc.updatePlugin('VideoMixer', {
  image: []
});

视频

可以添加视频源,video参数数组中传入一个带有唯一 id 的对象代表合流一个视频源:

await trtc.updatePlugin('VideoMixer', {
  video: [{
    id: 'video1',
    url: './video.mp4',
    layout: {
      x: 0,
      y: 0,
      width: 1000,
      height: 500,
      zIndex: 5,
    }
  }]
});

更新视频参数:

await trtc.updatePlugin('VideoMixer', {
  video: [{
    id: 'video1',
    url: './another-video.mp4',
    layout: {
      x: 100,
      y: 100,
      width: 1280,
      height: 720,
      zIndex: 5,
    }
  }]
});

删除视频:

await trtc.updatePlugin('VideoMixer', {
  video: []
});

关闭合流

移除所有输入源并关闭合流:

await trtc.stopPlugin('VideoMixer');

合流推流

推流与否不由合流插件控制,但startPlugin会返回合流的视频轨道,可以将其作为startLocalVideo等接口的参数来控制推流:

const { track } = await trtc.startPlugin('VideoMixer', {
  canvasInfo: {
    width: 1920,
    height: 1080
  },
  // ...
});
// 推流
await trtc.startLocalVideo({
  option: {
    videoTrack: track,
    profile: {
      width: 1920,
      height: 1080,
      bitrate: 2000
    }
  }
});

合流帧率

指定合流画布帧率:

await trtc.startPlugin('VideoMixer', {
  canvasInfo: {    
    width: 1920,
    height: 1080,
    frameRate: 20
  },
});

注:不推荐手动设置合流帧率,因为插件内部会自动计算合适的帧率——有摄像头或屏幕时,取摄像头和屏幕分享视频的最大帧率,没有时固定帧率为15。手动指定可能会损失部分性能。

错误处理

因合流插件在调用 start/updatePlugin 时可以传入的参数众多,为确保在某些输入源出现错误时不会影响其他输入源的正常合流,插件的错误处理策略为:

  • 传入的输入源参数部分应用成功,部分失败时,接口不会抛出错误,而是以返回值形式告知业务侧成功和失败信息。
  • 传入的输入源参数全部应用失败,或出现其他可能中断合流的错误时,接口会抛出错误,并携带失败信息。

业务侧逻辑示例:

try {
  const { result } = await trtc.updatePlugin('VideoMixer', {
    camera: [/**/],
    screen: [/**/],
    text: [/**/],
    image: [/**/],
    video: [/**/],
  });
  // 部分失败时的错误处理
  result.failedDetails.forEach(({id, error}: {id: string, error: any}) => {
    console.error(`${id} mix failed`, error);
  })
  console.log(result.successOptions) // 成功应用的参数信息
} catch (error: any) {
  // 全部失败时的错误捕获
  console.error(error);
  error?.data?.failedDetails.forEach(({ id, error }: { id: string, error: any }) => {
    console.error(`${id} mix failed`, error);
  })  
}

关于 successOptions 的详细说明

successOptions 返回的是本次更新后合流底层真正应用的参数,具体理解为:

  • 当没有任何错误发生时,successOptions 就是传入的 options

  • 当部分错误发生时,successOptions 包括成功更新部分传入的参数,以及失败部分沿用旧的参数。

    举例来说,假设合流已经有了摄像头和屏幕输入源:

    await trtc.startPlugin('VideoMixer', {
      camera: [{/*zIndex = 1*/}],
      screen: [{/*zIndex = 0*/}]
    });
    

    此时想更新摄像头的 zIndex 为0(会失败,因为 zIndex 重复了),并更新 screen 的 mirror 为 true(不会失败):

    await trtc.updatePlugin('VideoMixer', {
      camera: [{/*zIndex = 0*/}],
      screen: [{/*mirror = true*/}]
    });
    

    这种情况下就是部分失败(摄像头更新失败,屏幕更新成功),返回的 successOptions 的值形式如下:

    {
      camera: [{/*zIndex = 1*/}], // 更新失败,相当于应用的还是之前的zIndex
      screen: [{/*mirror = true*/}]  // 更新成功,为本次更新传入的参数
    }
    

    也就是说,successOptions 告诉了业务侧更新后合流真正应用的参数是什么,以便业务侧进行 UI 或数据的同步。

API 说明

trtc.startPlugin('VideoMixer', options)

用于开启合流插件。

options: VideoMixerOptions

Name Type Required Description
view string | HTMLElement | null 选填 合流视频预览的 HTMLElement 实例或者 Id, 如果不传或传入 null, 则不会播放合流视频。
canvasInfo CanvasInfo 必填 设置合流画布的参数
camera CameraSource[] 选填 控制摄像头输入源的参数
screen ScreenSource[] 选填 控制共享屏幕输入源的参数
text TextSource[] 选填 控制文字输入源的参数
image ImageSource[] 选填 控制图片输入源的参数
video VideoSource[] 选填 控制视频输入源的参数
onScreenShareStop (id: string) => {} 选填 合流共享屏幕停止时调用的回调,用户未通过 sdk 而是通过浏览器按钮取消屏幕分享时可能会用到,回调参数为停止采集的屏幕 id

CanvasInfo

Name Type Required Description
canvasColor string | CanvasGradient | CanvasPattern 选填 合流背景色,参考 canvas fillStyle 属性
width number 必填 合流画布宽度
height number 必填 合流画布高度
frameRate number 选填 手动指定合流画面帧率。一般情况下最好由合流内部自动计算帧率,手动指定可能会损失部分性能

CameraSource

Name Type Required Description
id string 必填 标识输入源的唯一 id
layout LayerOption 必填 布局参数
cameraId string 选填 采集的摄像头 id,指定采集哪个摄像头
videoTrack MediaStreamTrack 选填 自定义采集的 videoTrack
profile VideoProfile 选填 视频采集参数。默认值:480p_2

ScreenSource

Name Type Required Description
id string 必填 标识输入源的唯一 id
layout LayerOption 必填 布局参数
profile ScreenShareProfile 选填 屏幕采集参数。默认值:1080p
captureElement HTMLElement 选填 采集当前页面中的某个 HTMLElement,支持 Chrome 104+。
preferDisplaySurface 'current-tab' | 'tab' | 'window' | 'monitor' 选填 控制屏幕分享预选框中的不同类型采集的显示优先级,默认是 monitor,即:在屏幕分享采集预选框中,优先展示屏幕采集。若填 'current-tab',预选框只会展示当前页面。支持 Chrome 94+。

TextSource

Name Type Required Description
id string 必填 标识输入源的唯一 id
layout LayerOption 必填 布局参数
content string 必填 文字内容
color string | CanvasGradient | CanvasPattern 选填 文字颜色,参考 canvas fillStyle 属性
font string 选填 字体,参考 canvas font 属性

ImageSource

Name Type Required Description
id string 必填 标识输入源的唯一 id
layout LayerOption 必填 布局参数
url string 必填 图片url

VideoSource

Name Type Required Description
id string 必填 标识输入源的唯一 id
layout LayerOption 必填 布局参数
url string 必填 视频url

LayerOption

Name Type Required Description
x number 必填 相对画布左上角的横向坐标,可以为负数
y number 必填 相对画布左上角的纵向坐标,可以为负数
width number 必填 输入源的布局宽度,需要 > 0
height number 必填 输入源的布局高度,需要 > 0
zIndex number 必填 输入源层级,每个输入源的层级不应重复
fillMode 'contain' | 'cover' | 'fill' 选填 画面填充布局的模式,摄像头默认cover,其余输入源默认contain,参考 CSS object-fit 属性。
mirror boolean 选填 是否镜像
rotation 0 | 90 | 180 | 270 选填 画面旋转角度
hidden boolean 选填 是否暂时隐藏画布上的输入源,使用场景:不想物理层面关闭摄像头/屏幕、临时隐藏输入源而不销毁资源

Returns: Promise<MixParseResult>

MixParseResult

Name Type Description
track MediaStreamTrack 合流输出的视频轨道
result object 本次传入的参数中执行成功或失败的相关信息,结构如下:
Name Type Description
successOptions VideoMixerOptions | UpdateVideoMixerOptions 本次更新成功执行的options
failedDetails Array<MixFailedDetail> 本次更新执行失败的options的详细信息

MixFailedDetail

Name Type Description
id string 更新失败的输入源 id
error Error 更新失败的原因

Example:

// 示例1: 开启合流,仅设置画布
await trtc.startPlugin('VideoMixer', {
  view: 'local_preview_div',
  canvasInfo: {
    canvasColor: 'green',
    width: 1920,
    height: 1080
  },
});
// 示例2: 开启合流,合流输入源,并在失败时打印原因
try {
  const { result } = await trtc.startPlugin('VideoMixer', {
    view: `local_preview_div`,
    canvasInfo: {
      width: 1920,
      height: 1080,
    },
    screen: [
      {
        id: 'screen1',
        profile: { width: 1920, height: 1080, frameRate: 15 },
        preferDisplaySurface: 'tab',
        layout: {
          x: 0,
          y: 0,
          width: 1920,
          height: 1080,
          zIndex: 0,
        },
      },
    ],
    // ...
    onScreenShareStop: (id: string) => {
      console.log(`screen share stop, id: ${id}`);
    },
  });
  result.failedDetails.forEach(({ id, error }: { id: string; error: any }) => {
    console.error(`${id} mix failed`, error);
  });
} catch (error: any) {
  console.error(error);
  error?.data?.failedDetails.forEach(({ id, error }: { id: string, error: any }) => {
    console.error(`${id} mix failed`, error);
  })  
}

trtc.updatePlugin('VideoMixer', options)

options: UpdateVideoMixerOptions

Name Type Required Description
view string | HTMLElement | null 选填 合流视频预览的 HTMLElement 实例或者 Id, 如果不传或传入 null, 则不会播放合流视频。
canvasInfo CanvasInfo 选填 设置合流画布的参数
camera CameraSource[] 选填 控制摄像头输入源的参数
screen ScreenSource[] 选填 控制共享屏幕输入源的参数
text TextSource[] 选填 控制文字输入源的参数
image ImageSource[] 选填 控制图片输入源的参数
video VideoSource[] 选填 控制视频输入源的参数

Returns

返回值同startPlugin

Example

// 示例1: 移除所有摄像头
await trtc.updatePlugin('VideoMixer', {
  camera: []
});
// 示例2: 更新合流参数,并在失败时打印原因
try {
  const { result } = await trtc.updatePlugin('VideoMixer', {
    camera: [
      {
        id: 'camera1',
        profile: { width: 640, height: 480, frameRate: 30 },
        layout: {
          x: 100,
          y: 0,
          width: 640,
          height: 480,
          zIndex: 1,
        },
      },
    ],
    // ...
  });
  result.failedDetails.forEach(({id, error}: {id: string, error: any}) => {
    console.error(`${id} mix failed`, error);
  })
} catch (error: any) {
  console.error(error);
  error?.data?.failedDetails.forEach(({ id, error }: { id: string, error: any }) => {
    console.error(`${id} mix failed`, error);
  })  
}

trtc.stopPlugin('VideoMixer')

停止合流插件。

Example:

await trtc.stopPlugin('VideoMixer');

特殊说明

1.分辨率、码率怎么设置?

  • 分辨率就是画布宽高
  • 码率不由合流插件控制,业务侧推流时通过 start/updateLocalVideo 设置。

2.为什么输入源传参都是数组形式,如果只想更新数组中某一类输入源怎么做?

  • 每种类型的输入源都是全量更新的方式,目的是为了支持批量更新以及删除(通过对比前后传入的数组)。
  • 每种输入源参数都是可选的,比如只想更新 camera 时,updatePlugin 时只传入 camera 数组,其他类型输入源的数组不传,或者一并传入但是保留原参数。示例如下:
await trtc.startPlugin('VideoMixer', { // 假设已经合流了摄像头和屏幕
  camera: [/* config */],
  screen: [/* config */]
});

此时如果只想更新 camera 而不更新 screen,那么有两种方式:

  1. 仅传入camera,不传入screen
await trtc.updatePlugin('VideoMixer', {
  camera: [/* newConfig */], // 只传入camera数组
});
  1. 保留screen的旧参数一并传入
await trtc.updatePlugin('VideoMixer', {
  camera: [/* newConfig */],
  screen: [/* config */] // 参数保持原样
});

两种方式更推荐第一种,因为合流插件需要解析的参数更少。第二种方式虽然效果是一样的,但是会重复解析参数。

3.传入数组的前提下,只想更新某一个输入源怎么办?

比如更新某一个摄像头(id = 1),那么传入的 camera 数组只改动 id = 1 的元素的参数,其他元素虽然不用更新,但是参数需要保持原样传入。

4.采集摄像头和屏幕时设置的码率为什么无效?

采集摄像头和屏幕时设置的码率会被忽略,上行码率由业务侧推流时调用的接口(start/updateLocalVideo)设置。

5.设置旋转角度不是围绕中心旋转?

合流旋转的原理是将画面旋转后,画面左上角仍保持 layout 传入的坐标再绘制,如果要实现中心旋转,业务侧需自行调整坐标。

6.如果合流屏幕共享期间用户不是通过sdk而是点击浏览器的按钮停止屏幕共享,业务侧如何感知?

startPlugin 的参数中提供了onScreenShareStop回调,使用示例:

await trtc.startPlugin('VideoMixer', {
  // ...
  onScreenShareStop: (id: string) => {
    console.log(`screen share stop, id: ${id}`);
  }
});

最佳实践

  1. 因合流插件的接口start/updatePlugin可以传递的参数较多,每次更新时尽量少传没有变动且非必填的参数,比如在多次高频更新摄像头坐标时,可以不必传入采集相关的参数(如 profile、cameraId 等只要传入一次即可),以减少插件底层解析参数的开销。
  2. 画布分辨率设置越大、输入源越多渲染开销越大,业务侧需考虑设备情况决定合适的合流参数。