capacitor-native-video-compressor 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * WebCodecs output goes to the origin private file system so a long video
3
+ * never sits whole in memory. The File handed back stays readable only while
4
+ * its OPFS entry exists, and callers keep it well past `compressVideo` (until
5
+ * the upload is done, or longer for a retry), so a successful output stays
6
+ * until the caller releases it with `finishUpload({ jobId })` or the tab
7
+ * closes. Each tab writes under its own directory and holds a Web Lock named
8
+ * after it for its whole life; every compress removes the directories whose
9
+ * lock nobody holds any more, i.e. those of closed tabs. A failed or cancelled
10
+ * job deletes its own file straight away.
11
+ */
12
+ const OPFS_ROOT = 'native-video-compressor';
13
+ const lockName = (tabId) => `${OPFS_ROOT}:${tabId}`;
14
+ let tabId = null;
15
+ const getTabId = () => (tabId !== null && tabId !== void 0 ? tabId : (tabId = crypto.randomUUID()));
16
+ let fileCount = 0;
17
+ // Memoised apart from the directories: they are reopened after site data is
18
+ // cleared, but this tab still holds its lock, and requesting it again would
19
+ // wait forever on ourselves.
20
+ let tabLock = null;
21
+ let tabDirsPromise = null;
22
+ // A rejected request (e.g. a sandboxed iframe) rejects here, so openTabDirs
23
+ // fails and the caller buffers in memory instead of waiting forever.
24
+ const holdTabLock = () => new Promise((granted, rejected) => {
25
+ navigator.locks
26
+ .request(lockName(getTabId()), () => {
27
+ granted();
28
+ return new Promise(() => undefined);
29
+ })
30
+ .catch(rejected);
31
+ });
32
+ const sweepClosedTabs = async (base) => {
33
+ // Names first, locks second: a tab that creates its directory between the two
34
+ // reads already holds its lock, so it is never mistaken for a closed one.
35
+ const names = [];
36
+ for await (const name of base.keys())
37
+ names.push(name);
38
+ const { held = [] } = await navigator.locks.query();
39
+ const live = new Set(held.map((l) => l.name));
40
+ for (const name of names) {
41
+ if (!live.has(lockName(name)))
42
+ await base.removeEntry(name, { recursive: true }).catch(() => undefined);
43
+ }
44
+ };
45
+ const openTabDirs = async () => {
46
+ const base = await (await navigator.storage.getDirectory()).getDirectoryHandle(OPFS_ROOT, { create: true });
47
+ if (navigator.locks) {
48
+ await (tabLock !== null && tabLock !== void 0 ? tabLock : (tabLock = holdTabLock().catch((e) => {
49
+ tabLock = null;
50
+ throw e;
51
+ })));
52
+ }
53
+ return { base, dir: await base.getDirectoryHandle(getTabId(), { create: true }) };
54
+ };
55
+ /** Null when OPFS or `createWritable` (missing on Safari's main thread) isn't there. */
56
+ export const createOpfsOutput = async () => {
57
+ var _a;
58
+ try {
59
+ if (typeof navigator === 'undefined' || !((_a = navigator.storage) === null || _a === void 0 ? void 0 : _a.getDirectory))
60
+ return null;
61
+ const { base, dir } = await (tabDirsPromise !== null && tabDirsPromise !== void 0 ? tabDirsPromise : (tabDirsPromise = openTabDirs().catch((e) => {
62
+ tabDirsPromise = null;
63
+ throw e;
64
+ })));
65
+ // Without Web Locks another tab's live files can't be told apart: no sweep.
66
+ if (navigator.locks)
67
+ await sweepClosedTabs(base).catch(() => undefined);
68
+ const name = `${++fileCount}.mp4`;
69
+ const handle = await dir.getFileHandle(name, { create: true }).catch((e) => {
70
+ // The directory was removed under us (site data cleared): reopen it next time.
71
+ if ((e === null || e === void 0 ? void 0 : e.name) === 'NotFoundError')
72
+ tabDirsPromise = null;
73
+ throw e;
74
+ });
75
+ if (typeof handle.createWritable !== 'function') {
76
+ await dir.removeEntry(name).catch(() => undefined);
77
+ return null;
78
+ }
79
+ const writable = await handle.createWritable();
80
+ const discard = async () => {
81
+ // Rejects harmlessly once mediabunny holds the writer; it closes it then.
82
+ await writable.abort().catch(() => undefined);
83
+ await dir.removeEntry(name).catch(() => undefined);
84
+ };
85
+ // OPFS files carry no MIME type, and callers derive the extension from it.
86
+ const read = async () => (await handle.getFile()).slice(0, undefined, 'video/mp4');
87
+ return { writable, read, discard };
88
+ }
89
+ catch (e) {
90
+ console.warn('[compress] OPFS unavailable, buffering output in memory:', e);
91
+ return null;
92
+ }
93
+ };
94
+ //# sourceMappingURL=opfs-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opfs-output.js","sourceRoot":"","sources":["../../src/opfs-output.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebCodecs output goes to the origin private file system so a long video\n * never sits whole in memory. The File handed back stays readable only while\n * its OPFS entry exists, and callers keep it well past `compressVideo` (until\n * the upload is done, or longer for a retry), so a successful output stays\n * until the caller releases it with `finishUpload({ jobId })` or the tab\n * closes. Each tab writes under its own directory and holds a Web Lock named\n * after it for its whole life; every compress removes the directories whose\n * lock nobody holds any more, i.e. those of closed tabs. A failed or cancelled\n * job deletes its own file straight away.\n */\nconst OPFS_ROOT = 'native-video-compressor';\nconst lockName = (tabId: string) => `${OPFS_ROOT}:${tabId}`;\n\nlet tabId: string | null = null;\nconst getTabId = () => (tabId ??= crypto.randomUUID());\nlet fileCount = 0;\n\n// Memoised apart from the directories: they are reopened after site data is\n// cleared, but this tab still holds its lock, and requesting it again would\n// wait forever on ourselves.\nlet tabLock: Promise<void> | null = null;\nlet tabDirsPromise: Promise<{ base: FileSystemDirectoryHandle; dir: FileSystemDirectoryHandle }> | null = null;\n\n// A rejected request (e.g. a sandboxed iframe) rejects here, so openTabDirs\n// fails and the caller buffers in memory instead of waiting forever.\nconst holdTabLock = () =>\n new Promise<void>((granted, rejected) => {\n navigator.locks\n .request(lockName(getTabId()), () => {\n granted();\n return new Promise<never>(() => undefined);\n })\n .catch(rejected);\n });\n\nconst sweepClosedTabs = async (base: FileSystemDirectoryHandle) => {\n // Names first, locks second: a tab that creates its directory between the two\n // reads already holds its lock, so it is never mistaken for a closed one.\n const names: string[] = [];\n for await (const name of base.keys()) names.push(name);\n const { held = [] } = await navigator.locks.query();\n const live = new Set(held.map((l) => l.name));\n for (const name of names) {\n if (!live.has(lockName(name))) await base.removeEntry(name, { recursive: true }).catch(() => undefined);\n }\n};\n\nconst openTabDirs = async () => {\n const base = await (await navigator.storage.getDirectory()).getDirectoryHandle(OPFS_ROOT, { create: true });\n if (navigator.locks) {\n await (tabLock ??= holdTabLock().catch((e) => {\n tabLock = null;\n throw e;\n }));\n }\n return { base, dir: await base.getDirectoryHandle(getTabId(), { create: true }) };\n};\n\nexport interface OpfsOutput {\n writable: FileSystemWritableFileStream;\n read: () => Promise<Blob>;\n discard: () => Promise<void>;\n}\n\n/** Null when OPFS or `createWritable` (missing on Safari's main thread) isn't there. */\nexport const createOpfsOutput = async (): Promise<OpfsOutput | null> => {\n try {\n if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) return null;\n const { base, dir } = await (tabDirsPromise ??= openTabDirs().catch((e) => {\n tabDirsPromise = null;\n throw e;\n }));\n // Without Web Locks another tab's live files can't be told apart: no sweep.\n if (navigator.locks) await sweepClosedTabs(base).catch(() => undefined);\n const name = `${++fileCount}.mp4`;\n const handle = await dir.getFileHandle(name, { create: true }).catch((e) => {\n // The directory was removed under us (site data cleared): reopen it next time.\n if (e?.name === 'NotFoundError') tabDirsPromise = null;\n throw e;\n });\n if (typeof handle.createWritable !== 'function') {\n await dir.removeEntry(name).catch(() => undefined);\n return null;\n }\n const writable = await handle.createWritable();\n const discard = async () => {\n // Rejects harmlessly once mediabunny holds the writer; it closes it then.\n await writable.abort().catch(() => undefined);\n await dir.removeEntry(name).catch(() => undefined);\n };\n // OPFS files carry no MIME type, and callers derive the extension from it.\n const read = async () => (await handle.getFile()).slice(0, undefined, 'video/mp4');\n return { writable, read, discard };\n } catch (e) {\n console.warn('[compress] OPFS unavailable, buffering output in memory:', e);\n return null;\n }\n};\n"]}
package/dist/esm/web.d.ts CHANGED
@@ -1,18 +1,30 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
2
  import type { NativeVideoCompressorPlugin, CompressOptions, CompressResult, CancelOptions, CancelResult } from './definitions';
