全部产品

WebSDK 接入指南

aliyun-video-editing-websdk 是一个快速搭建云智能剪辑 web 端界面的 SDK,本文将介绍如何接入这个 SDK

一、准备工作

在 html 的 head 标签中引入 sdk 的 CSS 文件,如下所示

<head>
  <link rel="stylesheet" href="https://g.alicdn.com/thor-server/video-editing-websdk/x.x.x/index.css">
</head>

其中 x.x.x 为版本号,当前最新版本为 3.6.0

然后在 html 的 body 标签中创建一个用以挂载剪辑界面的节点,在 body 标签的尾部引入 sdk 的 JS 文件,再引用一个 script 用以调用 websdk,如下所示:

<body>
  <div id="aliyun-video-editor" style="height:700px"></div> // 您可以根据需要改变container高度
  <script src="https://g.alicdn.com/thor-server/video-editing-websdk/x.x.x/index.js"></script>
  <script>
    // 调用 SDK 的代码放在这里
  </script>
</body>

二、初始化

WebSDK 目前只提供单独 JS 引入的方式,后续会考虑提供 npm 包。该单独 JS 会在 window 上挂载 AliyunVideoEditor 对象。

WebSDK 调用方式如下:

window.AliyunVideoEditor.init(config);

config 是一个对象,对象的 key-value 说明见下一节

三、config 参数说明

我们先通过 WebSDK 生成的界面介绍一下常用的一些 config 参数是如何被调用的

界面介绍各部分介绍如下:

  1. 当前工程绑定的素材,页面初始化时 SDK 会调用 getEditingProjectMaterials 参数获取当前工程已绑定的素材并展示在对应的 tab(根据素材的 mediaType 属性决定展示在哪个 tab)

  2. 当用户点击这个按钮后,SDK 会调用 searchMedia 参数,调用方此时应展示一个选择素材的界面(比如一个弹窗),并且在用户选择并确认之后,将用户选中的素材返回给 SDK,SDK 会将这些素材添加在对应的 tab 中

  3. 当用户的鼠标悬浮在素材上时,会展示一个删除按钮,用户如果点击这个删除按钮,SDK 会调用 deleteEditingProjectMaterials 参数通知调用方该素材已经从工程中删除,调用方应发起请求将这个素材与这个工程解绑

  4. 当前工程的时间线,页面初始化时 SDK 会调用 getEditingProject 参数获取当前工程的时间线并展示,调用方应发起请求从服务端拿到当前的时间线等数据传给 SDK

  5. 当用户点击保存按钮时,SDK 会调用 updateEditingProject 参数,将工程的时间线等数据传给调用方,调用方应将这些数据发送给服务端进行保存

  6. 当用户点击生成按钮时,SDK 会调用 produceEditingProjectVideo 参数,将工程的时间线等数据传给调用方,一般而言,这些数据是不够的,调用方应展示一个弹窗让用户填写更多的参数,比如希望合成的视频分辨率等,在用户点击提交的时候,调用方将这些用户填写的数据连同 SDK 传出的数据提交给服务端进行合成。

贴纸界面

7. 当用户首次点击贴纸 tab 时,SDK 会调用 getStickerCategories 参数获取贴纸的分类并展示在 8 的位置,同时调用 getStickers 参数获取第一个贴纸分类下的贴纸

8. 当用户点击一个未点击过的贴纸分类时,SDK 会调用 getStickers 参数获取该分类的贴纸

config 详细的参数项如下所示:

参数名

参数说明

是否必选

参数类型

参数类型说明

引入版本

locale

界面语言

'zh-CN' | 'en-US'

语言代码,默认展示中文

3.0.0

container

WebSDK 生成界面挂载的 DOM 节点

HTMLElement

DOM 节点

3.0.0

defaultAspectRatio

默认的预览比例

PlayerAspectRatio

预览比例枚举值,详见第四节,默认 16:9

3.4.0

defaultSubtitleText

字幕的默认文本内容

string

如不填,则为“阿里云剪辑”(中文)或"Online Editing"(英文)。如填写,不能超过 20个字符

3.6.0

useDynamicSrc

是否动态获取资源的 src

boolean

3.0.0

getDynamicSrc

如果 useDynamicSrc 为 true,则需要传此项,用以动态获取资源的 src

(mediaId: string, mediaType: 'video' | 'audio' | 'image') => Promise<string>

一个函数,参数为资源的 mediaId 和 mediaType。返回一个 Promise,该 Promise 应该 resolve 资源新的 src

3.0.0

getEditingProjectMaterials

获取工程关联的素材

() => Promise<Media[]>

一个函数,返回一个 Promise,该 Promise 应该 resolve 所有素材类型的素材数组

