视频点播提供专业在线的视频剪辑能力,针对自动化、智能化剪辑以及多人协作视频制作需求,您可以基于时间线进行云剪辑。通过阅读本文,您可以了解如何接入视频剪辑Web SDK。

使用说明

本文以引入4.10.0版本的视频剪辑Web SDK为例进行说明。

操作步骤

  1. 引入视频剪辑Web SDK。

    在项目前端页面文件中的<head>标签处引入视频剪辑Web SDK的CSS文件,如下所示:

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

    <body>标签处添加一个用以挂载剪辑界面的<div>节点,并在<body>标签末尾添加引入Web SDK的JS文件,同时添加一个用以调用Web SDK的<script>节点。

    <body>
      <div id="aliyun-video-editor" style="height:700px"></div> // 您可以根据需要改变 container 高度
      <script src="https://g.alicdn.com/thor-server/video-editing-websdk/4.10.0/index.js"></script>
      <script>
        // 调用 SDK 的代码放在这里
      </script>
    </body>
  2. 初始化视频剪辑Web SDK。
    window.AliyunVideoEditor.init(config);

config属性说明

参数类型必填描述引入版本
localestring界面语言,取值:
  • zh-CN(默认值):中文。
  • en-US:英文。
3.0.0
containerElementWeb SDK生成界面挂载的DOM节点。3.0.0
defaultAspectRatioPlayerAspectRatio默认的视频预览比例,默认为16∶9。3.4.0
defaultSubtitleTextstring默认的字幕内容,不超过20个字符,默认为“阿里云剪辑”。3.6.0
useDynamicSrcboolean是否动态获取资源信息。3.0.0
getDynamicSrc(mediaId: string, mediaType: 'video' | 'audio' | 'image' | 'font', mediaOrigin?:'private' | 'public', inputUrl?: string) => Promise<string>;动态获取资源信息,必填与否与参数useDynamicSrc一致。返回的Promise对象需要resolve资源新的信息。3.10.0
getEditingProjectMaterials() => Promise<Media[]>;获取工程关联的素材。返回的Promise对象需要resolve所有素材类型的数组。3.0.0
searchMedia(mediaType: 'video' | 'audio' | 'image') => Promise<Media[]>;资源库导入素材按钮相应函数。单击导入素材后会搜索媒资信息,将媒资库媒资导入到资源库中。返回的Promise对象需要resolve新增素材的数组。
重要 您需要调用AddEditingProjectMaterials接口将新增的素材与工程关联起来。
3.0.0
deleteEditingProjectMaterials(mediaId: string, mediaType: 'video' | 'audio' | 'image') => Promise<void>;解绑工程与素材。返回的Promise对象需要resolve。3.0.0
getStickerCategories() => Promise<StickerCategory[]>;获取贴纸分类,如果不传,则不分类。返回的Promise对象需要resolve贴纸的分类数组。3.0.0
getStickers(config: {categoryId?: string; page: number; size: number}) => Promise<StickerResponse>;获取贴纸,如果贴纸没有分类,则categoryId 为空。返回的Promise对象需要resolve贴纸的总量和贴纸数组。3.0.0
getEditingProject() => Promise<{timeline?: Timeline; projectId?: string; modifiedTime?: string}>;获取工程的时间线。返回的Promise对象需要resolve时间线Timeline数据、项目ID和最后修改时间。3.0.0
updateEditingProject(data: {coverUrl: string; duration: number; timeline: Timeline; isAuto: boolean}) => Promise<{projectId: string}>;保存工程的时间线,参数依次为:工程的封面图地址、时长(单位:秒)、Timeline数据和是否自动保存(当前每分钟自动保存1次)。返回的Promise对象需要resolve项目ID。3.0.0
produceEditingProjectVideo(data:{ coverUrl: string; duration: number; aspectRatio: PlayerAspectRatio; timeline: Timeline; recommend: IProduceRecommend; }) => Promise<void>;生成视频,参数依次为:工程的封面图地址、时长(单位:秒)、视频比例、Timeline数据和recommend (视频合成的分辨率、码率的推荐数据)。返回的Promise对象需要resolve。4.4.0
customTexts{importButton?:string;updateButton?:string;produceButton?:string;backButton?:string;logoUrl?:string;}自定义部分文案,参数依次对应视频剪辑界面的导入素材保存导出视频返回按钮的文案和左上角Logo。3.7.0
getPreviewWaterMarks() => Promise<Array<{ url?: string; mediaId?:string; width?: number; height?: number; x?: number; y?: number; xPlusWidth?: number; yPlusHeight?: number; opacity?: number; }>>;预览区添加水印,防止截屏(合成时没有水印),参数如下所示:
  • url:水印图片地址,与水印媒资ID二选一。
  • mediaId:水印媒资ID,与水印图片地址二选一。
  • width:图片的宽缩放百分比,范围:(0,1],默认值为1,表示自适应预览区。
  • height:图片的高缩放百分比,范围:(0,1],默认值为1,表示自适应预览区。
  • x:图片相对预览区的x坐标值,范围:[0~1]。
  • y:图片相对预览区的y坐标值,范围:[0~1]。
  • xPlusWidth:图片定位的左右偏移参数,例如:图片左右居中,则x=0.5,xPlusWidth=-0.5。
  • yPlusHeight:图片定位的上下偏移参数,例如:图片左右居中,则y=0.5,yPlusWidth=-0.5。
  • opacity:图片透明度。