3
+ export declare const webCodecsSize: (displayWidth: number, displayHeight: number, shortSide: number) => {
4
+ width: number;
5
+ } | {
6
+ height: number;
7
+ };
8
+ export declare const ffmpegScaleFilter: (shortSide: number) => string;
9
+ export declare const capBitrate: (target: number, sourceBitrate: number | undefined, floor: number, headroom?: number) => number;
10
+ export declare const codecHeadroom: (codec: string | null | undefined) => number;
3
11
  export declare class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {
4
12
  private ffmpeg;
5
13
  private isLoaded;
6
14
  /** The mediabunny conversion running now, so `cancel()` can abort it. */
7
15
  private activeConversion;
8
- /** The job `compressVideo` is running right now, for `cancel({ jobId })`. */
9
- private currentJobId;
16
+ /** The job `compressVideo` is running right now; a fresh object per call, so two calls never look alike. */
17
+ private currentJob;
10
18
  private cancelRequested;
19
+ /** Kept OPFS outputs by jobId, deleted by `finishUpload`. */
20
+ private keptOutputs;
11
21
  initialize(): Promise<{
12
22
  success: boolean;
13
23
  message?: string;
14
24
  }>;
15
- finishUpload(): Promise<{
25
+ finishUpload({ jobId }: {
26
+ jobId: string;
27
+ }): Promise<{
16
28
  released: boolean;
17
29
  }>;
18
30
  compressVideo(options: CompressOptions): Promise<CompressResult>;
@@ -21,16 +33,19 @@ export declare class NativeVideoCompressorWeb extends WebPlugin implements Nativ
21
33
  * so acting on both is safe.
22
34
  *
23
35
  * WebCodecs: `conversion.cancel()` makes the in-flight `execute()` reject
24
- * with `ConversionCanceledError`, surfaced as a `CancelledError` below.
36
+ * with `ConversionCanceledError`, surfaced as a CANCELLED `CompressError` below.
25
37
  * ffmpeg: `exec()` runs synchronously inside the worker and never reaches a
26
38
  * message poll while encoding, so `worker.terminate()` is the only way to
27
39
  * stop it — its cost is the loaded core, which is why the core URLs are
28
40
  * cached above.
29
41
  */
30
42
  cancel(options?: CancelOptions): Promise<CancelResult>;
43
+ private stopEngines;
31
44
  private fetchSourceBlob;
32
45
  private canUseWebCodecs;
33
46
  private compressWithWebCodecs;
47
+ /** False when mediabunny can't convert this input. */
48
+ private runConversion;
34
49
  private loadFFmpeg;
35
50
  private compressWithFfmpeg;
36
51
  }