From fc81e65a616dbd6e21bc6ebdc5fa47fe8b0caace Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Jul 2024 09:15:05 +0000 Subject: [PATCH 1/7] Release v0.13.6 --- .changeset/light-boats-taste.md | 6 ------ packages/av-canvas/CHANGELOG.md | 8 ++++++++ packages/av-canvas/package.json | 2 +- packages/av-cliper/CHANGELOG.md | 6 ++++++ packages/av-cliper/package.json | 2 +- packages/av-recorder/CHANGELOG.md | 7 +++++++ packages/av-recorder/package.json | 2 +- 7 files changed, 24 insertions(+), 9 deletions(-) delete mode 100644 .changeset/light-boats-taste.md diff --git a/.changeset/light-boats-taste.md b/.changeset/light-boats-taste.md deleted file mode 100644 index 8323d867..00000000 --- a/.changeset/light-boats-taste.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@webav/av-canvas': patch -'@webav/av-cliper': patch ---- - -fix: cant stop decode audio when reset diff --git a/packages/av-canvas/CHANGELOG.md b/packages/av-canvas/CHANGELOG.md index ebea5d41..bc9cfe66 100644 --- a/packages/av-canvas/CHANGELOG.md +++ b/packages/av-canvas/CHANGELOG.md @@ -1,5 +1,13 @@ # @webav/av-canvas +## 0.13.6 + +### Patch Changes + +- 737cb31: fix: cant stop decode audio when reset +- Updated dependencies [737cb31] + - @webav/av-cliper@0.13.6 + ## 0.13.5 ### Patch Changes diff --git a/packages/av-canvas/package.json b/packages/av-canvas/package.json index 6b164bf1..7da828b9 100644 --- a/packages/av-canvas/package.json +++ b/packages/av-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-canvas", - "version": "0.13.5", + "version": "0.13.6", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-cliper/CHANGELOG.md b/packages/av-cliper/CHANGELOG.md index 09ab1f7b..254936cc 100644 --- a/packages/av-cliper/CHANGELOG.md +++ b/packages/av-cliper/CHANGELOG.md @@ -1,5 +1,11 @@ # @webav/av-cliper +## 0.13.6 + +### Patch Changes + +- 737cb31: fix: cant stop decode audio when reset + ## 0.13.5 ### Patch Changes diff --git a/packages/av-cliper/package.json b/packages/av-cliper/package.json index 3ef49aa1..0250abf8 100644 --- a/packages/av-cliper/package.json +++ b/packages/av-cliper/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-cliper", - "version": "0.13.5", + "version": "0.13.6", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-recorder/CHANGELOG.md b/packages/av-recorder/CHANGELOG.md index 44288514..8da82355 100644 --- a/packages/av-recorder/CHANGELOG.md +++ b/packages/av-recorder/CHANGELOG.md @@ -1,5 +1,12 @@ # @webav/av-recorder +## 0.13.6 + +### Patch Changes + +- Updated dependencies [737cb31] + - @webav/av-cliper@0.13.6 + ## 0.13.5 ### Patch Changes diff --git a/packages/av-recorder/package.json b/packages/av-recorder/package.json index d34e4b62..d31f0169 100644 --- a/packages/av-recorder/package.json +++ b/packages/av-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-recorder", - "version": "0.13.5", + "version": "0.13.6", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ From 9d01c66c9addc0c32900fab183e1e5f775d01989 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jul 2024 10:54:25 +0000 Subject: [PATCH 2/7] Release v0.13.7 --- .changeset/real-shoes-drum.md | 6 ------ packages/av-canvas/CHANGELOG.md | 8 ++++++++ packages/av-canvas/package.json | 2 +- packages/av-cliper/CHANGELOG.md | 6 ++++++ packages/av-cliper/package.json | 2 +- packages/av-recorder/CHANGELOG.md | 7 +++++++ packages/av-recorder/package.json | 2 +- 7 files changed, 24 insertions(+), 9 deletions(-) delete mode 100644 .changeset/real-shoes-drum.md diff --git a/.changeset/real-shoes-drum.md b/.changeset/real-shoes-drum.md deleted file mode 100644 index dab9d2c0..00000000 --- a/.changeset/real-shoes-drum.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@webav/av-canvas': patch -'@webav/av-cliper': patch ---- - -chore: add timeout error log diff --git a/packages/av-canvas/CHANGELOG.md b/packages/av-canvas/CHANGELOG.md index bc9cfe66..4d1be9cc 100644 --- a/packages/av-canvas/CHANGELOG.md +++ b/packages/av-canvas/CHANGELOG.md @@ -1,5 +1,13 @@ # @webav/av-canvas +## 0.13.7 + +### Patch Changes + +- 0fe21e5: chore: add timeout error log +- Updated dependencies [0fe21e5] + - @webav/av-cliper@0.13.7 + ## 0.13.6 ### Patch Changes diff --git a/packages/av-canvas/package.json b/packages/av-canvas/package.json index 7da828b9..05679fb2 100644 --- a/packages/av-canvas/package.json +++ b/packages/av-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-canvas", - "version": "0.13.6", + "version": "0.13.7", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-cliper/CHANGELOG.md b/packages/av-cliper/CHANGELOG.md index 254936cc..50d59054 100644 --- a/packages/av-cliper/CHANGELOG.md +++ b/packages/av-cliper/CHANGELOG.md @@ -1,5 +1,11 @@ # @webav/av-cliper +## 0.13.7 + +### Patch Changes + +- 0fe21e5: chore: add timeout error log + ## 0.13.6 ### Patch Changes diff --git a/packages/av-cliper/package.json b/packages/av-cliper/package.json index 0250abf8..3b9eec7e 100644 --- a/packages/av-cliper/package.json +++ b/packages/av-cliper/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-cliper", - "version": "0.13.6", + "version": "0.13.7", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-recorder/CHANGELOG.md b/packages/av-recorder/CHANGELOG.md index 8da82355..40800947 100644 --- a/packages/av-recorder/CHANGELOG.md +++ b/packages/av-recorder/CHANGELOG.md @@ -1,5 +1,12 @@ # @webav/av-recorder +## 0.13.7 + +### Patch Changes + +- Updated dependencies [0fe21e5] + - @webav/av-cliper@0.13.7 + ## 0.13.6 ### Patch Changes diff --git a/packages/av-recorder/package.json b/packages/av-recorder/package.json index d31f0169..b033fa7f 100644 --- a/packages/av-recorder/package.json +++ b/packages/av-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-recorder", - "version": "0.13.6", + "version": "0.13.7", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ From fc6cfc7693103432b9607aa50b33060d6fee59fb Mon Sep 17 00:00:00 2001 From: hughfenghen Date: Tue, 9 Jul 2024 14:24:30 +0800 Subject: [PATCH 3/7] fix: gen thumbnails throw decode error --- .changeset/curvy-avocados-share.md | 5 +++ packages/av-cliper/src/clips/mp4-clip.ts | 44 +++++++++++++----------- 2 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 .changeset/curvy-avocados-share.md diff --git a/.changeset/curvy-avocados-share.md b/.changeset/curvy-avocados-share.md new file mode 100644 index 00000000..399cec7d --- /dev/null +++ b/.changeset/curvy-avocados-share.md @@ -0,0 +1,5 @@ +--- +'@webav/av-cliper': patch +--- + +fix: gen thumbnails throw decode error diff --git a/packages/av-cliper/src/clips/mp4-clip.ts b/packages/av-cliper/src/clips/mp4-clip.ts index 3567fdc6..b7f4e7fb 100644 --- a/packages/av-cliper/src/clips/mp4-clip.ts +++ b/packages/av-cliper/src/clips/mp4-clip.ts @@ -245,6 +245,8 @@ export class MP4Clip implements IClip { imgWidth = 100, opts?: Partial, ): Promise> { + await this.ready; + const { width, height } = this.#meta; const convtr = createVF2BlobConvtr( imgWidth, @@ -304,14 +306,14 @@ export class MP4Clip implements IClip { } else { const localFileReader = await this.#localFile.createReader(); // only decode key frame - const samples = await Promise.all( + const chunks = await Promise.all( this.#videoSamples .filter( (s) => !s.deleted && s.is_sync && s.cts >= start && s.cts <= end, ) .map((s) => sample2Chunk(s, EncodedVideoChunk, localFileReader)), ); - if (samples.length === 0) { + if (chunks.length === 0) { await localFileReader.close(); resolver(); return; @@ -322,7 +324,7 @@ export class MP4Clip implements IClip { output: (vf) => { cnt += 1; pushPngPromise(vf); - if (cnt === samples.length) { + if (cnt === chunks.length) { localFileReader.close(); resolver(); } @@ -330,10 +332,7 @@ export class MP4Clip implements IClip { error: Log.error, }); dec.configure(vc); - samples.forEach((c) => { - dec.decode(c); - }); - await dec.flush(); + decodeGoP(dec, chunks, {}); } }); } @@ -695,11 +694,14 @@ class VideoFrameFinder { this.#lastVfDur = chunks[0]?.duration ?? 0; decodeGoP(dec, chunks, { - softDecode: this.#downgradeSoftDecode, - onDowngradeSoft: () => { - this.#downgradeSoftDecode = true; - Log.warn('Downgrade to software decode'); - this.reset(); + onDecodingError: (err) => { + if (this.#downgradeSoftDecode) { + throw err; + } else { + this.#downgradeSoftDecode = true; + Log.warn('Downgrade to software decode'); + this.reset(); + } }, }); @@ -1110,14 +1112,13 @@ function splitAudioSampleByTime(audioSamples: ExtMP4Sample[], time: number) { return [preSlice, postSlice]; } -// 兼容 IDR 帧解码异常 +// 兼容解码错误 function decodeGoP( dec: VideoDecoder, chunks: EncodedVideoChunk[], opts: { idrFrameDowngrade?: boolean; - softDecode: boolean; - onDowngradeSoft: () => void; + onDecodingError?: (err: Error) => void; }, ) { let i = 0; @@ -1130,6 +1131,7 @@ function decodeGoP( i === 0 && err.message.includes('A key frame is required after configure') ) { + // 第一帧携带 SEI 信息会导致解码失败 const newChunk = removeNonIDRData(chunks[0]); if (newChunk == null) throw err; @@ -1147,12 +1149,12 @@ function decodeGoP( // windows 某些设备 flush 可能不会被 resolved,所以不能 await flush dec.flush().catch((err) => { if (!(err instanceof Error)) throw err; - if (err.message.includes('Decoding error')) { - if (opts.softDecode) { - throw err; - } else { - opts.onDowngradeSoft(); - } + if ( + err.message.includes('Decoding error') && + opts.onDecodingError != null + ) { + opts.onDecodingError(err); + return; } // reset 中断解码器,预期会抛出 AbortedError if (!err.message.includes('Aborted due to close')) { From 1465298d84be3e9f2fec2c75670a98b35d8e7d4f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jul 2024 06:25:58 +0000 Subject: [PATCH 4/7] Release v0.13.8 --- .changeset/curvy-avocados-share.md | 5 ----- packages/av-canvas/CHANGELOG.md | 7 +++++++ packages/av-canvas/package.json | 2 +- packages/av-cliper/CHANGELOG.md | 6 ++++++ packages/av-cliper/package.json | 2 +- packages/av-recorder/CHANGELOG.md | 7 +++++++ packages/av-recorder/package.json | 2 +- 7 files changed, 23 insertions(+), 8 deletions(-) delete mode 100644 .changeset/curvy-avocados-share.md diff --git a/.changeset/curvy-avocados-share.md b/.changeset/curvy-avocados-share.md deleted file mode 100644 index 399cec7d..00000000 --- a/.changeset/curvy-avocados-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@webav/av-cliper': patch ---- - -fix: gen thumbnails throw decode error diff --git a/packages/av-canvas/CHANGELOG.md b/packages/av-canvas/CHANGELOG.md index 4d1be9cc..fd7a80b8 100644 --- a/packages/av-canvas/CHANGELOG.md +++ b/packages/av-canvas/CHANGELOG.md @@ -1,5 +1,12 @@ # @webav/av-canvas +## 0.13.8 + +### Patch Changes + +- Updated dependencies [c7fcdcf] + - @webav/av-cliper@0.13.8 + ## 0.13.7 ### Patch Changes diff --git a/packages/av-canvas/package.json b/packages/av-canvas/package.json index 05679fb2..0a88fbe8 100644 --- a/packages/av-canvas/package.json +++ b/packages/av-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-canvas", - "version": "0.13.7", + "version": "0.13.8", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-cliper/CHANGELOG.md b/packages/av-cliper/CHANGELOG.md index 50d59054..f1459275 100644 --- a/packages/av-cliper/CHANGELOG.md +++ b/packages/av-cliper/CHANGELOG.md @@ -1,5 +1,11 @@ # @webav/av-cliper +## 0.13.8 + +### Patch Changes + +- c7fcdcf: fix: gen thumbnails throw decode error + ## 0.13.7 ### Patch Changes diff --git a/packages/av-cliper/package.json b/packages/av-cliper/package.json index 3b9eec7e..410c199a 100644 --- a/packages/av-cliper/package.json +++ b/packages/av-cliper/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-cliper", - "version": "0.13.7", + "version": "0.13.8", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ diff --git a/packages/av-recorder/CHANGELOG.md b/packages/av-recorder/CHANGELOG.md index 40800947..bbac84c7 100644 --- a/packages/av-recorder/CHANGELOG.md +++ b/packages/av-recorder/CHANGELOG.md @@ -1,5 +1,12 @@ # @webav/av-recorder +## 0.13.8 + +### Patch Changes + +- Updated dependencies [c7fcdcf] + - @webav/av-cliper@0.13.8 + ## 0.13.7 ### Patch Changes diff --git a/packages/av-recorder/package.json b/packages/av-recorder/package.json index b033fa7f..6a5a5528 100644 --- a/packages/av-recorder/package.json +++ b/packages/av-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@webav/av-recorder", - "version": "0.13.7", + "version": "0.13.8", "private": false, "repository": "https://github.com/bilibili/WebAV", "keywords": [ From 0976c7a6f185e515657ca2bac7b563d06481e4d0 Mon Sep 17 00:00:00 2001 From: hughfenghen Date: Tue, 9 Jul 2024 17:13:03 +0800 Subject: [PATCH 5/7] chore: update readme --- README.md | 11 ++++++----- README_CN.md | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a3e437e7..d0d6b89c 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ English | [中文](./README_CN.md) WebAV is an SDK for **creating/editing** video files on the web platform, built on WebCodecs. -### Key Features +### Features -- Cross-platform: Runs on Edge, Chrome browsers, and Electron -- High performance: 10-20 times faster than ffmpeg.wasm -- Small size: Approximately 50kb (MINIFIED + GZIPPED, without tree-shaking) -- Cost Reduction: By utilizing client-side computation entirely, server costs are reduced. +- Cross-platform: Supports running on Edge and Chrome browsers, as well as in Electron. +- Zero Cost: Fully utilizes client-side computation, eliminating server costs. +- Privacy and Security: No user data is uploaded. +- High Performance: 10 to 20 times faster than ffmpeg.wasm. +- Small Size: Approximately 50KB (MINIFIED + GZIPPED, without tree-shaking). _Compatible with Chrome 102+_ diff --git a/README_CN.md b/README_CN.md index fa6cb20d..bd6457c2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,12 +4,13 @@ WebAV 是一个在 Web 平台上**创建/编辑**视频文件的 SDK,基于 WebCodecs 构建。 -### 主要特性 +### 优势 - 跨平台:支持在 Edge、Chrome 浏览器,以及 Electron 中运行 -- 性能强:是 ffmpeg.wasm 的 10~20 倍 +- 零成本:完全使用客户端计算,无需服务器成本 +- 隐私安全:不会上传用户的任何数据 +- 高性能:是 ffmpeg.wasm 的 10~20 倍 - 体积小:约 50kb(MINIFIED + GZIPPED, 未 tree-shaking) -- 降成本:完全使用客户端计算,降低服务器成本。 _兼容 chrome 102+_ From 1319e06bf32330dbe34340da93ef4bf43b216673 Mon Sep 17 00:00:00 2001 From: hughfenghen Date: Tue, 9 Jul 2024 18:24:45 +0800 Subject: [PATCH 6/7] chore: update readme --- README.md | 1 + README_CN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index d0d6b89c..4dc38d9c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ WebAV is an SDK for **creating/editing** video files on the web platform, built - Zero Cost: Fully utilizes client-side computation, eliminating server costs. - Privacy and Security: No user data is uploaded. - High Performance: 10 to 20 times faster than ffmpeg.wasm. +- Easy to Extend: Developer-friendly for web developers, easily integrates with Canvas and WebAudio for custom functionality. - Small Size: Approximately 50KB (MINIFIED + GZIPPED, without tree-shaking). _Compatible with Chrome 102+_ diff --git a/README_CN.md b/README_CN.md index bd6457c2..3664de0e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -10,6 +10,7 @@ WebAV 是一个在 Web 平台上**创建/编辑**视频文件的 SDK,基于 We - 零成本:完全使用客户端计算,无需服务器成本 - 隐私安全:不会上传用户的任何数据 - 高性能:是 ffmpeg.wasm 的 10~20 倍 +- 易扩展:对 Web 开发者非常友好,能轻松与 Canvas、WebAudio 配合,实现自定义功能 - 体积小:约 50kb(MINIFIED + GZIPPED, 未 tree-shaking) _兼容 chrome 102+_ From 60fd9ed89c278baaa7739bd492cd8626f19b4d2c Mon Sep 17 00:00:00 2001 From: hughfenghen Date: Wed, 10 Jul 2024 16:53:01 +0800 Subject: [PATCH 7/7] fix: deocde timeout because many thumbnails task --- .changeset/fair-cycles-repair.md | 5 + .../src/clips/__tests__/mp4-clip.test.ts | 11 ++ packages/av-cliper/src/clips/mp4-clip.ts | 176 ++++++++++-------- 3 files changed, 117 insertions(+), 75 deletions(-) create mode 100644 .changeset/fair-cycles-repair.md diff --git a/.changeset/fair-cycles-repair.md b/.changeset/fair-cycles-repair.md new file mode 100644 index 00000000..92a84616 --- /dev/null +++ b/.changeset/fair-cycles-repair.md @@ -0,0 +1,5 @@ +--- +'@webav/av-cliper': patch +--- + +fix: deocde timeout because many thumbnails task diff --git a/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts b/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts index 88556f4b..879bea7c 100644 --- a/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts +++ b/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts @@ -55,6 +55,17 @@ test('thumbnails', async () => { clip.destroy(); }); +test('thumbnails aborted', async () => { + const clip = new MP4Clip((await fetch(mp4_bunny)).body!); + await clip.ready; + clip.thumbnails().catch((err) => { + expect((err as Error).message).toBe('generate thumbnails aborted'); + }); + await Promise.resolve(); + clip.thumbnails(100, { step: 1e6 }); + clip.destroy(); +}); + const mp4_bunny_1 = `//${location.host}/video/bunny_1.mp4`; test('clone mp4clip', async () => { diff --git a/packages/av-cliper/src/clips/mp4-clip.ts b/packages/av-cliper/src/clips/mp4-clip.ts index b7f4e7fb..07b55349 100644 --- a/packages/av-cliper/src/clips/mp4-clip.ts +++ b/packages/av-cliper/src/clips/mp4-clip.ts @@ -234,6 +234,7 @@ export class MP4Clip implements IClip { }); } + #thumbAborter = new AbortController(); /** * Generate thumbnails, default generate 100px width thumbnails by every key frame. * @@ -245,7 +246,13 @@ export class MP4Clip implements IClip { imgWidth = 100, opts?: Partial, ): Promise> { + this.#thumbAborter.abort(); + this.#thumbAborter = new AbortController(); + const aborterSignal = this.#thumbAborter.signal; + await this.ready; + const abortMsg = 'generate thumbnails aborted'; + if (aborterSignal.aborted) throw Error(abortMsg); const { width, height } = this.#meta; const convtr = createVF2BlobConvtr( @@ -254,87 +261,68 @@ export class MP4Clip implements IClip { { quality: 0.1, type: 'image/png' }, ); - return new Promise>(async (resolve) => { - const pngPromises: Array<{ ts: number; img: Promise }> = []; - const vc = this.#decoderConf.video; - if (vc == null) { - resolver(); - return; - } - - async function resolver() { - resolve( - await Promise.all( - pngPromises.map(async (it) => ({ - ts: it.ts, - img: await it.img, - })), - ), - ); - } - - function pushPngPromise(vf: VideoFrame) { - pngPromises.push({ - ts: vf.timestamp, - img: convtr(vf), - }); - } - - const { start = 0, end = this.#meta.duration, step } = opts ?? {}; - if (step) { - if ( - this.#decoderConf.video == null || - this.#videoSamples.length === 0 - ) { + return new Promise>( + async (resolve, reject) => { + let pngPromises: Array<{ ts: number; img: Promise }> = []; + const vc = this.#decoderConf.video; + if (vc == null || this.#videoSamples.length === 0) { resolver(); return; } - let cur = start; - // 创建一个新的 VideoFrameFinder 实例,避免与 tick 方法共用而导致冲突 - const videoFrameFinder = new VideoFrameFinder( - await this.#localFile.createReader(), - this.#videoSamples, - this.#decoderConf.video, - ); - while (cur <= end) { - const vf = await videoFrameFinder.find(cur); - if (vf) pushPngPromise(vf); - cur += step; + aborterSignal.addEventListener('abort', () => { + reject(Error(abortMsg)); + }); + + async function resolver() { + if (aborterSignal.aborted) return; + resolve( + await Promise.all( + pngPromises.map(async (it) => ({ + ts: it.ts, + img: await it.img, + })), + ), + ); } - videoFrameFinder.destroy(); - resolver(); - } else { - const localFileReader = await this.#localFile.createReader(); - // only decode key frame - const chunks = await Promise.all( - this.#videoSamples - .filter( - (s) => !s.deleted && s.is_sync && s.cts >= start && s.cts <= end, - ) - .map((s) => sample2Chunk(s, EncodedVideoChunk, localFileReader)), - ); - if (chunks.length === 0) { - await localFileReader.close(); - resolver(); - return; + + function pushPngPromise(vf: VideoFrame) { + pngPromises.push({ + ts: vf.timestamp, + img: convtr(vf), + }); } - let cnt = 0; - const dec = new VideoDecoder({ - output: (vf) => { - cnt += 1; - pushPngPromise(vf); - if (cnt === chunks.length) { - localFileReader.close(); - resolver(); - } - }, - error: Log.error, - }); - dec.configure(vc); - decodeGoP(dec, chunks, {}); - } - }); + const { start = 0, end = this.#meta.duration, step } = opts ?? {}; + if (step) { + let cur = start; + // 创建一个新的 VideoFrameFinder 实例,避免与 tick 方法共用而导致冲突 + const videoFrameFinder = new VideoFrameFinder( + await this.#localFile.createReader(), + this.#videoSamples, + vc, + ); + while (cur <= end && !aborterSignal.aborted) { + const vf = await videoFrameFinder.find(cur); + if (vf) pushPngPromise(vf); + cur += step; + } + videoFrameFinder.destroy(); + resolver(); + } else { + await thumbnailByKeyFrame( + this.#videoSamples, + this.#localFile, + vc, + aborterSignal, + { start, end }, + (vf, done) => { + pushPngPromise(vf); + if (done) resolver(); + }, + ); + } + }, + ); } async split(time: number) { @@ -1185,3 +1173,41 @@ function removeNonIDRData(chunk: EncodedVideoChunk) { } return null; } + +async function thumbnailByKeyFrame( + samples: ExtMP4Sample[], + localFile: OPFSToolFile, + decConf: VideoDecoderConfig, + abortSingl: AbortSignal, + time: { start: number; end: number }, + onOutput: (vf: VideoFrame, done: boolean) => void, +) { + const fileReader = await localFile.createReader(); + let cnt = 0; + const dec = new VideoDecoder({ + output: (vf) => { + cnt += 1; + const done = cnt === chunks.length; + onOutput(vf, done); + if (done) fileReader.close(); + }, + error: Log.error, + }); + abortSingl.addEventListener('abort', () => { + fileReader.close(); + dec.close(); + }); + + const chunks = await Promise.all( + samples + .filter( + (s) => + !s.deleted && s.is_sync && s.cts >= time.start && s.cts <= time.end, + ) + .map((s) => sample2Chunk(s, EncodedVideoChunk, fileReader)), + ); + if (chunks.length === 0 || abortSingl.aborted) return; + + dec.configure(decConf); + decodeGoP(dec, chunks, {}); +}