Skip to content

Meishe Streaming SDK

美摄SDK包含两部分:EffectSDK和StreamingSDK。美摄SDK基于WebAssembly功能实现,StreamingSDk包含了视频拍摄、编辑、特效渲染等各种功能,提供一整套视频编辑能力。

本文档主要讲解StreamingSDK的基本使用流程。您可以通过本文档内置的StackBlitz链接在线上实时编辑并预览。

一:基础流程和操作

1.引入SDK

您可以通过CDN的方式便捷引入SDK,CDN链接:

html
https://alieasset.meishesdk.com/NvWasm/domain/3-14-2-release/9/NvStreamingSdk.js

此连接为测试使用,客户正式使用,需要联系商务获取 sdk文件。 切勿在客户环境中使用。

这里假设您是SPA应用,需要在您的index.html中通过CDN方式引入SDK:

index.html:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://alieasset.meishesdk.com/NvWasm/domain/3-14-2-release/9/NvStreamingSdk.js"></script>
  </head>
</html>

2.安装SDK加载包

您需要通过美摄提供的meishewasmloader来加载SDK。

npm下载:

shell
npm install meishewasmloader

pnpm下载:

shell
pnpm install meishewasmloader

yarn下载:

shell
yarn add meishewasmloader

3.环境设置

SDK依赖于SharedArrayBuffer,由于浏览器安全性限制,因此您需要在您的环境中开启COOPCOEP。您可以在项目中做出以下修改:

vite.config.ts:

ts
...
server: {
    headers: {
    'Cross-Origin-Opener-Policy': 'same-origin',
    'Cross-Origin-Embedder-Policy': 'require-corp',
    },
},
...

4.初始化SDK

您可以通过以下方法初始化SDK:

ts
 import { WASMLoader }  from 'meishewasmloader';
  const wasmLoader = WASMLoader({
      // 加载进度回调       
      showLoader: function (state) {},
      // 失败回调
      showError(errorText: string) {},
      // 成功回调
      loadingFinished() {},
  });
// 开始调用函数加载
  wasmLoader.loadEmscriptenModule("https://alieasset.meishesdk.com/NvWasm/domain/3-14-2-release/9/");

5.授权和验证(可忽略)

未通过授权验证的SDK生成的视频会带有默认水印,只有通过授权验证之后才会去除,授权文件可以在美摄官网与商务联系获得。SDK内置授权验证接口,方法如下:

ts
//nvsGetStreamingContextInstance为sdk初始化后提供的全局方法,可以理解为StreamingSDK的instance
const nvStreamingContext = nvsGetStreamingContextInstance();
// 授权验证的回调函数
nvStreamingContext.onWebRequestAuthFinish = (success) => {
  if (!success) {
      console.error('SDK鉴权失败');
  } else {
      console.error('SDK鉴权成功');
  }
};
nvStreamingContext.verifySdkLicenseFile('鉴权地址')

6.获取StreamingContextInstance(SDK引擎实例)

当SDK初始化成功后,许多SDK内置方法会挂载到window中,您可以直接使用以下方法获取SDK引擎实例:

ts
const nvStreamingContext = nvsGetStreamingContextInstance();

在后续的开发中,大多数功能都要依赖nvStreamingContext实现。

7.创建LiveWindow

在您的项目中创建id为“live-window”的canvas标签。

代码如下:

html
<canvas id="live-window" />

同时记录canvas的宽和高:

ts
let canvas = document.getElementById('live-window') as HTMLCanvasElement;
// devicePixelRatio为设备像素分辨率比例
let width = canvas.clientWidth * window.devicePixelRatio;
let height = canvas.clientHeight * window.devicePixelRatio;
//LiveWindow对容器的宽高有限制,width必须是4的倍数,height必须是2的倍数
width = canvas.width = width - (width % 4);
height = canvas.height = height - (height % 2);

点击window.devicePixelRatio查看详情。

LiveWindow--流媒体窗口,可以简单理解为Editor播放器,它是实时编辑效果的展示窗口。

创建LiveWindow:

ts
const liveWindow = nvStreamingContext.createLiveWindow('live-window')

8.创建Timeline(时间线)

时间线是Editor的主要内容之一,轨道、片段、资源、素材、字幕、特效等都依托于时间线存在,添加到时间线即代表资源已被使用。

时间线占用资源较小,如有需要,一个程序中可以创建多个时间线。一般创建一条时间线即可。

创建时间线方法如下:

ts
//NvsCreateTimelineFlagEnum、NvsVideoResolution、NvsRational、NvsAudioResolution均为SDk初始化后注入的全局Class,无需引入
// 时间线创建的标识
const TIMELINE_FLAGS =
  NvsCreateTimelineFlagEnum.DontAddDefaultVideoTransition +
  NvsCreateTimelineFlagEnum.ForceAudioSampleFormatUsed +
  NvsCreateTimelineFlagEnum.RecordingUserOperation +
  NvsCreateTimelineFlagEnum.SyncAudioVideoTrasitionInVideoTrack;
const timeline = nvStreamingContext.createTimeline(
  new NvsVideoResolution(width,height),
  new NvsRational(25, 1),
  new NvsAudioResolution(44100, 2),
  TIMELINE_FLAGS
);

9.连接Timeline和LiveWindow

未连接的Timeline和LiveWindow是分离的独立模块,只有连接后LiveWindow才能展示受Timeline控制的画面。

连接方法如下:

ts
nvStreamingContext.connectTimelineWithLiveWindow(
  timeline,
  liveWindow
);

10.添加轨道(Track)

