智能生产制作提供单独预览Timeline的能力,您可以根据实际需求在前端页面文件中引入。通过阅读本文,您可以了解如何接入预览组件Web SDK。
使用说明
本文中引入的预览组件Web SDK的版本号5.2.2(仅供参考),从5.0.0开始,你需要申请License授权才能使用剪辑Web SDK。获取最新的版本信息,请参见视频剪辑工程——帮助信息。
License授权方式有2种:
方式1(推荐):购买企业标准版(售价3万元,支持进行官网价5万元的IMS任意功能消费抵扣)及以上订阅资源包,会赠送webSDK License,购买说明请参见:计费概述;
方式2:其他订阅方式,您可以单独购买webSDK license,定价2万/年,可工单联系官方获取购买链接;
运行环境要求
Web SDK预览组件依赖于浏览器的安全策略,因此必须通过HTTP服务访问页面(例如:http://localhost:7001/)。请勿直接双击打开本地HTML文件。
操作步骤
- 引入预览组件Web SDK。 - 在项目前端页面文件中的 - <head>标签处引入预览组件Web SDK的CSS文件。- <head> <link rel="stylesheet" href="https://g.alicdn.com/thor-server/video-editing-websdk/5.2.2/player.css"> </head>- 在 - <body>标签处添加一个用以挂载预览界面的- <div>节点,并在- <body>标签末尾添加引入Web SDK的JS文件,同时添加一个用以调用Web SDK的- <script>节点。- <body> <div id="player-wrapper" style="height:500px"></div> // 您可以根据需要改变 container 高度 <script src="https://g.alicdn.com/thor-server/video-editing-websdk/5.2.2/player.js"></script> <script> // 调用 SDK 的代码放在这里 </script> </body>
- 初始化预览组件Web SDK。 - const player = new window.AliyunTimelinePlayer({ container: document.querySelector('#player-wrapper'), licenseConfig: { rootDomain: "", // license使用的根域名,例如abc.com licenseKey: "", // 申请的licenseKey,没有配置licenseKey,在预览时会出现水印,没有配置license的情况下,只能在localhost的域名下预览 }, timeline: { // 可选 VideoTracks: [], // 省略 VideoTracks 的内容 AudioTracks: [], // 省略 AudioTracks 的内容 AspectRatio: '16:9', }, getMediaInfo: (mediaId) => { return Promise.resolve('https://example.com/url/for/this/media.mp4'); }, });- 初始化实例 - new window.AliyunTimelinePlayer(PlayerInitConfig)中的参数- PlayerInitConfig说明如下:- interface PlayerInitConfig { licenseConfig?: { rootDomain?: string; licenseKey?: string; }; // license配置 getTimelineMaterials?: (params: TimelineMaterial[]) => Promise<InputMedia[]>; // timeline媒资素材 mode?: 'component' | 'player'; // 播放器组件样式:component为纯组件 componentClass?: string; container: Element;// 预览组件的容器,HTML 元素 locale?: Locales;// 界面语言,可选 'zh-CN' | 'en-US',默认 'zh-CN' controls?: boolean; // 是否显示底部的播控栏,默认 true timeline?: AliyunTimeline; // WebSDK 生成的 timeline 对象,需要注意与 OpenAPI 的 timeline 不完全一致 playbackRate?: number;// 初始化时的播放速度,默认是 1。注意,该值的范围与浏览器本身对 video 的 playbackRate 限制有关 aspectRatio?: string;// 初始化时的画面比例,默认是 '16:9'。可自定义宽高比,以英文冒号相连。建议与制作 AliyunTimeline 时的画面比例一致 minWidth?: number | string;// 指定播放器最小宽度 customFontList?: CustomFontItem[]; // 自定义字体列表 getMediaInfo?: ( mediaId: string, mediaType: MediaType, mediaOrigin?: MediaOrigin | undefined, inputUrl?: string | undefined, ) => Promise<string | DynamicSrcObj>; }// 与 https://help.aliyun.com/document_detail/453478.html 中的 getDynamicSrc 一样,通过 mediaId 返回其对应的带鉴权的播放 URL
- AliyunTimelinePlayer类的定义如下:- class AliyunTimelinePlayer { static getSubtitleEffectColorStyles(): Array<{ key: string; cover: string; }>; // 获取花字列表 static getSubtitleBubbles(): Array<{ key: string; cover: string; }>;// 获取气泡字列表 static setDefaultLocale(locale?: Locales): void; // 设置默认语言 static getDefaultFontList(): CustomFontItem[]; // 获取默认字体列表 static getVideoEffects(): Array<{ subType: string; cover: string; name: string; title: string; category: string | undefined; }>; // 获取特效列表 static getVideoFilters(): Array<{ subType: string; cover: string; name: string; title: string; category: string | undefined; }>;// 获取滤镜列表 static getVideoTransitions(): Array<{ subType: string; cover: string; name: string; title: string; category: string | undefined; }>;// 获取转场列表 static parseTimeline( timeline: string | IServerTimeline, // 需要传入画布的大小,一般是Timeline中FECanvas的Width,Height的值 options: { outputWidth: number; outputHeight: number } = { outputWidth: 800, outputHeight: 450 }, ):ParsedResult; // 后端Timeline在前端进行预览转换 constructor(config: PlayerInitConfig); play(): void; // js 控制播放 pause(): void; // js 控制暂停 destroy(): void; // 销毁实例 on(eventName: string, callback: Function): any; // 监听事件 once(eventName: string, callback: Function): any; // 监听事件一次 off(eventName: string): any; // 取消监听事件 get event$(): IObservable<EventData<any>>; //事件流,通过subscribe订阅所有事件,参考rxjs订阅逻辑 get version(): string | undefined; // 版本 get duration(): number;// 时长 get timeline(): any;// 获取当前Timeline set timeline(timeline: any); // 设置当前Timeline //(websdk5.2.2以上版本 )当前场景宽度,计算x,y绝对值的时候使用 get stageWidth(): number; //(websdk5.2.2以上版本 ) 当前场景高度,计算x,y绝对值的时候使用 get stageHeight(): number; //(websdk5.2.2以上版本 ) 设置Timeline,自动parseTimeline setTimeline(timeline: any, autoParse?: boolean): Promise<void>; get currentTime(): number;// 获取播放时间 set currentTime(currentTime: number); // 设置播放时间 get controls(): boolean; // 获取当前控件显示状态 set controls(show: boolean); // 隐藏当前控件 get aspectRatio(): string; // 获取当前组件比例 set aspectRatio(ratio: string); // 设置当前组件比例,例如16:9 get playbackRate(): number; // 获取播放器速率 set playbackRate(speed: number); // 设置播放器速率 setFontList(value: CustomFontItem[]): Promise<void>; // 设置自定义字体 //(websdk5.2.2以上版本 ) 监控轨道数据变化 watchTrack(handler?: (tracks: ExportTrack[]) => void): () => void; //(websdk5.2.2以上版本 ) 删除当前轨道 removeTrack(id: number): void; //(websdk5.2.2以上版本 ) 设置轨道属性 setTrack(id: number, options: { visible?: boolean; mainTrack?: boolean; }): void; // 新增轨道(websdk5.2.2以上版本 ) addTrack(track: {id: number;type: TrackType; clips: ExportClip[];visible: boolean;mainTrack?: boolean;} ): void; // 删除轨道素材(websdk5.2.2以上版本 ) removeClip(id: number): void; // 获取轨道素材详细配置(websdk5.2.2以上版本 ) getClip(id: number): ServerClip; // 新增轨道素材 (websdk5.2.2以上版本 ) addClip(clip: ServerClip): void; // 设置素材入点 (websdk5.2.2以上版本 ) setClipTimelineIn(id: number, timelineIn: number): void; // 设置素材出点 (websdk5.2.2以上版本 ) setClipTimelineOut(id: number, timelineOut: number): void; // 更新素材 (websdk5.2.2以上版本 ) updateClip(id: number, handle: (clip: ServerClip) => ServerClip): void; // 监听素材变化 (websdk5.2.2以上版本 ) watchClip(id: number, handle: (clip: ServerClip | null) => void): () => void; // 转换为后端Timeline (websdk5.2.2以上版本 ) toBackendTimeline(): { duration: number; timeline: any; }; // 查询当前轨道信息 (websdk5.2.2以上版本 ) queryTracks(handler: (track: ExportTrack) => boolean): ExportTrack[]; // 查询当前素材信息 (websdk5.2.2以上版本 ) queryClips(handler: (clip: ExportClip, track: ExportTrack) => boolean): ExportClip[]; // 高亮素材,让素材可以自由移动 (websdk5.2.2以上版本 ) focusClip(id: number, autoSeek?: boolean): void; // 取消素材高亮 (websdk5.2.2以上版本 ) blurClip(): void; } // watchTrack导出的clip,可以通过getClip获取该clip的具体属性 interface ExportClip { id: number; type: MaterialType; timelineIn: number; timelineOut: number; } // watchTrack导出的Track interface ExportTrack { id: number; type: TrackType; clips: ExportClip[]; visible: boolean; mainTrack?: boolean; } // 自定义字体接口 interface CustomFontItem { key: string; // 字体唯一标识 name?: string; // 展示的名字 url: string; // 字体地址 urlType?: 'dynamic' | 'static'; // 如果url需要动态从getMedinfo中获取,填写dynamic否则填static } // 播放器状态 enum PLAYER_STATE { PLAYING = 0, PAUSED = 1, STALLED = 2, ENDED = 3, BROKEN = 4 } //前端解析Timeline结果 interface ParsedResult { success: boolean; // 解析是否成功 logs: any[]; timeline: IServerTimeline; // Timeline解析结果 toBackendTimeline: (fTimeline: IServerTimeline) => IServerTimeline; //前端Timeline转换为服务端Timeline mediaMap: ParsedMediaMap; output: { width: number; height: number; }; fontList: Array<{ key: string; url: string }>; } type ParsedMediaMap = { [key: string]: { mediaType: string; mediaId: string; mediaUrl?: string; width?: number; height?: number; duration: number; }; } type InputMedia = (InputVideo | InputAudio | InputImage); interface InputVideo { mediaId: string; mediaIdType?: MediaIdType; mediaType: 'video'; video: { duration: number; }; } interface InputAudio { mediaId: string; mediaIdType?: MediaIdType; mediaType: 'audio'; audio: { duration: number; }; } interface InputImage { mediaId: string; mediaIdType?: MediaIdType; mediaType: 'image'; image: { coverUrl?: string; }; } type MediaIdType = 'mediaId' | 'mediaURL';
 
- 释放实例。 - 使用 - destroy方法可以释放实例,释放后容器为空元素。- player.destroy();
示例代码
本文中的示例为完整示例,实现一个功能:创建一个带自定义字体,转场,特效的Timeline,可编辑字幕的预览器。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      rel="stylesheet"
      href="https://g.alicdn.com/thor-server/video-editing-websdk/5.2.2/player.css"
    />
    <script src="https://g.alicdn.com/thor-server/video-editing-websdk/5.2.2/player.js"></script>
    <title>云剪预览播放器</title>
  </head>
  <body>
    <div id="root" style="width: 820px;margin: 30px auto;" >
      <div id="player" style="width: 800px; height: 450px"></div>
      <div style="margin-top: 16px">
        <div>
          <label>ID</label>   <br/>
          <input id="subtitle" value="123" />
          <br/>
          <label>内容</label>   <br/>
          <textarea id="content"  rows="4" >自定义编辑字幕</textarea>
        </div>
        <button id="addSubtitle">添加字幕</button>
        <button id="updateSubtitle">修改字幕</button>
        <button id="moveSubtitle">移动字幕</button>
        <button id="removeSubtitle">删除字幕</button>
      </div>
    </div>
    <script type="text/javascript">
      const timelineDemo = {
        Version: 1,
        SdkVersion: "4.12.2",
        VideoTracks: [
          {
            // 视频轨道
            Id: 1,
            Type: "Video",
            Visible: true,
            Disabled: false,
            Count: 1,
            VideoTrackClips: [
              {
                // 图片
                Id: 3,
                TrackId: 1,
                Type: "Image",
                MediaURL:
                  "https://img.alicdn.com/imgextra/i4/O1CN01TDN7Gw1SCgVv61T4s_!!6000000002211-0-tps-1920-1046.jpg",
                Title: "test.png",
                X: 0,
                Y: 0,
                Width: 1,
                Height: 1,
                TimelineIn: 0,
                TimelineOut: 5,
                Duration: 5,
                VirginDuration: 5,
              },
              {
                // 视频
                Id: 4,
                TrackId: 1,
                Type: "Video",
                MediaURL:
                  "https://ice-pub-media.myalicdn.com/vod-demo/%E6%9C%80%E7%BE%8E%E4%B8%AD%E5%9B%BD%E7%BA%AA%E5%BD%95%E7%89%87-%E6%99%BA%E8%83%BD%E5%AD%97%E5%B9%95.mp4",
                X: 0,
                Y: 0,
                Width: 1,
                Height: 1,
                TimelineIn: 4, // 转场的入点(TimelineIn)需要在上一个片段出点(TimelineOut)以前
                TimelineOut: 9,
                Duration: 5,
                VirginDuration: 5,
                Effects: [
                  {
                    // 转场
                    Type: "Transition",
                    Id: 13,
                    Name: "transitions.directional",
                    SubType: "directional",
                    Duration: 1, // 转场时长=上一个片段出点(TimelineOut)-这个片段的入点(TimelineIn)
                    From: 3, // 上一个片段的ID
                  },
                ],
              },
            ],
          },
          {
            // 特效轨道
            Id: 2,
            Type: "Effect",
            Visible: true,
            Disabled: false,
            Count: 1,
            VideoTrackClips: [
              {
                Type: "VFX",
                Id: 2,
                TrackId: 1,
                SubType: "heartfireworks",
                TimelineIn: 0,
                TimelineOut: 5,
                Duration: 5,
                X: 0,
                Y: 0,
                Width: 0,
                Height: 0,
              },
            ],
          },
        ],
        AudioTracks: [],
        SubtitleTracks: [
          {
            Id: 2,
            Type: "Text",
            Visible: true,
            Disabled: false,
            Count: 1,
            SubtitleTrackClips: [
              {
                Id: 1,
                TrackId: 2,
                Type: "Text",
                X: 0,
                Y: 0,
                TimelineIn: 0,
                TimelineOut: 2,
                Duration: 2,
                VirginDuration: 2,
                FontSize: 30,
                FontColor: "#333333",
                FontColorOpacity: 1,
                Content: "FontUrl自定义字体",
                Alignment: "BottomCenter",
                // 自定义字体
                FontUrl:
                  "https://ice-pub-media.myalicdn.com/mts-fonts/%E7%AB%99%E9%85%B7%E6%96%87%E8%89%BA%E4%BD%93.ttf",
              },
            ],
          },
        ],
        AspectRatio: "16:9",
        From: "websdk",
        FECanvas: {
          Width: 800,
          Height: 450,
        },
        FEConfig: {
          AutoProportion: "681:383",
        },
      };
      const player = new window.AliyunTimelinePlayer({
        container: document.getElementById("player"),
        getMediaInfo: async (mediaId, mediaType, mediaOri) => {
          console.log(">>>", mediaId, mediaType, mediaOri);
          // 如果直接是mediaUrl的timeline,公网可访问,直接返回预览地址就可以
          return mediaId;
          /**
           * // 动态请求地址参考如下代码
           *     if (mediaType === "font") {
        params.InputURL = InputURL;
        delete params.MediaId;
      }
      if (mediaOrigin === "mediaURL") {
        params.InputURL = mediaId;
        delete params.MediaId;
      }
      const apiName =
        mediaOrigin === "public" ? "GetPublicMediaInfo" : "GetMediaInfo";
      return request(apiName, {
        // https://help.aliyun.com/document_detail/197842.html
        MediaId: mediaId,
      })
        .then((res) => {
          // 注意,这里仅作为示例,实际中建议做好错误处理,避免如 FileInfoList 为空数组时报错等异常情况
          const fileInfoList = get(res, "data.MediaInfo.FileInfoList", []);
          let mediaUrl, maskUrl;
          let sourceFile = fileInfoList.find((item) => {
            return item?.FileBasicInfo?.FileType === "source_file";
          });
          if (!sourceFile) {
            sourceFile = fileInfoList[0];
          }
          const maskFile = fileInfoList.find((item) => {
            return (
              item.FileBasicInfo &&
              item.FileBasicInfo.FileUrl &&
              item.FileBasicInfo.FileUrl.indexOf("_mask") > 0
            );
          });
          if (maskFile) {
            maskUrl = get(maskFile, "FileBasicInfo.FileUrl");
          }
          mediaUrl = get(sourceFile, "FileBasicInfo.FileUrl");
          const codec = get(sourceFile, "VideoStreamInfoList[0].CodecName");
          return {
            url: mediaUrl,
            codec,
            maskUrl,
          };
        })
        .catch((ex) => {
          // 外链地址兜底逻辑
          if (mediaOrigin === "mediaURL") {
            return mediaId;
          }
        });
           *
           * **/
        },
      });
      // 预览前端Timeline,setTimeline方法会自动解析MediaUrl和字体文件
      player.setTimeline(timelineDemo);
      // 可以通过toBackendTimeline转换为后端Timeline
      console.log("toBackendTimeline", player.toBackendTimeline());
      document.getElementById("addSubtitle").addEventListener("click", () => {
        player.addClip({
          Id: Number(document.getElementById('subtitle').value),
          TrackId: 2,
          Type: "Text",
          X: 0,
          Y: 0,
          TimelineIn: player.currentTime,
          TimelineOut: player.currentTime+2,
          Duration: 2,
          VirginDuration: 2,
          FontSize: 30,
          FontColor: "#333333",
          FontColorOpacity: 1,
          Content: document.getElementById('content').value,
          Alignment: "TopCenter",
          // 自定义字体
          FontUrl:
            "https://ice-pub-media.myalicdn.com/mts-fonts/%E7%AB%99%E9%85%B7%E6%96%87%E8%89%BA%E4%BD%93.ttf",
        });
      });
      document.getElementById("updateSubtitle").addEventListener("click", () => {
        player.updateClip(Number(document.getElementById('subtitle').value),(item)=>{
          return Object.assign({},item,{Content: document.getElementById('content').value})
        });
      });
      document.getElementById("moveSubtitle").addEventListener("click", () => {
        player.focusClip( Number(document.getElementById('subtitle').value));
      });
      document.getElementById("removeSubtitle").addEventListener("click", () => {
        player.blurClip();
        player.removeClip( Number(document.getElementById('subtitle').value));
      });
      // 监听轨道变化
      player.watchTrack((tracks)=>{
         console.log('timeline变化:');
         console.log(JSON.stringify(tracks,null,4));
      });
    </script>
  </body>
</html>
对于复杂的预览组件使用场景,例如编辑轨道、字幕、视频特效、转场、位置、宽高以及自定义播放器UI等功能,可以参考以下开源DEMO。示例的部署方法请参见Demo体验。

常见问题
如何更新timeline?
在创建AliyunTimelinePlayer的实例后,通过赋值即可更新timeline。
player.timeline = {....}如果出现缓存导致的问题,可以尝试先赋空值,清空timeline的内容,再赋值为新的timeline。
player.timeline = {} // 赋空值,清空timeline的内容预览字体大小和合成的视频不一致?
如果合成时字体变小,须确保timeline中包含FECanvas字段,FECanvas字段表示预览器的分辨率,在合成时服务端会根据这个分辨率及输出的分辨率对字体进行缩放,常见FECanvas分辨率如下:
//16:9
FECanvas: {Width: 800, Height: 450}
//9:16
FECanvas: {Width: 253.125, Height: 450}预览播放器内置素材列表?
// 获取字体列表
AliyunTimelinePlayer.getDefaultFontList();
// 获取花字列表
AliyunTimelinePlayer.getSubtitleEffectColorStyles(); 
// 获取气泡字列表
AliyunTimelinePlayer.getSubtitleBubbles();
// 获取特效列表
AliyunTimelinePlayer.getVideoEffects(); 
// 获取滤镜列表
AliyunTimelinePlayer.getVideoFilters();
// 获取转场列表
AliyunTimelinePlayer.getVideoTransitions();传参时需要保留空格,例如"Font":"Alibaba PuHuiTi", 。
预览播放器组件是否支持多实例?
当前预览播放器由于内部实现的限制,暂时仅支持单实例模式。
前端报错player.js:27 TypeError: Cannot read properties of undefined (reading 'GLctx')?
出现这种错误一般是没有打开浏览器硬件加速导致的,请确保浏览器内核版本为Chrome且版本号大于80,并在浏览器上打开硬件加速。
当前Web SDK只支持Chrome内核浏览器。
为什么预览组件中的文字不显示?
页面是否通过HTTP服务(如 http://localhost:8080)打开,而非直接本地文件(file://协议)。