From d78292cf29f8f2e3d38f65255e65864fa9df0089 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 22 Jan 2025 18:46:39 +0100 Subject: [PATCH] `@remotion/media-parser`: `parseAndDownloadMedia()` done --- .../media-parser/src/internal-parse-media.ts | 8 ++-- .../src/parse-and-download-media.ts | 6 ++- packages/media-parser/src/perform-seek.ts | 28 +++++------ .../media-parser/src/state/parser-state.ts | 8 +--- .../src/test/parse-and-download-video.test.ts | 47 ++++++++++++++++++- packages/media-parser/src/writers/node.ts | 13 ++--- packages/media-parser/src/writers/writer.ts | 4 +- packages/webcodecs/src/convert-media.ts | 2 +- .../iso-base-media/create-iso-base-media.ts | 6 +-- .../create/matroska/create-matroska-media.ts | 6 +-- packages/webcodecs/src/create/media-fn.ts | 2 +- .../webcodecs/src/create/wav/create-wav.ts | 6 +-- .../writers/buffer-implementation/writer.ts | 10 ++-- packages/webcodecs/src/writers/web-fs.ts | 10 ++-- 14 files changed, 101 insertions(+), 55 deletions(-) diff --git a/packages/media-parser/src/internal-parse-media.ts b/packages/media-parser/src/internal-parse-media.ts index 7367ce3d43..af2d1ec754 100644 --- a/packages/media-parser/src/internal-parse-media.ts +++ b/packages/media-parser/src/internal-parse-media.ts @@ -128,7 +128,7 @@ export const internalParseMedia: InternalParseMedia = async function < }); }; - const checkIfDone = () => { + const checkIfDone = async () => { const startCheck = Date.now(); const hasAll = hasAllInfo({ fields, @@ -149,7 +149,7 @@ export const internalParseMedia: InternalParseMedia = async function < if (state.iterator.counter.getOffset() === contentLength) { Log.verbose(logLevel, 'Reached end of file'); - state.discardReadBytes(true); + await state.discardReadBytes(true); return true; } @@ -160,7 +160,7 @@ export const internalParseMedia: InternalParseMedia = async function < triggerInfoEmit(); let iterationWithThisOffset = 0; - while (!checkIfDone()) { + while (!(await checkIfDone())) { if (signal?.aborted) { throw new Error('Aborted'); } @@ -247,7 +247,7 @@ export const internalParseMedia: InternalParseMedia = async function < } const timeFreeStart = Date.now(); - state.discardReadBytes(false); + await state.discardReadBytes(false); timeFreeingData += Date.now() - timeFreeStart; } diff --git a/packages/media-parser/src/parse-and-download-media.ts b/packages/media-parser/src/parse-and-download-media.ts index 0f76d5139a..6b0d815186 100644 --- a/packages/media-parser/src/parse-and-download-media.ts +++ b/packages/media-parser/src/parse-and-download-media.ts @@ -7,7 +7,7 @@ export const parseAndDownloadMedia: ParseAndDownloadMedia = async (options) => { filename: 'hmm', mimeType: 'shouldnotmatter', }); - return internalParseMedia({ + const returnValue = await internalParseMedia({ fields: options.fields ?? null, logLevel: options.logLevel ?? 'info', mode: 'download', @@ -49,4 +49,8 @@ export const parseAndDownloadMedia: ParseAndDownloadMedia = async (options) => { signal: options.signal ?? undefined, src: options.src, }); + + await content.finish(); + + return returnValue; }; diff --git a/packages/media-parser/src/perform-seek.ts b/packages/media-parser/src/perform-seek.ts index 05dcb4e03e..58023f2dc7 100644 --- a/packages/media-parser/src/perform-seek.ts +++ b/packages/media-parser/src/perform-seek.ts @@ -24,16 +24,10 @@ export const performSeek = async ({ mode, contentLength, } = state; - const skippingAhead = seekTo > iterator.counter.getOffset(); - if (mode === 'download' && !skippingAhead) { - throw new Error( - `Cannot seek backwards in download mode. Current position: ${iterator.counter.getOffset()}, seekTo: ${seekTo}`, - ); - } - if (!skippingAhead && !supportsContentRange) { + if (seekTo <= iterator.counter.getOffset()) { throw new Error( - 'Content-Range header is not supported by the reader, but was asked to seek', + `Seeking backwards is not supported. Current position: ${iterator.counter.getOffset()}, seekTo: ${seekTo}`, ); } @@ -41,10 +35,7 @@ export const performSeek = async ({ throw new Error(`Unexpected seek: ${seekTo} > ${contentLength}`); } - if ( - skippingAhead && - iterator.counter.getOffset() + iterator.bytesRemaining() >= seekTo - ) { + if (iterator.counter.getOffset() + iterator.bytesRemaining() >= seekTo) { Log.verbose( logLevel, `Skipping over video data from position ${iterator.counter.getOffset()} -> ${seekTo}. Data already fetched`, @@ -53,7 +44,7 @@ export const performSeek = async ({ return currentReader; } - if (skippingAhead && !supportsContentRange) { + if (!supportsContentRange) { Log.verbose( logLevel, `Skipping over video data from position ${iterator.counter.getOffset()} -> ${seekTo}. Fetching but not reading all the data inbetween because Content-Range is not supported`, @@ -62,6 +53,15 @@ export const performSeek = async ({ return currentReader; } + if (mode === 'download') { + Log.verbose( + logLevel, + `Skipping over video data from position ${iterator.counter.getOffset()} -> ${seekTo}. Fetching but not reading all the data inbetween because in download mode`, + ); + iterator.discard(seekTo - iterator.counter.getOffset()); + return currentReader; + } + const time = Date.now(); Log.verbose( logLevel, @@ -75,7 +75,7 @@ export const performSeek = async ({ signal, }); iterator.skipTo(seekTo); - state.discardReadBytes(true); + await state.discardReadBytes(true); Log.verbose( logLevel, diff --git a/packages/media-parser/src/state/parser-state.ts b/packages/media-parser/src/state/parser-state.ts index 4baeadf0be..68a8e17d96 100644 --- a/packages/media-parser/src/state/parser-state.ts +++ b/packages/media-parser/src/state/parser-state.ts @@ -83,18 +83,14 @@ export const makeParserState = ({ const mp3Info = makeMp3State(); const images = imagesState(); - const discardReadBytes = (force: boolean) => { + const discardReadBytes = async (force: boolean) => { const {bytesRemoved, removedData} = iterator.removeBytesRead(force, mode); if (bytesRemoved) { Log.verbose(logLevel, `Freed ${bytesRemoved} bytes`); } if (removedData && onDiscardedData) { - onDiscardedData(removedData); - } - - if (bytesRemoved) { - Log.verbose(logLevel, `Freed ${bytesRemoved} bytes`); + await onDiscardedData(removedData); } }; diff --git a/packages/media-parser/src/test/parse-and-download-video.test.ts b/packages/media-parser/src/test/parse-and-download-video.test.ts index 4a7cd448cd..d2b3b094ed 100644 --- a/packages/media-parser/src/test/parse-and-download-video.test.ts +++ b/packages/media-parser/src/test/parse-and-download-video.test.ts @@ -1,13 +1,56 @@ import {exampleVideos} from '@remotion/example-videos'; -import {test} from 'bun:test'; +import {expect, test} from 'bun:test'; import {parseAndDownloadMedia} from '../parse-and-download-media'; import {nodeReader} from '../readers/from-node'; import {nodeWriter} from '../writers/node'; test('should be able to parse and download video', async () => { - await parseAndDownloadMedia({ + const {size} = await parseAndDownloadMedia({ src: exampleVideos.avi, reader: nodeReader, + fields: { + size: true, + }, writer: nodeWriter('output.avi'), }); + + const s = Bun.file('output.avi'); + expect(s.size).toBe(742478); + expect(size).toBe(742478); + await s.unlink(); }); + +test('should be able to parse and download video', async () => { + const {size} = await parseAndDownloadMedia({ + src: exampleVideos.iphonehevc, + reader: nodeReader, + fields: { + size: true, + }, + writer: nodeWriter('iphonehevc.mp4'), + }); + + const s = Bun.file('iphonehevc.mp4'); + expect(size).toBe(7418901); + expect(s.size).toBe(7418901); + await s.unlink(); +}); + +test( + 'should be able to parse and download remote video', + async () => { + const {size} = await parseAndDownloadMedia({ + src: 'https://remotion-assets.s3.eu-central-1.amazonaws.com/example-videos/transportstream.ts', + fields: { + size: true, + }, + writer: nodeWriter('output2'), + }); + + const s = Bun.file('output2'); + expect(s.size).toBe(1913464); + expect(size).toBe(1913464); + await s.unlink(); + }, + {timeout: 30_000}, +); diff --git a/packages/media-parser/src/writers/node.ts b/packages/media-parser/src/writers/node.ts index 9dab127bb3..ca2d140c5a 100644 --- a/packages/media-parser/src/writers/node.ts +++ b/packages/media-parser/src/writers/node.ts @@ -56,20 +56,21 @@ const createContent = (filename: string): CreateContent => { writPromise.then(() => updateDataAt(position, data)); return writPromise; }, - waitForFinish: async () => { - await writPromise; - }, getWrittenByteCount: () => written, remove, - save: async () => { + finish: async () => { + await writPromise; try { fs.closeSync(writeStream); - const file = await fs.promises.readFile(filename); - return new Blob([file]); + return Promise.resolve(); } catch (e) { return Promise.reject(e); } }, + getBlob: async () => { + const file = await fs.promises.readFile(filename); + return new Blob([file]); + }, }; return writer; diff --git a/packages/media-parser/src/writers/writer.ts b/packages/media-parser/src/writers/writer.ts index afd863d019..dcca9efafe 100644 --- a/packages/media-parser/src/writers/writer.ts +++ b/packages/media-parser/src/writers/writer.ts @@ -1,10 +1,10 @@ export type Writer = { write: (arr: Uint8Array) => Promise; - save: () => Promise; + finish: () => Promise; getWrittenByteCount: () => number; updateDataAt: (position: number, data: Uint8Array) => Promise; - waitForFinish: () => Promise; remove: () => Promise; + getBlob: () => Promise; }; export type CreateContent = (options: { diff --git a/packages/webcodecs/src/convert-media.ts b/packages/webcodecs/src/convert-media.ts index b71ca6cda7..5fdfede639 100644 --- a/packages/webcodecs/src/convert-media.ts +++ b/packages/webcodecs/src/convert-media.ts @@ -259,7 +259,7 @@ export const convertMedia = async function < }) .then(() => { resolve({ - save: state.save, + save: state.getBlob, remove: state.remove, finalState: throttledState.get(), }); diff --git a/packages/webcodecs/src/create/iso-base-media/create-iso-base-media.ts b/packages/webcodecs/src/create/iso-base-media/create-iso-base-media.ts index f9ca85bc74..eb1300564e 100644 --- a/packages/webcodecs/src/create/iso-base-media/create-iso-base-media.ts +++ b/packages/webcodecs/src/create/iso-base-media/create-iso-base-media.ts @@ -217,8 +217,8 @@ export const createIsoBaseMedia = async ({ const waitForFinishPromises: (() => Promise)[] = []; return { - save: () => { - return w.save(); + getBlob: () => { + return w.getBlob(); }, remove: async () => { await w.remove(); @@ -273,7 +273,7 @@ export const createIsoBaseMedia = async ({ logLevel, 'All write operations done. Waiting for finish...', ); - await w.waitForFinish(); + await w.finish(); }, }; }; diff --git a/packages/webcodecs/src/create/matroska/create-matroska-media.ts b/packages/webcodecs/src/create/matroska/create-matroska-media.ts index 5482f85acd..d487d8d0ca 100644 --- a/packages/webcodecs/src/create/matroska/create-matroska-media.ts +++ b/packages/webcodecs/src/create/matroska/create-matroska-media.ts @@ -239,8 +239,8 @@ export const createMatroskaMedia = async ({ } }); }, - save: () => { - return w.save(); + getBlob: async () => { + return w.getBlob(); }, remove: async () => { await w.remove(); @@ -274,13 +274,13 @@ export const createMatroskaMedia = async ({ await updateSeekWrite(); await w.write(createMatroskaCues(cues).bytes); - await w.waitForFinish(); const segmentSize = w.getWrittenByteCount() - segmentOffset - matroskaToHex(matroskaElements.Segment).byteLength - MATROSKA_SEGMENT_MIN_VINT_WIDTH; await updateSegmentSize(segmentSize); + await w.finish(); }, }; }; diff --git a/packages/webcodecs/src/create/media-fn.ts b/packages/webcodecs/src/create/media-fn.ts index 143fd46cf2..fb7c453115 100644 --- a/packages/webcodecs/src/create/media-fn.ts +++ b/packages/webcodecs/src/create/media-fn.ts @@ -4,7 +4,7 @@ import type {MakeTrackAudio, MakeTrackVideo} from './make-track-info'; import type {ProgressTracker} from './progress-tracker'; export type MediaFn = { - save: () => Promise; + getBlob: () => Promise; remove: () => Promise; addSample: (options: { chunk: AudioOrVideoSample; diff --git a/packages/webcodecs/src/create/wav/create-wav.ts b/packages/webcodecs/src/create/wav/create-wav.ts index d4a4c6f5ee..fba4b6498b 100644 --- a/packages/webcodecs/src/create/wav/create-wav.ts +++ b/packages/webcodecs/src/create/wav/create-wav.ts @@ -109,8 +109,8 @@ export const createWav = async ({ const waitForFinishPromises: (() => Promise)[] = []; return { - save: () => { - return w.save(); + getBlob: () => { + return w.getBlob(); }, remove: () => { return w.remove(); @@ -144,7 +144,7 @@ export const createWav = async ({ await Promise.all(waitForFinishPromises.map((p) => p())); await operationProm.current; await updateSize(); - await w.waitForFinish(); + await w.finish(); }, addTrack: async (track) => { if (track.type !== 'audio') { diff --git a/packages/webcodecs/src/writers/buffer-implementation/writer.ts b/packages/webcodecs/src/writers/buffer-implementation/writer.ts index 3383b4c667..3f304446bc 100644 --- a/packages/webcodecs/src/writers/buffer-implementation/writer.ts +++ b/packages/webcodecs/src/writers/buffer-implementation/writer.ts @@ -31,13 +31,18 @@ export const createContent: CreateContent = ({filename, mimeType}) => { writPromise = writPromise.then(() => write(arr)); return writPromise; }, - save: () => { + finish: async () => { + await writPromise; + if (removed) { return Promise.reject( new Error('Already called .remove() on the result'), ); } + return Promise.resolve(); + }, + getBlob() { const arr = new Uint8Array(buf); return Promise.resolve( // TODO: Unhardcode MIME type and file name @@ -53,9 +58,6 @@ export const createContent: CreateContent = ({filename, mimeType}) => { writPromise = writPromise.then(() => updateDataAt(position, newData)); return writPromise; }, - waitForFinish: async () => { - await writPromise; - }, }; return Promise.resolve(writer); }; diff --git a/packages/webcodecs/src/writers/web-fs.ts b/packages/webcodecs/src/writers/web-fs.ts index 79a1906581..e1143a8686 100644 --- a/packages/webcodecs/src/writers/web-fs.ts +++ b/packages/webcodecs/src/writers/web-fs.ts @@ -43,13 +43,16 @@ const createContent: CreateContent = async ({filename}) => { writPromise = writPromise.then(() => write(arr)); return writPromise; }, - save: async () => { + finish: async () => { + await writPromise; + try { await writable.close(); } catch { // Ignore, could already be closed } - + }, + async getBlob() { const newHandle = await directoryHandle.getFileHandle(actualFilename, { create: true, }); @@ -61,9 +64,6 @@ const createContent: CreateContent = async ({filename}) => { writPromise = writPromise.then(() => updateDataAt(position, data)); return writPromise; }, - waitForFinish: async () => { - await writPromise; - }, remove, };