@alpha2-sh/editor 0.0.0-stage → 0.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.
Files changed (99) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +108 -2
  3. package/dist/browser/bridge.d.ts +44 -0
  4. package/dist/browser/bridge.js +2176 -0
  5. package/dist/browser/extensions-worker-entry.d.ts +1 -0
  6. package/dist/browser/extensions-worker-entry.js +3 -0
  7. package/dist/browser/extensions-worker.d.ts +46 -0
  8. package/dist/browser/extensions-worker.js +149 -0
  9. package/dist/browser/hash-worker-entry.d.ts +1 -0
  10. package/dist/browser/hash-worker-entry.js +3 -0
  11. package/dist/browser/hash-worker.d.ts +24 -0
  12. package/dist/browser/hash-worker.js +87 -0
  13. package/dist/browser/index.d.ts +11 -0
  14. package/dist/browser/index.js +9 -0
  15. package/dist/browser/jobs.d.ts +82 -0
  16. package/dist/browser/jobs.js +291 -0
  17. package/dist/browser/resources.d.ts +40 -0
  18. package/dist/browser/resources.js +83 -0
  19. package/dist/browser/runtime/assets.d.ts +233 -0
  20. package/dist/browser/runtime/assets.js +633 -0
  21. package/dist/browser/runtime/audio.d.ts +60 -0
  22. package/dist/browser/runtime/audio.js +549 -0
  23. package/dist/browser/runtime/budget.d.ts +11 -0
  24. package/dist/browser/runtime/budget.js +26 -0
  25. package/dist/browser/runtime/caption-fonts.d.ts +34 -0
  26. package/dist/browser/runtime/caption-fonts.js +69 -0
  27. package/dist/browser/runtime/color.d.ts +7 -0
  28. package/dist/browser/runtime/color.js +12 -0
  29. package/dist/browser/runtime/engine.d.ts +91 -0
  30. package/dist/browser/runtime/engine.js +352 -0
  31. package/dist/browser/runtime/export-audio.d.ts +23 -0
  32. package/dist/browser/runtime/export-audio.js +75 -0
  33. package/dist/browser/runtime/exporter.d.ts +140 -0
  34. package/dist/browser/runtime/exporter.js +751 -0
  35. package/dist/browser/runtime/extension-import.d.ts +196 -0
  36. package/dist/browser/runtime/extension-import.js +521 -0
  37. package/dist/browser/runtime/fingerprint.d.ts +57 -0
  38. package/dist/browser/runtime/fingerprint.js +157 -0
  39. package/dist/browser/runtime/generators.d.ts +188 -0
  40. package/dist/browser/runtime/generators.js +173 -0
  41. package/dist/browser/runtime/locks.d.ts +32 -0
  42. package/dist/browser/runtime/locks.js +86 -0
  43. package/dist/browser/runtime/output.d.ts +87 -0
  44. package/dist/browser/runtime/output.js +230 -0
  45. package/dist/browser/runtime/preview-utils.d.ts +3 -0
  46. package/dist/browser/runtime/preview-utils.js +49 -0
  47. package/dist/browser/runtime/proxies.d.ts +72 -0
  48. package/dist/browser/runtime/proxies.js +369 -0
  49. package/dist/browser/runtime/proxy-storage.d.ts +59 -0
  50. package/dist/browser/runtime/proxy-storage.js +119 -0
  51. package/dist/browser/runtime/sources.d.ts +82 -0
  52. package/dist/browser/runtime/sources.js +819 -0
  53. package/dist/browser/runtime/staging.d.ts +10 -0
  54. package/dist/browser/runtime/staging.js +32 -0
  55. package/dist/browser/runtime/still.d.ts +21 -0
  56. package/dist/browser/runtime/still.js +52 -0
  57. package/dist/browser/runtime/wasm.d.ts +9 -0
  58. package/dist/browser/runtime/wasm.js +20 -0
  59. package/dist/browser/types.d.ts +549 -0
  60. package/dist/browser/types.js +1 -0
  61. package/dist/browser/worker-entry.d.ts +1 -0
  62. package/dist/browser/worker-entry.js +2 -0
  63. package/dist/browser/worker.d.ts +24 -0
  64. package/dist/browser/worker.js +239 -0
  65. package/dist/core/captions.d.ts +34 -0
  66. package/dist/core/captions.js +108 -0
  67. package/dist/core/clip-time.d.ts +31 -0
  68. package/dist/core/clip-time.js +125 -0
  69. package/dist/core/commands.d.ts +413 -0
  70. package/dist/core/commands.js +1118 -0
  71. package/dist/core/compositions.d.ts +234 -0
  72. package/dist/core/compositions.js +851 -0
  73. package/dist/core/document.d.ts +245 -0
  74. package/dist/core/document.js +683 -0
  75. package/dist/core/keyframes.d.ts +82 -0
  76. package/dist/core/keyframes.js +153 -0
  77. package/dist/core/presets.d.ts +64 -0
  78. package/dist/core/presets.js +241 -0
  79. package/dist/core/session.d.ts +150 -0
  80. package/dist/core/session.js +541 -0
  81. package/dist/core/text.d.ts +17 -0
  82. package/dist/core/text.js +17 -0
  83. package/dist/core/time.d.ts +19 -0
  84. package/dist/core/time.js +39 -0
  85. package/dist/core/timeline-index.d.ts +44 -0
  86. package/dist/core/timeline-index.js +59 -0
  87. package/dist/core/transitions.d.ts +20 -0
  88. package/dist/core/transitions.js +118 -0
  89. package/dist/core/validated.d.ts +23 -0
  90. package/dist/core/validated.js +52 -0
  91. package/dist/index.d.ts +12 -0
  92. package/dist/index.js +11 -0
  93. package/dist/native-client/index.d.ts +131 -0
  94. package/dist/native-client/index.js +354 -0
  95. package/dist/ports/index.d.ts +146 -0
  96. package/dist/ports/index.js +1 -0
  97. package/dist/react/index.d.ts +37 -0
  98. package/dist/react/index.js +94 -0
  99. package/package.json +79 -4
