真文件的配对与逐张播放
前四讲用的都是合成素材。本页收真文件,把已有结论逐条兑现在自己相册导出的东西上。
1 · 三种到手形态
从设备导出的结果不止一种,取决于导出方与源系统:
| 到手的东西 | 来源 | 识别方式 |
|---|---|---|
两个同名文件(IMG_0001.HEIC + IMG_0001.MOV) |
iPhone 直接导出 | 按基名配对 |
| 一个 JPEG,尾部嵌着 MP4 | Android 的 Motion Photo | 扫 APP1 里的 XMP |
| 只有静图,或只有视频 | 传输中拆散,或导出方只给一半 | 配不上对 |
第一行的配对判据是基名相同。这条规则不来自任何规范,而是导出工具的惯例——真正的配对依据是双方元数据里的 asset identifier(见双文件与配对 §2),但那个字段藏在 Apple maker note 与 QuickTime 元数据里,读它要解 EXIF 与 ISO-BMFF 的 box 树。文件名是同一信息的廉价代理,代价是重命名过的文件会配不上。
第三行值得单独收下而不是丢弃:拆散是双文件设计最常见的失效方式,把它显式标出来比静默忽略有用。
2 · 每一半各自探测
判断能否解码,唯一可靠的做法是真去加载:静图交给一个 Image,看 load 还是 error;视频交给一个 <video>,看 loadedmetadata 还是 error。不问 canPlayType,理由见解得开哪一半 §5。
两半的结果是独立的,四种组合都会出现。其中最常见的一种是 Chrome 上的「静图解不开、视频可播」:iPhone 导出的静图是 HEIC,Chrome 没有 HEIF 解码器。
这一格有一条退路:既然视频那一半解得开,就从视频里抽一帧当封面。currentTime 拨到锚点(单文件封装里读 MotionPhotoPresentationTimestampUs,双文件里没有现成值就取中点),等 seeked 后 drawImage 到
canvas。得到的封面在内容上正是静图那一帧的近似,只是画质经过一次有损重编码。
警示 · 抽帧的前提是画布未被污染:视频与页面同源,或跨域但带 crossorigin 且服务端放行 CORS。本页的文件来自 <input type="file">,包成 blob URL 后同源,canvas 不会被污染;两个条件都不满足时,drawImage 之后 toDataURL 会抛
SecurityError。
3 · 逐张播放的顺序
顺序播放是一串串行的等待:播一张,等它 ended,再播下一张。有两处需要兜底。
ended 不保证到达。视频在播放中途解码失败、或被浏览器暂停,都会让等待悬在那里,整条队列卡死。所以除了监听 ended 与 error,还要挂一个按时长估算的超时——图 2-1 取「时长加一秒」。
另一处是 play() 的拒绝。逐张播放由按钮触发,静音让整条队列不依赖任何激活状态:muted 的自动播放在主流配置下无需手势资格(Safari 的站点级「永不自动播放」与 iOS 低电量模式仍会拒绝,故 play() 的 promise 照样要 catch)。至于有声播放,取决于各浏览器的 autoplay
policy——Chrome 在用户与本域交互过之后即放行,用户点击产生的是黏性激活、不会在 await 之后失效;Safari 口径更严。跨浏览器不可依赖(核对于 2026-08)。
4 · 可当场核对的三条
选入真文件之后,前几讲的结论都能对照:
- HEIC 静图在 Chrome 里解不开,且与 MIME 标签无关(解得开哪一半 §4)
- 同一批文件里的
.MOV照常可播,容器不是障碍(同上 §2、§3) - 单文件 Motion Photo 从末尾回退
Item:Length就能切出视频(双文件与配对 §3)
第 1 条与第 2 条在同一张卡片上同时出现时,那个不对称最直观:一张 Live Photo 送进 Chrome,会动的那一半好端端的,不动的那一半反而是缺口。