4.3.5
exportVideoClipsSplit(data: Array<{ coverUrl: string; duration: number; aspectRatio: PlayerAspectRatio; timeline: Timeline; recommend?: IProduceRecommend; }>) => Promise<void>;将选中Timeline的多个独立片段拆分为不同Timeline并导出,参数依次为:默认封面图、导出Timeline的时长、导出比例、导出的Timeline片段、视频合成的分辨率或码率的推荐数据。4.4.0
exportVideoClipsMerge(data: { coverUrl: string; duration: number; aspectRatio: PlayerAspectRatio; timeline: Timeline; recommend?:IProduceRecommend; }) => Promise<void>;将选中Timeline同一轨道的多个独立片段合成为一个Timeline并导出,参数依次为:默认封面图、导出Timeline的时长、导出比例、导出的Timeline片段、视频合成的分辨率或码率的推荐数据。4.4.0
disableMediaMarksboolean当前VOD云剪辑暂时还不支持媒资标记,如果需要隐藏媒资标记相关功能,请设置disableMediaMarks=true4.8.6

数据结构说明:

  • PlayerAspectRatio
    enum PlayerAspectRatio {
      w1h1 = '1:1',
      w2h1 = '2:1',
      w4h3 = '4:3',
      w3h4 = '3:4',
      w9h16 = '9:16',
      w16h9 = '16:9',
      w21h9 = '21:9',
    }
  • VoiceConfig
    interface VoiceConfig {
      volume: number; // 音量,取值0~100,默认值50
      speech_rate: number; // 语速,取值范围:-500~500,默认值:0
      pitch_rate: number; // 语调,取值范围:-500~500,默认值:0
      format?: string; // 输出文件格式,支持:PCM/WAV/MP3
    }
  • Media
    interface Media {
      mediaId: string;
      mediaType: 'video' | 'audio' | 'image';
      video?: { // materialType 为'video'时须传入此项
        title: string;
        coverUrl: string;
        duration: number;
        width?: number; // 视频源的宽度,用于合成的推荐分辨率,不传入则不会有推荐的分辨率
        height?: number; // 视频源的高度,用于合成的推荐分辨率,不传入则不会有推荐的分辨率
        bitrate?: number; // 视频源的码率,用于合成的推荐码率,不传入则不会有推荐的码率
        src?: string; // 当 useDynamicSrc 为 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; // 当 useDynamicSrc 为 true 时,src 可以不传
        coverUrl?: string; // 音频封面
      };
      image?: { // materialType 为'image'时须传入此项
        title: string;
        src?: string; // 当 useDynamicSrc 为 true 时,src 可以不传
        coverUrl?: string; // 图片预览图,在列表及轨道区会优先展示该字段,如果没有,则展示 src 字段,如果两者都没有则无法在轨道区预览
        width?: number; // 图片的宽度,用于合成的推荐分辨率,不传入则不会有推荐的分辨率
        height?: number; // 图片的高度,用于合成的推荐分辨率,不传入则不会有推荐的分辨率
      };
    }
  • StickerCategory
    interface StickerCategory {
      id: string; // 分类的 id
      name: string; // 分类的名称,调用者自行切换语言
    }
  • StickerResponse
    interface Sticker {
      mediaId: string;
      src: string;
    }
      
    interface StickerResponse {
      total: number;
      stickers: Sticker[];
    }
  • IProduceRecommend
    interface IProduceRecommend {
      width?: number;
      height?: number;
      bitrate?: number;
    }