轨道(track)是素材片段(clip)的容器,一个timeline中可以存在多个轨道,轨道的顺序不同会体现出不同的覆盖关系,顺序越靠前,权重越大,层级也越高。要添加音视频,必须先创建轨道。

轨道分为音频轨道和视频轨道,创建方法如下:

ts
const videoTrack = timeline.appendVideoTrack();
const audioTrack = timeline.appendAudioTrack();

11.向轨道添加片段(clip)

一个素材是以片段(clip)的形式在轨道中存在的,一个轨道可以容纳多个素材。每个素材需要通过美摄转码服务转换成m3u8格式才能上轨(添加到轨道)。

在上轨之前,需要先使用SDK提供的FS接口下载/安装到内存中。

操作如下:

ts
// 转码后的资源地址
let videoResourceUrl =
  'https://alieasset.meishesdk.com/editor/2022/07/05/video/afd62303-3492-4c31-b09c-1c56c63b46a2/afd62303-3492-4c31-b09c-1c56c63b46a2.m3u8';
let audioResourceUrl =
  'https://alieasset.meishesdk.com/test/2024/05/24/audio/6dc60190-c22b-4740-a299-3981d1a8c7ec/6dc60190-c22b-4740-a299-3981d1a8c7ec.m3u8';

const videoResponse = await fetch(videoResourceUrl);
const videoText = await videoResponse.text();
const videoPath = `/afd62303-3492-4c31-b09c-1c56c63b46a2.m3u8`
// FS为全局对象,无需引入
FS.writeFile(videoPath, videoText)

const audioResponse = await fetch(audioResourceUrl);
const audioText = await audioResponse.text();
const audioPath = `/6dc60190-c22b-4740-a299-3981d1a8c7ec.m3u8`
FS.writeFile(audioPath, audioText)

const videoClip = videoTrack.appendClip(videoPath);
const audioClip = audioTrack.appendClip(audioPath);

12.播放/暂停

SDK提供了各种回调函数,其中onStreamingEngineStateChanged用来监听SDK播放状态的变化。

ts
// NvsStreamingEngineStateEnum无需引入
const playState = false
nvStreamingContext.onStreamingEngineStateChanged = (state) => {
  switch (state) {
    case NvsStreamingEngineStateEnum.StreamingEngineStatePlayback://播放
      playState(true);
      break;
    case NvsStreamingEngineStateEnum.StreamingEngineStateStopped://暂停
    case NvsStreamingEngineStateEnum.StreamingEngineStateSeeking://可以理解为时间切换中
      playState(false);
      break;
    default:
      playState(false);
      break;
  }
};

获取当前播放状态后,您可以根据不同的状态执行播放/暂停:

ts
let flags = 0;
if (playState) {
  // 虽然可以通过nvStreamingContext.streamingEngineReadyForTimelineModification来达到类似于暂停的效果,但是从直观意义上来讲,推荐使用stop方法。
  nvStreamingContext.stop();
} else {
  nvStreamingContext.playbackTimeline(
    timeline, // 当前时间线对象
    0, // 开始时间,单位为微秒
    -1, // 结束时间,单位为微秒, -1代表播放到结尾
    NvsVideoPreviewSizeModeEnum.LiveWindowSize, // 视频大小模式
    true, // 是否预加载
    (flags |= NvsPlaybackFlagEnum.BuddyHostOriginVideoFrame) // 播放标记
  );
}

至此,你已经完成了一个包含视频、音频,支持播放、暂停的简易版编辑器。

您可以通过点击StackBlitz,在线上环境实时编辑预览。

二:添加多轨

timeline可以添加多个轨道(track)。

多轨道可以通过播放区域调整可以实现分屏、多屏等效果,通过遮盖特性和透明度、模糊、关键帧等可以实现多层级、立体等效果,是一项十分使用实用的功能。

1.准备状态

在修改timeline和liveWindow之前,需要先改变nvStreamingContext为更新准备完成状态:

ts
await nvStreamingContext.streamingEngineReadyForTimelineModification();

2.创建新轨

ts
const videoTrack1 = timeline.appendVideoTrack();
const videoTrack2 = timeline.appendVideoTrack();
const audioTrack1 = tiemline.appendAudioTrack();
const audioTrack2 = tiemline.appendAudioTrack();

3.添加素材

ts
// addResource为安装资源方法的封装,您可以参考上方‘向轨道添加片段’自行封装,也可以拷贝StackBlitz中的线上代码
let path = await addResource(videoResourceUrl);
let audioPath = await addResource(audioResourceUrl)
const videoClip1 = videoTrack1.appendClip2(path, 0, 185000000);
const videoClip2 = videoTrack2.appendClip2(path, 0, 185000000);
const audioClip1 = audioTrack1.appendClip2(audioPath,0,185000000)
const audioClip2 = audioTrack1.appendClip2(audioPath,0,185000000)

到这一步多轨已经完成,但是由于两个轨道完全重叠且资源相同,新添加的轨道内素材画面会完全覆盖就轨道画面。所以我们不妨设置一些特效用以区分原轨道。

4.添加特效

添加特效用于区分原轨道画面。

您需要先开启特效:

ts
videoClip.enablePropertyVideoFx(true);

特效分为三类:string、boolean、float。分别通过setStringVal、setBooleanVal、setFloatVal来设置,具体参数可以点击内建特效表查看。

先获取片段特效实例:

ts
const propertyFx = videoClip.getPropertyVideoFx();

添加内建特效:

内建特效为SDK内置的、原生支持的特效,无需引入特效包即可实现。