3.0.0

searchMedia

从媒资库导入素材

(mediaType: 'video' | 'audio' | 'image') => Promise<Media[]>

一个函数,参数指定添加素材的类型。返回一个 Promise,该 Promise 应该 resolve 新增的素材数组。注意,调用方必须得自己将新增的素材与工程关联起来

3.0.0

deleteEditingProjectMaterials

将素材与工程解绑

(mediaId: string, mediaType: 'video' | 'audio' | 'image') => Promise<void>

一个函数,传入参数为被删除素材的 mediaId 和 mediaType,返回一个 Promise,调用方保存成功后 resolve,否则 reject

3.0.0

submitASRJob

提交智能识别字幕任务(m3u8 格式的素材暂不支持该功能)

(mediaId: string, startTime: string, duration: string) => Promise<ASRResult[]>

一个函数,传入参数为要识别字幕的素材 id,该素材片段的开始时间及时长,返回一个 Promise,该 Promise 应该 resolve 识别的结果 ASRResult 数组

3.1.0

submitAudioProduceJob

提交文字转语音任务

(text:string,voice:AudioProduceVoice)=>Promise<Media>

一个函数,传入参数为字幕的内容和用户选择的声音(详见第四节),返回一个 Promise,该 Promise 应该 resolve 生成语音的数据

3.5.0

getStickerCategories

获取贴纸分类,如不传,则不分类

() => Promise<StickerCategory[]>

一个函数,返回一个 Promise,该 Promise 应该 resolve 贴纸的分类数组

3.0.0

getStickers

获取贴纸

(config: {categoryId?: string; page: number; size: number}) => Promise<StickerResponse>

一个函数,返回一个 Promise,该 Promise 应该 resolve 贴纸的总量和贴纸数组。如果贴纸没有分类,则 categoryId 为空

3.0.0

getEditingProject

获取工程的时间线

() => Promise<{FEExtend?: FEExtend; projectId?: string; modifiedTime?: string}>

一个函数,返回一个 Promise,该 Promise 应该 resolve 这个工程的 FEExtend 数据、项目 id、最后修改时间

3.0.0

updateEditingProject

保存工程的时间线

(data: {coverUrl: string; duration: number; FEExtend: FEExtend; isAuto: boolean}) => Promise<{projectId: string}>

一个函数,传入参数为工程的封面图地址、时长(单位为秒)、 FEExtend 数据和是否自动保存(当前每分钟自动保存 1 次),返回一个 Promise,调用方保存成功后 resolve,否则 reject

3.0.0

produceEditingProjectVideo

生成视频

(data: {coverUrl: string; duration: number; aspectRatio: PlayerAspectRatio; timeline: Timeline}) => Promise<void>

一个函数,传入参数为工程的封面图地址、时长(单位为秒)和 Timeline 数据,返回一个 Promise,调用方生成成功后 resolve,否则 reject

3.0.0(aspectRatio 是 3.4.0 新增的)

四、数据结构补充说明

FEExtend

该数据为 SDK 内部数据,不建议对其进行修改,只需要透传即可

Timeline

该数据为 SDK 生成的时间线数据,用以发送给服务端进行视频的合成,具体结构定义,请参见 Timeline 配置说明

PlayerAspectRatio

枚举值:'16:9', '9:16'

AudioProduceVoice

枚举值:'xiaoyun', 'xiaogang', 'sitong', 'aitong'

(分别对应标准女声、标准男声、可爱男童声、软萌女童声)

Media

interface Media {
  mediaId: string;
  mediaType: 'video' | 'audio' | 'image';
  video?: { // materialType 为'video'时须传入此项
    title: string;
    coverUrl: string;
    duration: number;
    src?: string; // 当 useDynamicUrl 为 true 时,src 可以不传
    snapshots: string[];
    sprites: string[];
    spriteConfig: {
      num: string; // 雪碧图中有多少个小图
      lines: string; // 行数
      cols: string; // 列数
      cellWidth?: string; // 单个小图的宽度,可不传
      cellHeight?: string; // 单个小图的高度,可不传
    };
  };
  audio?: { // materialType 为'audio'时须传入此项
    title: string;
    duration: number;
    src?: string; // 当 useDynamicUrl 为 true 时,src 可以不传
    coverUrl?: string; // 音频封面
  };
  image?: { // materialType 为'image'时须传入此项
    title: string;
    src?: string; // 当 useDynamicUrl 为 true 时,src 可以不传
    coverUrl?: string; // 图片预览图,在列表及轨道区会优先展示该字段,如果没有,则展示 src 字段,如果两者都没有则无法在轨道区预览
  };
}