init()示例代码

重要 Web SDK只负责界面交互,不会发起请求,您需要通过Web SDK调用请求逻辑。请求本身应该先发送给您自己的服务端,您自己的服务端再根据AccessKey信息(AccessKey ID和AccessKey Secret)转发给相关的阿里云OpenAPI。
// 注意,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('GetPlayInfo', { // https://help.aliyun.com/document_detail/436555.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/454953.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/436573.html
      callDialog({
        onSubmit: async (materials) => {
          if (!projectId) { // 如果没有 projectId,需要先创建工程,如果能确保有 projectId,则不需要该步
            const addRes = await request('AddEditingProject', { // https://help.aliyun.com/document_detail/454948.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: async (mediaId, mediaType) => {
    const res = await 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();
  },
  getEditingProject: async () => {
    if (projectId) {
      const res = await request('GetEditingProject', { // https://help.aliyun.com/document_detail/454952.html
        ProjectId: projectId
      });
      
      const timelineString = res.data.Project.Timeline;
   
      return {
        projectId,
        timeline: timelineString ? JSON.parse(timelineString) : undefined,
        modifiedTime: res.data.Project.ModifiedTime,
        title:res.data.Project.Title // 项目标题
      };
    }
    return {};
  },
  updateEditingProject: ({ coverUrl, duration, timeline, isAuto }) => new Promise((resolve, reject) => {
    request('UpdateEditingProject', { // https://help.aliyun.com/document_detail/454949.html
      ProjectId: projectId,
      CoverURL: coverUrl,
      Duration: duration,
      Timeline: JSON.stringify(timeline)
    }).then((res) => {
      if (res.code === '200') {
        // WebSDK 本身会进行自动保存,isAuto 则是告诉调用方这次保存是否自动保存,调用方可以控制只在手动保存时才展示保存成功的提示
        !isAuto && Message.success('保存成功');
        resolve();
      } else {
        reject();
      }
    });
  }),
  produceEditingProjectVideo: ({ coverUrl, duration = 0, aspectRatio, timeline, recommend }) => {
    return new Promise((resolve) => {
      callDialog({ // 调用方需要自己实现提交合成任务的界面,这里的 callDialog 只是一种示例
        onSubmit: async ({ fileName, format, bitrate, description }) => { // 假设提交合成任务的界面让你获得了这些数据
          // 先根据 fileName 和 format 拼接出存储的 mediaURL
          const mediaURL = `http://bucketName.oss-cn-hangzhou.aliyuncs.com/${fileName}.${format}`;
          // 根据 WebSDK 传入的预览比例来决定合成的宽高
          const width = aspectRatio === '16:9' ? 640 : 360;
          const height = aspectRatio === '16:9' ? 360 : 640;
          // 若视频、图片素材传入的长宽、码率等数据,那么该函数返回的数据中的 recommend 就会包含了根据所使用的视频、图片计算得到的推荐的分辨率和码率
          // recommend 数据结构可以查看 IProduceRecommend
          // 你可以在提交界面上展示推荐数据或者直接使用在提交接口的参数里
          const res = await request('ProduceEditingProjectVideo', { // https://help.aliyun.com/document_detail/454947.html
            OutputMediaConfig: JSON.stringify({
              mediaURL,
              bitrate: recommend.bitrate || bitrate,
              width: recommend.width || width,
              height: recommend.height || height
            }),
            OutputMediaTarget: 'oss-object',
            ProjectMetadata: JSON.stringify({ Description: description }),
            ProjectId: projectId,
            Timeline: JSON.stringify(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),
          // 源视频的宽高、码率等数据,用于推荐合成数据,不传入或是0时无推荐数据
          width: Number(fileBasicInfo.Width) || 0,
          height: Number(fileBasicInfo.Height) || 0,
          bitrate: Number(fileBasicInfo.Bitrate) || 0,
          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,
          // 图片的宽高等数据,用于推荐合成数据,不传入或是0时无推荐数据
          width: Number(fileBasicInfo.Width) || 0,
          height: Number(fileBasicInfo.Height) || 0,
        }
      }

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

相关参考

常见问题