ts
propertyFx.setFloatVal('Scale X', 0.5);//X轴缩放
propertyFx.setFloatVal('Scale Y', 0.5);//Y轴缩放
propertyFx.setFloatVal('Trans X', -100);//X轴位移
propertyFx.setFloatVal('Trans Y', 100);//Y轴位移
propertyFx.setFloatVal('Rotation', 30);//顺时针旋转
propertyFx.setFloatVal('Opacity', 0.9);//透明度

添加出入动画:

出入动画非内建特效,需要安装特效包后方可实现。

ts
const inAnimationPackageUrl = 'https://qasset.meishesdk.com/material/pu/videofx/0A2158E2-A290-4CFB-B1FD-868A96ED9E8B/0A2158E2-A290-4CFB-B1FD-868A96ED9E8B.8.videofx';
const inAnimationUuid = await installAsset(inAnimationPackageUrl);
// 入动画,或者组合动画
propertyFx.setStringVal('Package Id', inAnimationUuid as string);
// 入动画开始时间和结束时间,按照视频片段内的时间戳计算
propertyFx.setFloatVal('Package Effect In', 0);//入动画开始时间,单位微秒
propertyFx.setFloatVal('Package Effect Out', 2000000);//入动画结束时间,单位微秒

完整代码:

ts
await nvStreamingContext.streamingEngineReadyForTimelineModification();
const videoTrack = timeline.appendVideoTrack();
let path = await addResource(videoResourceUrl);
const videoClip = videoTrack.appendClip2(path, 0, 185000000);
if (videoClip) {
  // 对视频做缩放、平移、旋转和动画等调整
  videoClip.enablePropertyVideoFx(true);
  const propertyFx = videoClip.getPropertyVideoFx();
  if (propertyFx) {
    propertyFx.setFloatVal('Scale X', 0.5);
    propertyFx.setFloatVal('Scale Y', 0.5);
    propertyFx.setFloatVal('Trans X', -100);
    propertyFx.setFloatVal('Trans Y', 100);
    propertyFx.setFloatVal('Rotation', 30);
    propertyFx.setFloatVal('Opacity', 0.9);
    const inAnimationPackageUrl =
      'https://qasset.meishesdk.com/material/pu/videofx/0A2158E2-A290-4CFB-B1FD-868A96ED9E8B/0A2158E2-A290-4CFB-B1FD-868A96ED9E8B.8.videofx';
    const inAnimationUuid = await installAsset(inAnimationPackageUrl);
    // 入动画,或者组合动画
    propertyFx.setStringVal('Package Id', inAnimationUuid as string);
    // 入动画开始时间和结束时间,按照视频片段内的时间戳计算
    propertyFx.setFloatVal('Package Effect In', 0);//入动画开始时间,单位微秒
    propertyFx.setFloatVal('Package Effect Out', 2000000);//入动画结束时间,单位微秒
  }
}
// 移动当前时间至0秒,建议自行记录currentTime
nvStreamingContext.seekTimeline(
  timeline,
  0,
  NvsVideoPreviewSizeModeEnum.LiveWindowSize,
  NvsSeekFlagEnum.BuddyHostVideoFrame
);

您可以通过点击StackBlitz,在线上环境实时编辑预览。

三:添加转场

转场分为两类:内建转场和素材包转场。

1:内建转场

内建转场是SDK内置的转场效果,包含

  • Fade(淡入淡出)
  • Turning(翻转)
  • Swap(层叠)
  • Stretch In(伸展进入)
  • Page Curl(卷页)
  • Lens Flare(镜头炫光)
  • Star(星形)
  • Dip To Black(闪黑)
  • Dip To White(闪白)
  • Push To Right(右推拉)
  • Push To Top(上推拉)
  • Upper Left Into(斜推)

内建转场无需下载,可以直接使用。

用法如下:

ts
// 第一个参数是轨道中的片段索引,从0开始,第二个参数是内建特效名
videoTrack.setBuiltinTransition(0, 'Turning');

2:素材包转场

除内建转场外,您也可以通过加载素材包的形式在项目中引入转场。相比于内建转场,使用素材包转场的可选性更多。可以根据不同的视频风格,搭配不同的转场效果(您可以在美摄官网联系美摄商务获取素材包)。

素材包转场需要一个素材地址,类似于这样的:

ts
const packageUrl = 'https://qasset.meishesdk.com/material/pu/transition/02D05082-E3C3-498D-AAB2-15DC62AB2018/02D05082-E3C3-498D-AAB2-15DC62AB2018.1.videotransition'

获取地址后需要下载素材包并通过包管理器安装到内存,代码如下:

ts
async function installAsset(packageUrl: string) {
  let res = await fetch(packageUrl);
  const packageInFS = '/' + packageUrl.split('/').pop();
  await FS.writeFile(packageInFS, new Uint8Array(await res.arrayBuffer()));
  let assetType = NvsAssetPackageTypeEnum.VideoTransition;
  return new Promise((resolve, reject) => {
    if (assetType === undefined) {
      reject(assetUuid || '');
      return;
    }
    // 检查包状态,如果已经安装,则不需要再次安装
    // Check the status of asset package first. If it has been installed, don't need install it again.
    const status = nvsGetStreamingContextInstance()
      .getAssetPackageManager()
      .getAssetPackageStatus(assetUuid || '', assetType);
    if (status !== NvsAssetPackageStatusEnum.NotInstalled) {
      resolve(assetUuid || '');
      return;
    }
    // 通过安装包的回调判断是否成功
    // This callback function means installation finished
    nvsGetStreamingContextInstance().getAssetPackageManager().onFinishAssetPackageInstallation =
      (assetPackageId, assetPackageFilePath, assetPackageType, error) => {
        FS.unlink(assetPackageFilePath, 0);
        // error为0表示成功
        // error is 0 means success
        if (error === 0 && assetPackageId === assetUuid) {
          resolve(assetUuid || '');
        } else {
          reject(assetUuid || '');
        }
      };
    nvsGetStreamingContextInstance()
      .getAssetPackageManager()
      .installAssetPackage(packageInFS, '', assetType);
  });
}