ASRResult

interface ASRResult {
  content: string; // 字幕的内容
  from: number; // 字幕的开始相对于识别素材的开始的时间偏移量
  to: number; // 字幕的结束相对于识别素材的开始的时间偏移量
}

StickerCategory

interface StickerCategory {
  id: string; // 分类的 id
  name: string; // 分类的名称,调用者自行切换语言
}

StickerResponse

interface Sticker {
  mediaId: string;
  src: string;
}
  
interface StickerResponse {
  total: number;
  stickers: Sticker[];
}

五、init 方法示例代码

WebSDK 本身只负责界面和交互,不会发起请求,您需要将请求的逻辑提供给 WebSDK 调用。请求本身应该先发送给您自己的服务端,您自己的服务端再根据自己的 AccessKeyId, AccessKeySecret 转发给相关的阿里云服务端。

// 注意,WebSDK 本身并不提供 request 这个方法,这里仅作为示例,您可以使用您喜欢的网络请求库,如 axios 等

window.AliyunVideoEditor.init({
  container: document.getElementById('aliyun-video-editor'),
  locale: 'zh-CN',
  useDynamicSrc: true, // 媒资库默认情况下播放地址会过期,所以需要动态获取
  getDynamicSrc: (mediaId, mediaType) => new Promise((resolve, reject) => {
    request('GetMediaInfo', { // https://help.aliyun.com/document_detail/197842.html
      MediaId: mediaId
    }).then((res) => {
      if (res.code === '200') {
        // 注意,这里仅作为示例,实际中建议做好错误处理,避免如 FileInfoList 为空数组时报错等异常情况
        resolve(res.data.MediaInfo.FileInfoList[0].FileBasicInfo.FileUrl);
      } else {
        reject();
      }
    });
  }),
  getEditingProjectMaterials: () => {
    if (projectId) { // projectId 由调用方自己保存
      return request('GetEditingProjectMaterials', { // https://help.aliyun.com/document_detail/209068.html
        ProjectId: projectId
      }).then((res) => {
        const data = res.data.MediaInfos;
        return transMediaList(data); // 需要做一些数据变换,具体参考后文
      });
    }
    return Promise.resolve([]);
  },
  searchMedia: (mediaType) => { // mediaType 为用户当前所在的素材 tab,可能为 video | audio | image,您可以根据这个参数对应地展示同类型的可添加素材
    return new Promise((resolve) => {
      // 调用方需要自己实现展示媒资、选择媒资添加的界面,这里的 callDialog 只是一种示例,WebSDK 本身并不提供
      // 关于展示媒资,请参考:https://help.aliyun.com/document_detail/197964.html
      callDialog({
        onSubmit: async (materials) => {
          if (!projectId) { // 如果没有 projectId,需要先创建工程,如果能确保有 projectId,则不需要该步
            const addRes = await request('CreateEditingProject', { // https://help.aliyun.com/document_detail/197834.html
              Title: 'xxxx',
            });
            projectId = addRes.data.Project.ProjectId;
          }

          // 组装数据
          const valueObj = {};
          materials.forEach(({ mediaType, mediaId }) => {
            if (!valueObj[mediaType]) {
              valueObj[mediaType] = mediaId;
            } else {
              valueObj[mediaType] += mediaId;
            }
          })
          const res = await request('AddEditingProjectMaterials', { // https://help.aliyun.com/document_detail/209069.html
            ProjectId: projectId,
            MaterialMaps: valueObj,
          });
          if (res.code === '200') {
            return resolve(transMediaList(res.data.MediaInfos));
          }
          resolve([]);
        }
      });
    });
  },
  deleteEditingProjectMaterials: (mediaId, mediaType) => {
    const res = request('DeleteEditingProjectMaterials', { // https://help.aliyun.com/document_detail/209067.html
      ProjectId: projectId,
      MaterialType: mediaType,
      MaterialIds: mediaId
    });
    if (res.code === '200') return Promise.resolve();
    return Promise.reject();
  },
  getStickerCategories: async () => {
    const res = await request('ListAllPublicMediaTags', { // https://help.aliyun.com/document_detail/207796.html
      BusinessType: 'sticker',
      WebSdkVersion: window.AliyunVideoEditor.version
    });

    const stickerCategories = res.data.MediaTagList.map(item => ({
      id: item.MediaTagId,
      name: myLocale === 'zh-CN' ? item.MediaTagNameChinese : item.MediaTagNameEnglish // myLocale 是您期望的语言
    }));
    return stickerCategories;
  },
  getStickers: async ({ categoryId, page, size }) => {
    const params = {
      PageNo: page,
      PageSize: size,
      IncludeFileBasicInfo: true,
      MediaTagId: categoryId
    };

    const res = await request('ListPublicMediaBasicInfos', params); // https://help.aliyun.com/document_detail/207797.html

    const fileList = res.data.MediaInfos.map(item => ({
      mediaId: item.MediaId,
      src: item.FileInfoList[0].FileBasicInfo.FileUrl
    }));

    return {
      total: res.data.TotalCount,
      stickers: fileList
    };
  },
  getEditingProject: async () => {
    if (projectId) {
      const res = await request('GetEditingProject', { // https://help.aliyun.com/document_detail/197837.html
        ProjectId: projectId,
        FEExtendFlag: 1
      });
      
      const FEExtendString = res.data.Project.FEExtend;
      const timelineString = res.data.Project.Timeline;
   
      return {
        projectId,
        timeline: timelineString ? JSON.parse(timelineString) : undefined,
        FEExtend: FEExtendString ? JSON.parse(FEExtendString) : undefined,
        modifiedTime: res.data.Project.ModifiedTime
      };
    }
    return {};
  },
  updateEditingProject: ({ coverUrl, duration, FEExtend, isAuto }) => new Promise((resolve, reject) => {
    request('UpdateEditingProject', { // https://help.aliyun.com/document_detail/197835.html
      ProjectId: projectId,
      CoverURL: coverUrl,
      Duration: duration,
      FEExtend: JSON.stringify(FEExtend)
    }).then((res) => {
      if (res.code === '200') {
        // WebSDK 本身会进行自动保存,isAuto 则是告诉调用方这次保存是否自动保存,调用方可以控制只在手动保存时才展示保存成功的提示
        !isAuto && Message.success('保存成功');
        resolve({
          projectId
        });
      } else {
        reject();
      }
    });
  }),
  produceEditingProjectVideo: ({ coverUrl, duration = 0, timeline }) => {
    return new Promise((resolve) => {
      callDialog({ // 调用方需要自己实现提交合成任务的界面,这里的 callDialog 只是一种示例
        onSubmit: async ({ title, description, mediaMetadata, produceConfig }) => { // 假设提交合成任务的界面让你获得了这些数据
          const res = await request('SubmitMediaProducingJob', { // https://help.aliyun.com/document_detail/197853.html
            Title: title,
            Description: description,
            ProjectId: projectId,
            MediaMetadata: JSON.stringify(mediaMetadata),
            ProduceConfig: JSON.stringify(produceConfig),
            Timeline: timeline
          });
          if (res.code === '200') {
            Message.success('生成视频成功');
          }
          resolve();
        }
      });
    });
  }
});