@@ -0,0 +1,87 @@
1
+ /** Which storage APIs this page has, as an export's result reports them. */
2
+ export function storageSupport(): {
3
+ opfs: boolean;
4
+ savePicker: boolean;
5
+ estimate: boolean;
6
+ };
7
+ /**
8
+ * Opens where an export writes, refusing before anything renders only an in-memory output past its
9
+ * limit.
10
+ *
11
+ * Storage is never refused ahead: `navigator.storage.estimate()` is a guess, and WebKit's usage
12
+ * counts every write since its last disk scan, so `quota - usage` can be near zero on an empty
13
+ * disk. A full disk fails at the write (`STORAGE_QUOTA_EXCEEDED`); the estimate is only reported.
14
+ *
15
+ * Returns `{ mode, destination, storageEstimate, abort, finish }`: `destination` is the sink's,
16
+ * `abort` discards the file, and `finish(sinkOutput)` turns the finished output into the artifact's
17
+ * fields.
18
+ */
19
+ export function openExportOutput({ jobId, name, target, durationSeconds, videoBitrate, audioBitrate, }: {
20
+ jobId: any;
21
+ name: any;
22
+ target: any;
23
+ durationSeconds: any;
24
+ videoBitrate: any;
25
+ audioBitrate: any;
26
+ }): Promise<{
27
+ mode: string;
28
+ estimatedBytes: any;
29
+ storageEstimate: null;
30
+ destination: {
31
+ kind: string;
32
+ writable: WritableStream<import("alpha2/export-destination").PositionedWrite>;
33
+ };
34
+ stats: () => import("alpha2/export-destination").FileDestinationStats;
35
+ abort: (reason: any) => Promise<void>;
36
+ finish: (output: any) => Promise<{
37
+ kind: string;
38
+ name: any;
39
+ bytes: any;
40
+ }>;
41
+ } | {
42
+ mode: string;
43
+ estimatedBytes: any;
44
+ storageEstimate: {
45
+ quotaBytes: number;
46
+ usageBytes: number;
47
+ neededBytes: number;
48
+ belowEstimate: boolean;
49
+ } | null;
50
+ destination: {
51
+ kind: string;
52
+ writable: WritableStream<import("alpha2/export-destination").PositionedWrite>;
53
+ };
54
+ stats: () => import("alpha2/export-destination").FileDestinationStats;
55
+ abort: (reason: any) => Promise<void>;
56
+ finish: (output: any) => Promise<{
57
+ kind: string;
58
+ file: File;
59
+ name: any;
60
+ bytes: any;
61
+ release: () => Promise<void>;
62
+ }>;
63
+ } | {
64
+ mode: string;
65
+ estimatedBytes: any;
66
+ storageEstimate: null;
67
+ destination: {
68
+ kind: string;
69
+ maxBytes: number;
70
+ };
71
+ stats: () => null;
72
+ abort: () => Promise<void>;
73
+ finish: (output: any) => Promise<{
74
+ kind: string;
75
+ blob: Blob;
76
+ name: any;
77
+ bytes: any;
78
+ }>;
79
+ }>;
80
+ /**
81
+ * Deletes temporary export files left by pages that are gone: started over a day ago and held by
82
+ * no page. Resolves to the names removed.
83
+ * @returns {Promise<string[]>}
84
+ */
85
+ export function sweepStaleExports(now?: number): Promise<string[]>;
86
+ /** The most an in-memory output may hold at its peak unless asked otherwise. */
87
+ export const DEFAULT_MEMORY_OUTPUT_LIMIT: number;
@@ -0,0 +1,230 @@
1
+ // Where a browser export's file goes: streamed into a temporary OPFS file (or a user-picked file)
2
+ // so encoded video never accumulates in memory. In-memory output is for small outputs sized up
3
+ // front, never a silent fallback for a large one.
4
+ import { estimatedOutputBytes, memoryOutputPeakBytes, MEMORY_OUTPUT_COPIES, MEMORY_OUTPUT_MARGIN, WritableFileDestination, } from "alpha2/export-destination";
5
+ import { ExportFailureError } from "alpha2/export-failure";
6
+ import { holdShared, whenUnheld } from "./locks.js";
7
+ /** The most an in-memory output may hold at its peak unless asked otherwise. */
8
+ export const DEFAULT_MEMORY_OUTPUT_LIMIT = 512 * 1024 * 1024;
9
+ /** The origin-private directory temporary export files live in. */
10
+ const EXPORT_DIRECTORY = "alpha2-exports";
11
+ /**
12
+ * A temporary export older than this, that no page holds, belongs to a page that is gone. Where the
13
+ * browser has no Web Locks, age alone decides.
14
+ */
15
+ const STALE_EXPORT_MS = 24 * 60 * 60 * 1000;
16
+ /** The Web Lock a page holds on a temporary export file while it writes or keeps it. */
17
+ const exportLock = (fileName) => `alpha2-export:${fileName}`;
18
+ /** Free storage an export is reported to want beyond its estimate. */
19
+ const STORAGE_MARGIN = MEMORY_OUTPUT_MARGIN;
20
+ /** Which storage APIs this page has, as an export's result reports them. */
21
+ export function storageSupport() {
22
+ const storage = globalThis.navigator?.storage;
23
+ return {
24
+ // Handles gained `createWritable` later than the directory, so that is checked at open.
25
+ opfs: typeof storage?.getDirectory === "function",
26
+ savePicker: typeof globalThis.showSaveFilePicker === "function",
27
+ estimate: typeof storage?.estimate === "function",
28
+ };
29
+ }
30
+ /**
31
+ * Opens where an export writes, refusing before anything renders only an in-memory output past its
32
+ * limit.
33
+ *
34
+ * Storage is never refused ahead: `navigator.storage.estimate()` is a guess, and WebKit's usage
35
+ * counts every write since its last disk scan, so `quota - usage` can be near zero on an empty
36
+ * disk. A full disk fails at the write (`STORAGE_QUOTA_EXCEEDED`); the estimate is only reported.
37
+ *
38
+ * Returns `{ mode, destination, storageEstimate, abort, finish }`: `destination` is the sink's,
39
+ * `abort` discards the file, and `finish(sinkOutput)` turns the finished output into the artifact's
40
+ * fields.
41
+ */
42
+ export async function openExportOutput({ jobId, name, target, durationSeconds, videoBitrate, audioBitrate, }) {
43
+ const estimate = estimatedOutputBytes({
44
+ durationSeconds,
45
+ videoBitrate,
46
+ audioBitrate,
47
+ });
48
+ const kind = target?.kind ?? "opfs";
49
+ if (kind === "file")
50
+ return openPickedFile(target.handle, estimate);
51
+ if (kind === "opfs") {
52
+ const opened = await openOriginPrivateFile(jobId, name, estimate);
53
+ if (opened)
54
+ return opened;
55
+ }
56
+ return openMemory(target?.maxBytes ?? DEFAULT_MEMORY_OUTPUT_LIMIT, estimate, {
57
+ name,
58
+ fellBack: kind !== "memory",
59
+ });
60
+ }
61
+ async function openPickedFile(handle, estimate) {
62
+ const writable = await handle
63
+ .createWritable({ keepExistingData: false })
64
+ .catch((cause) => {
65
+ throw refused("OUTPUT_IO_FAILED", `The chosen file cannot be written: ${cause?.message ?? cause}`, { cause });
66
+ });
67
+ const destination = new WritableFileDestination(writable);
68
+ return {
69
+ mode: "file",
70
+ estimatedBytes: estimate,
71
+ storageEstimate: null,
72
+ destination: { kind: "stream", writable: destination.writable },
73
+ stats: () => destination.stats,
74
+ abort: (reason) => destination.abort(reason),
75
+ finish: async (output) => ({
76
+ kind: "saved",
77
+ name: handle.name,
78
+ bytes: output.bytes,
79
+ }),
80
+ };
81
+ }
82
+ async function openOriginPrivateFile(jobId, name, estimate) {
83
+ const storage = globalThis.navigator?.storage;
84
+ if (typeof storage?.getDirectory !== "function")
85
+ return null;
86
+ let directory;
87
+ let handle;
88
+ let writable;
89
+ const fileName = `${Date.now()}-${jobId}.mp4`;
90
+ // Held from before the file exists until it is removed, so another page's sweep leaves it alone.
91
+ const lease = await holdShared(exportLock(fileName));
92
+ try {
93
+ directory = await (await storage.getDirectory()).getDirectoryHandle(EXPORT_DIRECTORY, { create: true });
94
+ handle = await directory.getFileHandle(fileName, { create: true });
95
+ if (typeof handle.createWritable !== "function") {
96
+ await directory.removeEntry(fileName).catch(() => { });
97
+ void lease.release();
98
+ return null;
99
+ }
100
+ }
101
+ catch {
102
+ void lease.release();
103
+ return null;
104
+ }
105
+ const storageEstimate = await reportedStorage(storage, estimate);
106
+ try {
107
+ writable = await handle.createWritable();
108
+ }
109
+ catch {
110
+ await directory.removeEntry(fileName).catch(() => { });
111
+ void lease.release();
112
+ return null;
113
+ }
114
+ const destination = new WritableFileDestination(writable);
115
+ let released = false;
116
+ const remove = async () => {
117
+ if (released)
118
+ return;
119
+ released = true;
120
+ try {
121
+ await directory.removeEntry(fileName);
122
+ }
123
+ finally {
124
+ void lease.release();
125
+ }
126
+ };
127
+ return {
128
+ mode: "opfs",
129
+ estimatedBytes: estimate,
130
+ storageEstimate,
131
+ destination: { kind: "stream", writable: destination.writable },
132
+ stats: () => destination.stats,
133
+ abort: async (reason) => {
134
+ await destination.abort(reason);
135
+ await remove().catch(() => { });
136
+ },
137
+ finish: async (output) => {
138
+ const file = await handle.getFile();
139
+ return {
140
+ kind: "file",
141
+ file: new File([file], name, { type: "video/mp4" }),
142
+ name,
143
+ bytes: output.bytes,
144
+ release: remove,
145
+ };
146
+ },
147
+ };
148
+ }
149
+ /**
150
+ * What the browser says about the origin's storage, beside what this export is expected to need:
151
+ * `belowEstimate` when its `quota - usage` is less. A report, never a refusal (see
152
+ * `openExportOutput`); `null` where the browser says nothing.
153
+ */
154
+ async function reportedStorage(storage, estimate) {
155
+ if (typeof storage.estimate !== "function")
156
+ return null;
157
+ const { quota, usage } = await storage.estimate().catch(() => ({}));
158
+ if (typeof quota !== "number" || typeof usage !== "number")
159
+ return null;
160
+ const needed = Math.ceil(estimate * STORAGE_MARGIN);
161
+ return {
162
+ quotaBytes: quota,
163
+ usageBytes: usage,
164
+ neededBytes: needed,
165
+ belowEstimate: quota - usage < needed,
166
+ };
167
+ }
168
+ function openMemory(limit, estimate, { name, fellBack }) {
169
+ const peak = memoryOutputPeakBytes(estimate);
170
+ if (peak > limit)
171
+ throw refused("RESOURCE_LIMIT", `${fellBack ? "This browser cannot write the export to disk, and an" : "An"} output of about ${megabytes(estimate)} needs up to ${megabytes(peak)} of memory while it is kept in memory, over the ${megabytes(limit)} limit. Export a shorter range or use the native exporter.`, { requestedBytes: peak, resource: "js" });
172
+ return {
173
+ mode: "memory",
174
+ estimatedBytes: estimate,
175
+ storageEstimate: null,
176
+ destination: {
177
+ kind: "memory",
178
+ maxBytes: Math.floor(limit / MEMORY_OUTPUT_COPIES),
179
+ },
180
+ stats: () => null,
181
+ abort: async () => { },
182
+ finish: async (output) => ({
183
+ kind: "blob",
184
+ blob: new Blob([output.buffer], { type: "video/mp4" }),
185
+ name,
186
+ bytes: output.buffer.byteLength,
187
+ }),
188
+ };
189
+ }
190
+ /**
191
+ * Deletes temporary export files left by pages that are gone: started over a day ago and held by
192
+ * no page. Resolves to the names removed.
193
+ * @returns {Promise<string[]>}
194
+ */
195
+ export async function sweepStaleExports(now = Date.now()) {
196
+ const removed = [];
197
+ try {
198
+ const directory = await (await globalThis.navigator.storage.getDirectory()).getDirectoryHandle(EXPORT_DIRECTORY, { create: false });
199
+ const stale = [];
200
+ for await (const [entry] of directory.entries()) {
201
+ const started = Number(entry.split("-", 1)[0]);
202
+ if (Number.isFinite(started) && now - started > STALE_EXPORT_MS)
203
+ stale.push(entry);
204
+ }
205
+ for (const entry of stale)
206
+ if (await whenUnheld(exportLock(entry), () => directory.removeEntry(entry)).catch(() => false))
207
+ removed.push(entry);
208
+ }
209
+ catch {
210
+ // No directory yet, or no OPFS: nothing to sweep.
211
+ }
212
+ return removed;
213
+ }
214
+ function refused(code, message, extra = {}) {
215
+ return new ExportFailureError({
216
+ code,
217
+ message,
218
+ origin: "browser",
219
+ operation: "export",
220
+ backend: "unknown",
221
+ phase: "prepare",
222
+ resource: extra.resource ?? "unknown",
223
+ ...(extra.requestedBytes === undefined
224
+ ? {}
225
+ : { requestedBytes: extra.requestedBytes }),
226
+ }, extra.cause === undefined ? undefined : { cause: extra.cause });
227
+ }
228
+ function megabytes(bytes) {
229
+ return `${(bytes / 1e6).toFixed(bytes < 1e7 ? 1 : 0)} MB`;
230
+ }
@@ -0,0 +1,3 @@
1
+ export function containRect(sourceWidth: any, sourceHeight: any, width: any, height: any): number[];
2
+ export function previewFrameIndex(seconds: any, fps: any): number;
3
+ export function seekVideoFrame(video: any, seconds: any, tolerance?: number): Promise<void>;
@@ -0,0 +1,49 @@
1
+ // Keep the source's display aspect ratio; transparent padding belongs to the clip and participates
2
+ // in its transform and transition.
3
+ export function containRect(sourceWidth, sourceHeight, width, height) {
4
+ const scale = Math.min(width / sourceWidth, height / sourceHeight);
5
+ const drawWidth = sourceWidth * scale;
6
+ const drawHeight = sourceHeight * scale;
7
+ return [
8
+ (width - drawWidth) / 2,
9
+ (height - drawHeight) / 2,
10
+ drawWidth,
11
+ drawHeight,
12
+ ];
13
+ }
14
+ // Select project frames from the clock instead of accumulating RAF delays.
15
+ export function previewFrameIndex(seconds, fps) {
16
+ return Math.floor(seconds * fps + 1e-7);
17
+ }
18
+ export async function seekVideoFrame(video, seconds, tolerance = 0.001) {
19
+ if (!video.seeking &&
20
+ Math.abs(video.currentTime - seconds) <= tolerance &&
21
+ video.readyState >= 2)
22
+ return;
23
+ await new Promise((resolve, reject) => {
24
+ const cleanup = () => {
25
+ clearTimeout(timeout);
26
+ video.removeEventListener("seeked", done);
27
+ video.removeEventListener("error", failed);
28
+ };
29
+ const done = () => {
30
+ if (video.seeking)
31
+ return;
32
+ cleanup();
33
+ resolve();
34
+ };
35
+ const failed = () => {
36
+ cleanup();
37
+ reject(video.error ?? new Error("video seek failed"));
38
+ };
39
+ const timeout = setTimeout(() => {
40
+ cleanup();
41
+ reject(new Error("video seek timed out"));
42
+ }, 10_000);
43
+ video.addEventListener("seeked", done);
44
+ video.addEventListener("error", failed);
45
+ // currentTime changes before the new frame is available; wait on a pending seek to this target.
46
+ if (!video.seeking || Math.abs(video.currentTime - seconds) > tolerance)
47
+ video.currentTime = seconds;
48
+ });
49
+ }
@@ -0,0 +1,72 @@
1
+ /** A proxy's longer displayed edge, in pixels. */
2
+ export const PROXY_LONG_EDGE: 960;
3
+ /**
4
+ * @typedef {{
5
+ * made_from: import("alpha2/project").MediaFingerprint,
6
+ * width: number,
7
+ * height: number,
8
+ * frame_rate?: { num: number, den: number },
9
+ * duration_ticks?: number,
10
+ * audio_sample_rate?: number,
11
+ * }} ProxyRecord
12
+ */
13
+ /**
14
+ * Keeps proxies by their original's SHA-256: in OPFS where the browser can write files there, and
15
+ * otherwise in this page's memory, within {@link DEFAULT_MEMORY_OUTPUT_LIMIT} unless asked
16
+ * otherwise.
17
+ */
18
+ export class ProxyStore {
19
+ constructor({ memoryLimit }?: {
20
+ memoryLimit?: number | undefined;
21
+ });
22
+ /**
23
+ * Holds the proxy of `hash` in use until `release()`, so no page removes it meanwhile (see
24
+ * `holdProxy`).
25
+ */
26
+ hold(hash: any): Promise<{
27
+ release(): Promise<void>;
28
+ }>;
29
+ /** The proxy made from the media with `hash`, if one is kept. */
30
+ find(hash: any): Promise<{
31
+ record: any;
32
+ file: File;
33
+ } | null>;
34
+ /**
35
+ * The proxies kept: each original's hash and the bytes its proxy takes, on disk or in this page's
36
+ * memory.
37
+ */
38
+ list(): Promise<{
39
+ hash: string;
40
+ bytes: number;
41
+ }[]>;
42
+ /**
43
+ * Removes the proxies of `hashes`, except those a page holds in use. Resolves to the hashes
44
+ * removed and those left in use.
45
+ */
46
+ remove(hashes: any): Promise<import("../types.js").ProxyRemoval>;
47
+ /**
48
+ * Makes a proxy of `file`, whose fingerprint is `fingerprint`, and keeps it. The video is scaled
49
+ * to fit {@link PROXY_LONG_EDGE} with a key frame every second; timestamps and the audio sample
50
+ * rate are the original's. A proxy already kept for the same bytes is returned instead. Pages of
51
+ * the origin make one hash's proxy in turn (`whileMakingProxy`).
52
+ *
53
+ * Refuses before encoding when the conversion would drop a track the fingerprint records. Rejects
54
+ * with `signal.reason` if `signal` aborts before the proxy is kept.
55
+ */
56
+ generate(file: any, fingerprint: any, { signal, onProgress }?: {}): Promise<{
57
+ record: any;
58
+ file: File;
59
+ }>;
60
+ #private;
61
+ }
62
+ export type ProxyRecord = {
63
+ made_from: import("alpha2/project").MediaFingerprint;
64
+ width: number;
65
+ height: number;
66
+ frame_rate?: {
67
+ num: number;
68
+ den: number;
69
+ };
70
+ duration_ticks?: number;
71
+ audio_sample_rate?: number;
72
+ };