您也可以点击下方的StackBlitz链接,在线上环境的utils/util.ts文件中查看对应代码。

包安装之后,通过以下方法使用素材包:

ts
const assetUuid = await installAsset(packageUrl);
// 第一个参数是轨道中的片段索引,从0开始,第二个参数是内建特效名
videoTrack.setPackagedTransition(1, assetUuid as string);

完整代码如下:

ts
await nvStreamingContext.streamingEngineReadyForTimelineModification();
let path = await addResource(videoResourceUrl);
videoTrack.addClip2(
  path,
  0,
  30000000,
  38000000
);
// 添加内建转场
// 内建转场列表可以参看:https://www.meishesdk.com/android/doc_ch/html/content/FxNameList_8md.html
videoTrack.setBuiltinTransition(0, 'Turning');
// 添加包转场
const packageUrl =
  'https://qasset.meishesdk.com/material/pu/transition/02D05082-E3C3-498D-AAB2-15DC62AB2018/02D05082-E3C3-498D-AAB2-15DC62AB2018.1.videotransition';
// installAsset是包安装方法,您可以点击下方的StackBlitz链接在线上环境中的utils/util.ts中查看代码
const assetUuid = await installAsset(packageUrl);
videoTrack.setPackagedTransition(1, assetUuid as string);
nvStreamingContext.seekTimeline(
  timeline,
  0,
  NvsVideoPreviewSizeModeEnum.LiveWindowSize,
  NvsSeekFlagEnum.BuddyHostVideoFrame
);

您可以通过点击StackBlitz,在线上环境实时编辑预览。

四:添加时间线特效

在上面的多轨示例中介绍过轨道特效,时间线特效与之类似,但是影响范围是整个时间线。

时间线特效也分为两种:内建特效和包特效。

您可以点击内建特效查看全部特效列表。

1:时间线内建特效用法如下:

ts
const timelineVideoFx = timeline.addBuiltinTimelineVideoFx(
  0,
  5000000,
  'Gaussian Blur'//高斯模糊
);

2:包特效

包特效也需要提供特效包地址:

ts
const packageUrl = 'https://qasset.meishesdk.com/material/pu/videofx/8EA07793-A3BB-4719-9882-3534E7D60618/8EA07793-A3BB-4719-9882-3534E7D60618.videofx';

与素材包转场一样,特效包也需要下载安装,且方法与素材包一致。如果您直接拷贝StackBlitz中的installAsset方法,则无需修改,可以直接使用。如果您拷贝转场中的示例,您需要修改assetType:

ts
async function installAsset(packageUrl: string) {
    ...
    let assetType = NvsAssetPackageTypeEnum.VideoFx;
    ...
}

包安装之后,通过以下方法使用素材包:

ts
const assetUuid = await installAsset(packageUrl);
timeline.addPackagedTimelineVideoFx(
  0,
  5000000,
  assetUuid as string
);

完整代码如下:

ts
    await nvStreamingContext.streamingEngineReadyForTimelineModification();
    // 添加内建特效
    // 内建特效列表及参数可以参看:https://www.meishesdk.com/android/doc_ch/html/content/FxNameList_8md.html
    const timelineVideoFx = timeline.addBuiltinTimelineVideoFx(
      0,
      5000000,
      'Gaussian Blur'
    );
    timelineVideoFx.setFloatVal('Radius', 20);
    // 添加包特效
    const packageUrl =
      'https://qasset.meishesdk.com/material/pu/videofx/8EA07793-A3BB-4719-9882-3534E7D60618/8EA07793-A3BB-4719-9882-3534E7D60618.videofx';
    const assetUuid = await installAsset(packageUrl);
    timeline.addPackagedTimelineVideoFx(
      0,
      5000000,
      assetUuid as string
    );
    nvStreamingContext.seekTimeline(
      timeline,
      0,
      NvsVideoPreviewSizeModeEnum.LiveWindowSize,
      NvsSeekFlagEnum.BuddyHostVideoFrame
    );

您可以通过点击StackBlitz,在线上环境实时编辑预览。

五:添加字幕

您可以通过以下方法添加字幕:

ts
const caption = timeline.addCaption(
  '你好',
  0,
  5000000,
  '',
  false
);

1:添加字幕样式

如果您希望字幕拥有炫酷的效果,可以使用字幕样式。

字幕样式需要素材包:

ts
const packageUrl = 'https://qasset.meishesdk.com/material/captionstyle/E30D10CF-6693-4BDD-BE66-418F86BB1578.5.captionstyle';

下载安装:

ts
async function installAsset(packageUrl: string) {
    ...
    let assetType = NvsAssetPackageTypeEnum.CaptionStyle;
    ...
}

使用字幕样式包:

ts
const assetUuid = await installAsset(packageUrl);
const caption = timeline.addCaption(
  '你好',
  0,
  5000000,
  assetUuid as string,
  false
);

2:字幕变型

添加字幕后可以缩放、旋转、位移等:

ts
caption.setCaptionTranslation(new NvsPointF(100, 100));
caption.scaleCaption2(2);
caption.rotateCaption2(45);

3:字体

字幕可以设置字体:

ts
const fontUrl = 'https://alieasset.meishesdk.com/font/站酷酷黑体.ttf';
const response = await fetch(fontUrl);
const fontInFS = '/' + fontUrl.split('/').pop();
await FS.writeFile(fontInFS, new Uint8Array(await response.arrayBuffer()));
caption.setFontByFilePath(fontInFS);

完整代码如下:

ts
    await nvStreamingContext.streamingEngineReadyForTimelineModification();
    // 添加字幕样式的字幕,文字为中文时,需要设置中文字体
    const packageUrl =
      'https://qasset.meishesdk.com/material/captionstyle/E30D10CF-6693-4BDD-BE66-418F86BB1578.5.captionstyle';
    const assetUuid = await installAsset(packageUrl);
    const caption = timeline.addCaption(
      '你好',
      0,
      5000000,
      assetUuid as string,
      false
    );
    const fontUrl = 'https://alieasset.meishesdk.com/font/站酷酷黑体.ttf';
    const response = await fetch(fontUrl);
    const fontInFS = '/' + fontUrl.split('/').pop();
    await FS.writeFile(fontInFS, new Uint8Array(await response.arrayBuffer()));
    // 对字幕做字体设置、缩放、平移和旋转等调整
    caption.setFontByFilePath(fontInFS);
    caption.setCaptionTranslation(new NvsPointF(100, 100));
    caption.scaleCaption2(2);
    caption.rotateCaption2(45);

    nvStreamingContext.seekTimeline(
      timeline,
      0,
      NvsVideoPreviewSizeModeEnum.LiveWindowSize,
      NvsSeekFlagEnum.BuddyHostVideoFrame
    );

您可以通过点击StackBlitz,在线上环境实时编辑预览。

六:模块字幕

您可以通过以下方式添加模块字幕:

ts
const caption = timeline.addModularCaption(
  '花字',
  0,
  5000000
);

1:添加字幕样式

需要模块字幕包:

ts
const packageUrl = 'https://qasset.meishesdk.com/material/captionstyle/48734DC5-6E58-46A9-9F48-E18CF1E25A1F.3.captionrenderer';

下载安装:

ts
async function installAsset(packageUrl: string) {
    ...
    let assetType = NvsAssetPackageTypeEnum.CaptionRenderer;
    ...
}

使用:

ts
const assetUuid = await installAsset(packageUrl);
const caption = timeline.addModularCaption(
  '花字',
  0,
  5000000
);
caption.applyModularCaptionRenderer(assetUuid as string);

2:字幕变型

添加字幕后可以缩放、旋转、位移等:

ts
caption.setCaptionTranslation(new NvsPointF(100, 100));
caption.scaleCaption2(2);
caption.rotateCaption2(45);

3:字体

模块字幕也可以指定字体:

ts
const fontUrl = 'https://alieasset.meishesdk.com/font/站酷酷黑体.ttf';
const response = await fetch(fontUrl);
const fontInFS = '/' + fontUrl.split('/').pop();
await FS.writeFile(fontInFS, new Uint8Array(await response.arrayBuffer()));
caption.setFontByFilePath(fontInFS);

完整代码如下:

ts
await nvStreamingContext.streamingEngineReadyForTimelineModification();

const packageUrl =
  'https://qasset.meishesdk.com/material/captionstyle/48734DC5-6E58-46A9-9F48-E18CF1E25A1F.3.captionrenderer';
const assetUuid = await installAsset(packageUrl);
const caption = timeline.addModularCaption(
  '花字',
  0,
  5000000
);
caption.applyModularCaptionRenderer(assetUuid as string);
const fontUrl = 'https://alieasset.meishesdk.com/font/站酷酷黑体.ttf';
const response = await fetch(fontUrl);
const fontInFS = '/' + fontUrl.split('/').pop();
await FS.writeFile(fontInFS, new Uint8Array(await response.arrayBuffer()));
caption.setFontByFilePath(fontInFS);
caption.setFontSize(100);

nvStreamingContext.seekTimeline(
  timeline,
  0,
  NvsVideoPreviewSizeModeEnum.LiveWindowSize,
  NvsSeekFlagEnum.BuddyHostVideoFrame
);

您可以通过点击StackBlitz,在线上环境实时编辑预览。

七:贴纸

SDK没有内置贴纸,必须传入贴纸素材包才能使用。

贴纸素材包:

ts
const packageUrl = 'https://qasset.meishesdk.com/material/pu/animatedsticker/A1509C3D-7F5C-43CB-96EE-639ED7616BB7/A1509C3D-7F5C-43CB-96EE-639ED7616BB7.1.animatedsticker';

下载安装:

ts
async function installAsset(packageUrl: string) {
    ...
    let assetType = NvsAssetPackageTypeEnum.AnimatedSticker;
    ...
}

您可以通过以下方式添加贴纸:

ts
const assetUuid = await installAsset(packageUrl);
const sticker = timeline.addAnimatedSticker(
  0,
  5000000,
  assetUuid as string,
  false,
  false,
  ''
);

1:贴纸变型

添加字幕后可以缩放、旋转、位移等:

ts
sticker.setTranslation(new NvsPointF(-100, 100));
sticker.scaleAnimatedSticker2(0.8);
sticker.rotateAnimatedSticker2(-30);

2:关键帧

贴纸可以设置关键帧,实现类似动画、过渡的效果:

ts
sticker.setCurrentKeyFrameTime(0);
sticker.setTranslation(new NvsPointF(-200, -100));
sticker.setCurrentKeyFrameTime(4000000);
sticker.setTranslation(new NvsPointF(0, 0));

完整代码如下:

ts
await nvStreamingContext.streamingEngineReadyForTimelineModification();
// 添加动画贴纸
// Add animated sticker
const packageUrl = 'https://qasset.meishesdk.com/material/pu/animatedsticker/A1509C3D-7F5C-43CB-96EE-639ED7616BB7/A1509C3D-7F5C-43CB-96EE-639ED7616BB7.1.animatedsticker';
const assetUuid = await installAsset(packageUrl);
const sticker = timeline.addAnimatedSticker(
  0,
  5000000,
  assetUuid as string,
  false,
  false,
  ''
);
// 对贴纸做缩放、平移和旋转等调整
sticker.setTranslation(new NvsPointF(-100, 100));
sticker.scaleAnimatedSticker2(0.8);
sticker.rotateAnimatedSticker2(-30);
// 设置贴纸平移关键帧
sticker.setCurrentKeyFrameTime(0);
sticker.setTranslation(new NvsPointF(-200, -100));
sticker.setCurrentKeyFrameTime(4000000);
sticker.setTranslation(new NvsPointF(0, 0));
nvStreamingContext.seekTimeline(
  timeline,
  0,
  NvsVideoPreviewSizeModeEnum.LiveWindowSize,
  NvsSeekFlagEnum.BuddyHostVideoFrame
);

您可以通过点击StackBlitz,在线上环境实时编辑预览。

八:时间线片段

您可以通过以下方法添加时间线片段(子时间线):

1:创建子时间线

ts
const clipTimeline = nvStreamingContext.createTimeline(
  new NvsVideoResolution(960, 540),
  new NvsRational(25, 1),
  new NvsAudioResolution(44100, 2)
);

2:添加轨道和片段

向子时间线中添加轨道、片段、特效。

ts
const videoTrack = clipTimeline.appendVideoTrack();
let path = await addResource(videoResourceUrl);
let clip = videoTrack.addClip2(path, 0, 1000000, 60000000);
clip.enablePropertyVideoFx(true);
const propertyFx = clip.getPropertyVideoFx();
if (propertyFx) {
  propertyFx.setFloatVal('Scale X', 0.8);
  propertyFx.setFloatVal('Scale Y', 0.8);
  propertyFx.setFloatVal('Trans X', -100);
  propertyFx.setFloatVal('Trans Y', 100);
  propertyFx.setFloatVal('Opacity', 0.7);
}

3:连接父时间线

上面的过程仅仅是创建了一个新的时间线,要做到时间线嵌套需要调用特定API:

ts
// defaultVideoTrack为原始时间线的视频轨道
defaultVideoTrack.addTimelineClip(clipTimeline, 0);

完整代码如下:

ts
    await nvStreamingContext.streamingEngineReadyForTimelineModification();
    // 构建片段时间线
    const clipTimeline = nvStreamingContext.createTimeline(
      new NvsVideoResolution(960, 540),
      new NvsRational(25, 1),
      new NvsAudioResolution(44100, 2)
    );
    const videoTrack = clipTimeline.appendVideoTrack();
    let path = await addResource(videoResourceUrl);
    let clip = videoTrack.addClip2(path, 0, 1000000, 60000000);
    clip.enablePropertyVideoFx(true);
    const propertyFx = clip.getPropertyVideoFx();
    if (propertyFx) {
      propertyFx.setFloatVal('Scale X', 0.8);
      propertyFx.setFloatVal('Scale Y', 0.8);
      propertyFx.setFloatVal('Trans X', -100);
      propertyFx.setFloatVal('Trans Y', 100);
      propertyFx.setFloatVal('Opacity', 0.7);
    }
    // defaultVideoTrack为原始时间线的视频轨道
    defaultVideoTrack.addTimelineClip(clipTimeline, 0);
    nvStreamingContext.seekTimeline(
      timeline,
      0,
      NvsVideoPreviewSizeModeEnum.LiveWindowSize,
      NvsSeekFlagEnum.BuddyHostVideoFrame
    );

您可以通过点击StackBlitz,在线上环境实时编辑预览。

九:特效

您可以点击特效名称列表查看全部类型和完整特效名称列表。

SDK中不同的主体和特效需要调用不同的API。

时间线内置特效:

无需下载,可以通过name直接使用,影响指定时间内整个时间线的所有轨道和片段。

ts
const raw = timeline.addBuiltinTimelineVideoFx(
  0,//入点时间
  5000000,//duration
  'Mosaic',//特效名 可替换
);

视频轨内置转场特效:

内置转场无需下载,但是有前提条件:轨道内必须包含最少两个片段且片段之间没有时间间隔。在此前提下,您可以通过以下方法设置转场:

ts
// 第一个参数为片段下标,第二个为转场名
videoTrack.setBuiltinTransition(0, 'Fade');

时间线包特效:

需要安装特效包。安装方法可以直接拷贝资源安装中的installAsset方法。与时间线内置特效一样,也是影响指定时间内整个时间线的所有轨道和片段。

ts
const xxx = await installAsset(path)
const videoFx = timeline.addPackagedTimelineVideoFx(
    0,// 入点
    5000000,// duration
    'xxx',// 特效包ID
);

视频轨包特效:

需要安装特效包。安装方法可以直接拷贝资源安装中的installAsset方法。

ts
const videoFx = videoTrack.addPackagedTrackVideoFx(
  0,// 入点
  5000000,// duration
  'xxx',// 特效包ID
);

音频片段内建特效:

无需下载,直接使用。注意主体为音频片段,而非音频轨道。

ts
const audioFx = audioClip.appendFx('Audio Reverb')

属性特效:

特效名称列表中,所有带有表格(包含参数、类型、最大值、最小值、默认值、说明等属性)的特效都属于属性特效。属性特效有些需要配合素材包使用,有些可以直接使用。具体分别可以通过表格信息中的“说明”来区分:如果“说明”中包含“xxxID”字样则代表该特效需要搭配素材包。