/**
 * 将服务端的素材信息转换成 WebSDK 需要的格式
 */
function transMediaList(data) {
  if (!data) return [];

  if (Array.isArray(data)) {
    return data.map((item) => {
      const basicInfo = item.MediaBasicInfo;
      const fileBasicInfo = item.FileInfoList[0].FileBasicInfo;
      const mediaId = basicInfo.MediaId;
      const result = {
        mediaId
      };
      const mediaType = basicInfo.MediaType
      result.mediaType = mediaType;

      if (mediaType === 'video') {
        result.video = {
          title: fileBasicInfo.FileName,
          duration: Number(fileBasicInfo.Duration),
          coverUrl: basicInfo.CoverURL
        };
        const spriteImages = basicInfo.SpriteImages
        if (spriteImages) {
          try {
            const spriteArr = JSON.parse(spriteImages);
            const sprite = spriteArr[0];
            const config = JSON.parse(sprite.Config);
            result.video.spriteConfig = {
              num: config.Num,
              lines: config.SpriteSnapshotConfig?.Lines,
              cols: config.SpriteSnapshotConfig?.Columns,
              cellWidth: config.SpriteSnapshotConfig?.CellWidth,
              cellHeight: config.SpriteSnapshotConfig?.CellHeight
            };
            result.video.sprites = sprite.SnapshotUrlList;
          } catch (e) {
            console.log(e);
          }
        }
      } else if (mediaType === 'audio') {
        result.audio = {
          title: fileBasicInfo.FileName,
          duration: Number(fileBasicInfo.Duration),
          coverURL: '' // 您可以给音频文件一个默认的封面图
        }
      } else if (mediaType === 'image') {
        result.image = {
          title: fileBasicInfo.FileName,
          coverUrl: fileBasicInfo.FileUrl
        }
      }

      return result;
    });
  } else {
    return [data];
  }
}

六、其他顶层接口

以上所叙述的都是 window.AliyunVideoEditor 对象中 init 方法的使用,这个对象还包含以下属性或方法:

  1. version

用以获取当前版本的版本号