实时预览与拖动播放头
大多数编辑器都应该用 attachPreviewCanvas() 显示主监看画面。它会处理 Canvas 像素尺寸、播放器帧、窗口缩放、过期请求取消和 ImageBitmap 释放。
连接主预览 Canvas
Section titled “连接主预览 Canvas”HTML:
<div class="monitor"> <canvas id="preview"></canvas></div>CSS:
.monitor { aspect-ratio: 16 / 9; background: #000;}
#preview { display: block; width: 100%; height: 100%;}TypeScript:
import { attachPreviewCanvas } from '@aelionsdk/sdk';
const canvas = document.querySelector<HTMLCanvasElement>('#preview')!;const preview = attachPreviewCanvas(session, canvas, { quality: 'adaptive', fit: 'contain', background: '#000000', pauseWhenHidden: true, renderOnResize: true, targetFrameMs: 1000 / 30, onFrame: frame => updateTimecode(frame.timeUs), onError: error => showPreviewError(error),});
await preview.render(0);await preview.render(0) 成功后,Canvas 应出现工程在 0 微秒处的画面。没有视频内容时会显示 Sequence 背景色,而不是自动显示文件封面。
这些选项怎么选
Section titled “这些选项怎么选”| 选项 | 建议初始值 | 说明 |
|---|---|---|
quality | adaptive | 根据近期渲染耗时在多个比例间调整 |
fit | contain | 完整显示画布;cover 会裁切,fill 会拉伸 |
pixelRatio | 省略 | 默认使用设备 DPR;大屏可限制到 1–2 来节省像素 |
pauseWhenHidden | true | 页面隐藏时暂停播放,回到前台再恢复 |
renderOnResize | true | Canvas 尺寸变化后重绘当前时间点 |
targetFrameMs | 1000 / 30 | 自适应质量的目标耗时,不是播放帧率 |
Canvas 的 CSS 大小和实际 canvas.width/height 是两回事。Controller 会用 CSS 尺寸乘 DPR 设置 backing store。不要在创建后反复手动覆盖 Canvas 的 width/height。
把指针坐标映射到工程画布
Section titled “把指针坐标映射到工程画布”onPointer 返回的 point 已经考虑 Canvas CSS 尺寸、DPR、contain/cover/fill
和黑边,坐标单位是 Project 像素:
const preview = attachPreviewCanvas(session, canvas, { fit: 'contain', onPointer: event => { if (!event.point.inside) return; selection.handlePointer({ type: event.type, pointerId: event.pointerId, buttons: event.buttons, x: event.point.x, y: event.point.y, }); },});已有 DOM 事件也可以单独转换:
canvas.addEventListener('pointermove', event => { const point = preview.toProjectPoint(event.clientX, event.clientY); if (point.inside) overlay.moveTo(point.x, point.y);});inside: false 表示指针位于 contain 产生的黑边或 Canvas 外部。Controller 只做
坐标换算,不替产品实现命中测试、选择、拖拽约束或 Transaction。
捕获 Canvas 视频流
Section titled “捕获 Canvas 视频流”浏览器支持 HTMLCanvasElement.captureStream() 时,可以复用同一个预览 Canvas
给实时协作、WebRTC 或临时录屏,不会建立第二条渲染链:
let stream: MediaStream;try { stream = preview.captureStream(30); await publishPreview(stream);} catch (error) { showUnsupportedCapture(error);}
function stopPublishing(): void { for (const track of stream.getTracks()) track.stop();}返回的流只有 Canvas 视频轨,不会自动包含 Session AudioWorklet 的声音。需要声音时
由应用显式组合自己的音频 MediaStreamTrack。captureStream() 也不是离线导出的
替代品:它受页面调度、预览画质和实时丢帧影响;生成交付文件仍应使用
session.export。
实现拖动播放头
Section titled “实现拖动播放头”简单版本可以直接在 range input 的 input 事件中请求画面:
scrubber.addEventListener('input', () => { const timeUs = Number(scrubber.value); void preview.render(timeUs);});每次 render() 都会取代尚未完成的旧请求。快速拖到 8 秒后,即使 3 秒那一帧解码得更晚,也不会覆盖当前画面。
复杂时间线通常每个 animation frame 只提交一次:
let pendingTimeUs = 0;let scheduled = false;
function requestScrub(timeUs: number): void { pendingTimeUs = timeUs; if (scheduled) return; scheduled = true;
requestAnimationFrame(() => { scheduled = false; void preview.render(pendingTimeUs); });}这会减少 UI 事件数量,但不替代 Controller 的过期请求取消。两层一起使用,既能合并 pointermove,也能处理已经发出的慢请求。
拖动与播放怎么配合
Section titled “拖动与播放怎么配合”常见产品行为是:
async function beginScrub(): Promise<void> { if (session.player.state === 'playing') await session.player.pause();}
function updateScrub(timeUs: number): void { requestScrub(timeUs);}
async function endScrub(timeUs: number): Promise<void> { await session.player.seek(timeUs);}拖动过程中只画单帧,松手时再重置播放器的音频时钟。不要在每个 pointermove 中调用 player.seek(),它需要清空并重新填充音频运行时。
Controller 默认会订阅 Player。调用 session.player.play() 后,播放帧会自动画到同一个 Canvas,不需要自己再建定时器。
在交互和审片之间切换画质
Section titled “在交互和审片之间切换画质”preview.setQuality('draft', 0.5); // 复杂拖动或低端设备preview.setQuality('full', 1); // 停帧审片preview.setQuality('adaptive'); // 恢复自动调节renderScale 必须大于 0 且不超过 1。缩小发生在文字、效果和合成之前,因此能实质减少渲染开销;导出不会继承这个设置,始终按完整尺寸执行。
默认自适应候选比例是 [1, 0.75, 0.5, 0.35]。可以根据产品目标修改:
const preview = attachPreviewCanvas(session, canvas, { quality: 'adaptive', adaptiveScales: [1, 0.67, 0.5],});缩略图不需要另建主 Preview Controller,可以直接请求单帧:
const abort = new AbortController();const result = await session.preview.renderFrame({ timeUs, quality: 'draft', renderScale: 0.25, signal: abort.signal,});
try { thumbnailContext.drawImage(result.bitmap, 0, 0);} finally { result.bitmap.close();}只为视口内缩略图发请求,并限制并发。滚出视口或时间线缩放变化时,调用 abort.abort() 取消旧任务。缩略图和主预览会竞争解码与 GPU 资源,不能为整条长时间线一次性生成所有帧。
完全接管播放器帧
Section titled “完全接管播放器帧”只有自定义 WebGL 呈现层时,才关闭 Controller 的 Player 订阅:
const preview = attachPreviewCanvas(session, canvas, { subscribePlayer: false,});
const unsubscribe = session.player.subscribe(frame => { try { drawWithCustomRenderer(frame.result.bitmap); } finally { frame.result.bitmap.close(); }});Player 只允许一个帧订阅者。接管之后,关闭 bitmap 和取消订阅都由你的代码负责。
看懂预览统计
Section titled “看懂预览统计”console.log(preview.snapshot());console.log(session.getStats().preview);cancelledFrames在快速拖动时升高是正常的;pending长时间为 true,说明当前帧一直没有完成;failedFrames增长时查看 Session diagnostic;workerPendingRequests持续增长通常表示生产请求太快或取消未生效;lastRenderScale可以解释画面为何暂时变糊。
组件卸载或切换工程时:
preview.dispose();它会取消当前请求,断开 ResizeObserver、visibility listener 和 Player 帧订阅。之后再调用 render() 会失败;新工程应创建新的 Controller。
如果画面正常但播放没有声音,继续看播放与音频。