设置属性特效前需要做以下操作:

ts
videoClip.enablePropertyVideoFx(true) // 开启视频片段属性特效
const fx = videoClip.getPropertyVideoFx() // 获取视频片段特效原型
const audioFx = audioClip.appendFx('xxx') // 添加并获取音频片段特效原型

根据参数类型的不同,属性特效需要调用不同的API。主要类型为:

类型API
STRINGsetStringVal
BOOLsetBooleanVal
FLOATsetFloatVal
MenusetMenuVal
INTsetIntVal
COLORsetColorVal

以上setAPI全是NvsFx中的方法,也就是通过getPropertyVideoFx获取的特效原型中的方法,以上所有方法都需要两个参数第一个传参为特效表中的参数,第二个为值。您可以通过以下方法设置:

ts
fx.setStringVal('Package Id','xxx') // 设置滤镜包,需要先安装资源包,再执行该操作
fx.setBooleanVal('Beauty Effect',true) // 开启美颜特效
fx.setFloatVal('Beauty Whitening',0.8) // 美白强度80%
fx.setMenuVal('Fill Mode', 'Stretch') // 设置画面填充模式为拉伸
fx.setIntVal('Advanced Beauty Type',1)// 设置高级美颜类型,需要先配置'Advanced Beauty Enable'开启高级美颜
fx.setColorVal('Makeup Lip Color',new NvsColor(1,0,0,1)) // 美妆口红颜色。NvsColor参数为0-1,分别为r、g、b、a

十:资源安装

SDK需要把资源安装到内存后才能使用。所以在使用特效包、素材包之前,需要先将资源下载安装。您可以通过SDK提供的FS对象,以下是完整的封装方法:

ts
export async function installAsset(packageUrl: string) {
  let res = await fetch(packageUrl);
  const packageInFS = '/' + packageUrl.split('/').pop();
  await FS.writeFile(packageInFS, new Uint8Array(await res.arrayBuffer()));
  const list = packageInFS.split('.');
  const assetUuid = list[0].split('/').pop();
  const suffix = list.pop();
  // 根据不同的包后缀区分不同的包类型,注意:特效包不允许修改名称
  let assetType = undefined;
  if (suffix === 'videofx') {
    assetType = NvsAssetPackageTypeEnum.VideoFx;
  } else if (suffix === 'captionstyle') {
    assetType = NvsAssetPackageTypeEnum.CaptionStyle;
  } else if (suffix === 'animatedsticker') {
    assetType = NvsAssetPackageTypeEnum.AnimatedSticker;
  } else if (suffix === 'videotransition') {
    assetType = NvsAssetPackageTypeEnum.VideoTransition;
  } else if (suffix === 'makeup') {
    assetType = NvsAssetPackageTypeEnum.Makeup;
  } else if (suffix === 'facemesh') {
    assetType = NvsAssetPackageTypeEnum.FaceMesh;
  } else if (suffix === 'captionrenderer') {
    assetType = NvsAssetPackageTypeEnum.CaptionRenderer;
  } else if (suffix === 'captioncontext') {
    assetType = NvsAssetPackageTypeEnum.CaptionContext;
  } else if (suffix === 'template') {
    assetType = NvsAssetPackageTypeEnum.Template;
  }
  return new Promise((resolve, reject) => {
    if (assetType === undefined) {
      reject(assetUuid || '');
      return;
    }
    // 检查包状态,如果已经安装,则不需要再次安装
    const status = nvsGetStreamingContextInstance()
      .getAssetPackageManager()
      .getAssetPackageStatus(assetUuid || '', assetType);
    if (status !== NvsAssetPackageStatusEnum.NotInstalled) {
      resolve(assetUuid || '');
      return;
    }
    // 通过安装包的回调判断是否成功
    nvsGetStreamingContextInstance().getAssetPackageManager().onFinishAssetPackageInstallation =
      (assetPackageId, assetPackageFilePath, assetPackageType, error) => {
        FS.unlink(assetPackageFilePath, 0);
        // error为0表示成功
        if (error === 0 && assetPackageId === assetUuid) {
          resolve(assetUuid || '');
        } else {
          reject(assetUuid || '');
        }
      };
    nvsGetStreamingContextInstance()
      .getAssetPackageManager()
      .installAssetPackage(packageInFS, '', assetType);
  });
}

本文档中提到的所有特效包、资源包、模型包等可以在美摄官网联系商务获取。

十一:网络资源

一般来说,上轨的资源(视频、音频、图片等)都需要经过转码(m3u8),但是SDk也提供无需转码、通过网络资源加载的方式。

相对于完全支持的转码资源,网络资源支持的类型较少,且需要您从资源中获取指定信息。

市面上相关的库有很多,目前比较推荐的是mediabuuny,在下方的代码示例中就是使用mediabuuny库获取的资源信息。

当然,如果您有其他熟悉的库,完全可以更换。只要您的最终数据满足IMetaData的格式就好。

网络资源的支持性不如转码资源,目前使用mediabuuny库测试,支持的资源格式如下:

视频格式:

  1. mp4
  2. m4v
  3. 3gp
  4. mov

音频格式:

  1. mp3
  2. wav
  3. m4a

示例代码如下:

ts
interface IVideoStream {
  width: number;
  height: number;
  duration: number;
}
interface IAudioStream {
  duration: number;
  channelCount: number;
  sampleRate: number;
}
enum EMediaType {
  audio = 'audio',
  video = 'video',
}
interface IMetaData {
  audioStreams: IAudioStream[];
  bitrate: number;
  duration: number;
  mediaType: EMediaType;
  videoStreams: IVideoStream[];
  webAssetUrl: string;
  webLocalFileId: string;
}
async function afterInitialize(resourceData: {
    type: 'audio' | 'video';
    resourceUrl: string;
  }) {
    // 创建内存资源文件夹
    FS.mkdir('/localmedia');
    // 您可以自由拟定uuid,本质上是一串字符串
    let uuid = generateUUID();
    const webLocalPath = `/localmedia/${uuid}=.weblocal`;
    const defaultMetaData =      '{"mediaType":"video","webLocalFileId":"","webAssetUrl":"https://alieasset.meishesdk.com/test/resource/video/2025/09/01/92493/0d6b19e4d57b4fe0a7b7df0e3430a801.mp4","duration":30000000,"bitrate":493820,"videoStreams":[{"duration":30000000,"width":864,"height":480}],"audioStreams":[{"duration":30000000,"sampleRate":44100,"channelCount":2}]}';

    const nvStreamingContext = nvsGetStreamingContextInstance();
    await nvStreamingContext.streamingEngineReadyForTimelineModification();
    // 清空timeline中的音视频轨道,当然您可以根据实际需求判断是否删除这一段代码
    let videocount = timeline.videoTrackCount();
    for (let i = videocount; i > 0; i--) {
      let res = timeline.removeVideoTrack(i - 1);
    }
    let audiocount = timeline.audioTrackCount();
    for (let i = 0; i < audiocount; i++) {
      timeline.removeAudioTrack(i);
    }

    let webLocalData: IMetaData = JSON.parse(defaultMetaData);
    // 使用mediabuuny库,支持音视频
    webLocalData = await uploadFile(
      resourceData.resourceUrl
      resourceData.type
    );
    const webLocalString = JSON.stringify(webLocalData);
    FS.writeFile(webLocalPath, webLocalString);
    if (webLocalData.mediaType === 'video') {
      const videoTrack = timeline.appendVideoTrack();
      videoTrack.addClipWithSpeedExt2(
        webLocalPath,
        0,
        webLocalData.duration,
        0,
        webLocalData.duration,
        1,
        true,
      );
    } else {
      const videoTrack = timeline.appendVideoTrack();
      videoTrack.appendClip2(
        ':/footage/transparent_black.png',
        0,
        webLocalData.duration,
      );
      const audioTrack = timeline.appendAudioTrack();
      audioTrack.addClipWithSpeedExt2(
        webLocalPath,
        0,
        webLocalData.duration,
        0,
        webLocalData.duration,
        1,
        true,
      );
    }
    nvStreamingContext.seekTimeline(
      timeline,
      0,
      NvsVideoPreviewSizeModeEnum.LiveWindowSize,
      NvsSeekFlagEnum.BuddyHostVideoFrame
    );
  }


  async function uploadFile(resource: File | string, type: 'audio' | 'video') {
    let webAssetUrl = resource instanceof File ? '' : resource;
    const source =
      resource instanceof File
        ? new BlobSource(resource)
        : new UrlSource(resource);
    const input = new Input({
      source,
      formats: ALL_FORMATS, // Accept all formats
    });
    let mediaType = type as EMediaType;

    let duration = await input.computeDuration().then((res) => res * 1000000);
    let tracks = await input.getTracks();
    let bitrate = 493820;
    let audioStream = {
      channelCount: 0,
      duration: 0,
      sampleRate: 0,
    };
    let videoStream = {
      duration: 0,
      width: 0,
      height: 0,
    };

    let hasAudioTrack = false;
    for (let i = 0; i < tracks.length; i++) {
      let track = tracks[i];
      let duration = await track
        .computeDuration()
        .then((second) => getFrameTime(second * 1000000));
      shortDelay()
        .then(() => track.computePacketStats())
        .then((stats) => {
          bitrate = stats.averageBitrate;
        });
      if (track.isVideoTrack()) {
        let height = track.codedHeight;
        let width = track.codedWidth;
        videoStream = {
          duration,
          width,
          height,
        };
      } else if (track.isAudioTrack()) {
        hasAudioTrack = true;
        let channelCount = track.numberOfChannels;
        let sampleRate = track.sampleRate;
        audioStream = {
          channelCount,
          duration,
          sampleRate,
        };
      }
    }
    const metaRealData: IMetaData = {
      audioStreams: hasAudioTrack ? [audioStream] : [],
      bitrate,
      duration: getFrameTime(duration),
      mediaType,
      videoStreams: mediaType === 'video' ? [videoStream] : [],
      webAssetUrl,
      webLocalFileId: '',
    };

    return metaRealData;
  }

十二:本地资源

观察网络资源的使用方案后您应该可以发现,网络资源uploadFile方法接受的参数中,resource支持File类型。

从SDK的角度来讲,只要您将资源信息按照正确的格式写入FS即可,SDK并不关心是否网络/本地资源。

所以网络资源的代码稍加修改即可实现本地资源。

您可以使用input获取File,或者使用UI库的upload组件获取。相信这些操作对您来说不是难事,所以这里不再赘述。

十三:转码

转码服务是SDK完整生态中的重要一环,经过转码的资源可以获得最高的支持度(当然,您可以使用网络/本地资源的方式上轨)。

视频资源、音频资源、图片资源等经过转码后生成新的m3u8资源。这里的m3u8并非一般意义上的流文件,内部格式有所不同,标准化的m3u8文件无法在SDK中直接使用。

如果您需要完整的转码服务可以在美摄官网与美摄商务联系。

如果您需要完整的音视频解决方案也可以在美摄官网与美摄商务联系。