@mediabunny/prores 1.49.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.
- package/LICENSE +373 -0
- package/README.md +44 -0
- package/dist/bundles/mediabunny-prores.js +1946 -0
- package/dist/bundles/mediabunny-prores.min.js +890 -0
- package/dist/bundles/mediabunny-prores.min.mjs +889 -0
- package/dist/bundles/mediabunny-prores.mjs +1892 -0
- package/dist/mediabunny-prores.d.ts +11 -0
- package/dist/modules/src/index.d.ts +16 -0
- package/dist/modules/src/index.d.ts.map +1 -0
- package/dist/modules/src/index.js +195 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/index.ts +239 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026-present, Vanilagy and contributors
|
|
3
|
+
*
|
|
4
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CustomVideoDecoder, EncodedPacket, Logging, registerDecoder, VideoCodec, VideoSample } from 'mediabunny';
|
|
10
|
+
import { Decoder, Frame, PixelFormat, PIXEL_FORMATS, FilledFrame } from 'turbores';
|
|
11
|
+
import {
|
|
12
|
+
assert,
|
|
13
|
+
isWebKit,
|
|
14
|
+
} from '../../../src/misc';
|
|
15
|
+
import { type ProresFourCc } from '../../../src/codec';
|
|
16
|
+
|
|
17
|
+
const PRORES_LOADED_SYMBOL = Symbol.for('@mediabunny/prores loaded');
|
|
18
|
+
if ((globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL]) {
|
|
19
|
+
Logging._error(
|
|
20
|
+
'[WARNING]\n@mediabunny/prores was loaded twice.'
|
|
21
|
+
+ ' This will likely cause the decoder not to work correctly.'
|
|
22
|
+
+ ' Check if multiple dependencies are importing different versions of @mediabunny/prores,'
|
|
23
|
+
+ ' or if something is being bundled incorrectly.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
(globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL] = true;
|
|
27
|
+
|
|
28
|
+
class ProresDecoder extends CustomVideoDecoder {
|
|
29
|
+
private decoder: Decoder | null = null;
|
|
30
|
+
private framePool: Frame[] = [];
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
33
|
+
static override supports(codec: VideoCodec, config: VideoDecoderConfig): boolean {
|
|
34
|
+
return codec === 'prores';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
static _supportedVideoFrameFormats: PixelFormat[] | null = null;
|
|
39
|
+
|
|
40
|
+
/** @internal */
|
|
41
|
+
static _determineSupportedVideoFrameFormats() {
|
|
42
|
+
const result: PixelFormat[] = [];
|
|
43
|
+
const data = new Uint8Array(32);
|
|
44
|
+
|
|
45
|
+
for (const format of PIXEL_FORMATS) {
|
|
46
|
+
try {
|
|
47
|
+
const frame = new VideoFrame(data, {
|
|
48
|
+
format: format as VideoPixelFormat,
|
|
49
|
+
codedWidth: 2,
|
|
50
|
+
codedHeight: 2,
|
|
51
|
+
timestamp: 0,
|
|
52
|
+
duration: 0,
|
|
53
|
+
});
|
|
54
|
+
frame.close();
|
|
55
|
+
|
|
56
|
+
result.push(format);
|
|
57
|
+
} catch {
|
|
58
|
+
// Format is not supported
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async init() {
|
|
66
|
+
if (typeof VideoFrame !== 'undefined') {
|
|
67
|
+
// Not all VideoFrame implementations support all pixel formats, therefore let's determine the supported set
|
|
68
|
+
ProresDecoder._supportedVideoFrameFormats ??= ProresDecoder._determineSupportedVideoFrameFormats();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const decoder = await Decoder.create({
|
|
72
|
+
proresFourCc: this.config.codec as ProresFourCc,
|
|
73
|
+
useSharedMemory: Decoder.canUseSharedMemory(),
|
|
74
|
+
allowedOutputFormats: ProresDecoder._supportedVideoFrameFormats ?? undefined,
|
|
75
|
+
});
|
|
76
|
+
if (decoder instanceof Error) {
|
|
77
|
+
throw decoder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.decoder = decoder;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async decode(packet: EncodedPacket) {
|
|
84
|
+
assert(this.decoder);
|
|
85
|
+
|
|
86
|
+
if (this.decoder.useSharedMemory) {
|
|
87
|
+
await this.runDecode(packet);
|
|
88
|
+
} else {
|
|
89
|
+
while (this.decoder.decodeQueueSize >= this.decoder.concurrency) {
|
|
90
|
+
await this.decoder.dequeued;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
void this.runDecode(packet)
|
|
94
|
+
.catch(error => this.onError(error));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async runDecode(packet: EncodedPacket) {
|
|
99
|
+
assert(this.decoder);
|
|
100
|
+
|
|
101
|
+
let frame: Frame;
|
|
102
|
+
if (this.framePool.length > 0) {
|
|
103
|
+
frame = this.framePool.shift()!;
|
|
104
|
+
} else {
|
|
105
|
+
frame = new Frame();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await this.decoder.decode(packet.data, frame);
|
|
109
|
+
this.framePool.push(frame);
|
|
110
|
+
|
|
111
|
+
if (result instanceof Error) {
|
|
112
|
+
throw result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (result.visibleHeight < result.codedHeight && isWebKit()) {
|
|
116
|
+
// WebKit has (had) a bug with displaying height-trimmed YUV frames, so we must compact the frame data a
|
|
117
|
+
// little https://bugs.webkit.org/show_bug.cgi?id=317524
|
|
118
|
+
this.trimCodedHeightToVisibleHeight(result);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const colorSpaceInit = {
|
|
122
|
+
primaries: result.colorPrimariesString as VideoColorPrimaries | undefined,
|
|
123
|
+
matrix: result.colorMatrixString as VideoMatrixCoefficients | undefined,
|
|
124
|
+
transfer: result.colorTransferString as VideoTransferCharacteristics | undefined,
|
|
125
|
+
fullRange: result.colorRangeFull,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let sample: VideoSample;
|
|
129
|
+
if (typeof VideoFrame !== 'undefined') {
|
|
130
|
+
// Create a VideoFrame directly; this avoids the frame data being copied twice
|
|
131
|
+
const frame = new VideoFrame(result.frameData, {
|
|
132
|
+
format: result.pixelFormat as VideoPixelFormat,
|
|
133
|
+
codedWidth: result.codedWidth,
|
|
134
|
+
codedHeight: result.codedHeight,
|
|
135
|
+
visibleRect: {
|
|
136
|
+
x: 0,
|
|
137
|
+
y: 0,
|
|
138
|
+
width: result.visibleWidth,
|
|
139
|
+
height: result.visibleHeight,
|
|
140
|
+
},
|
|
141
|
+
timestamp: packet.microsecondTimestamp,
|
|
142
|
+
duration: packet.microsecondDuration,
|
|
143
|
+
colorSpace: colorSpaceInit,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
sample = new VideoSample(frame, {
|
|
147
|
+
timestamp: packet.timestamp,
|
|
148
|
+
duration: packet.duration,
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
sample = new VideoSample(result.frameData, {
|
|
152
|
+
format: result.pixelFormat,
|
|
153
|
+
codedWidth: result.codedWidth,
|
|
154
|
+
codedHeight: result.codedHeight,
|
|
155
|
+
visibleRect: {
|
|
156
|
+
left: 0,
|
|
157
|
+
top: 0,
|
|
158
|
+
width: result.visibleWidth,
|
|
159
|
+
height: result.visibleHeight,
|
|
160
|
+
},
|
|
161
|
+
timestamp: packet.timestamp,
|
|
162
|
+
duration: packet.duration,
|
|
163
|
+
colorSpace: colorSpaceInit,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.onSample(sample);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private trimCodedHeightToVisibleHeight(result: FilledFrame) {
|
|
171
|
+
const bytesPerSample = result.pixelFormat.includes('P') ? 2 : 1;
|
|
172
|
+
const subWidth = result.pixelFormat.includes('444') ? 1 : 2;
|
|
173
|
+
const subHeight = result.pixelFormat.includes('420') ? 2 : 1;
|
|
174
|
+
|
|
175
|
+
const chromaCodedWidth = result.codedWidth / subWidth;
|
|
176
|
+
const chromaCodedHeight = result.codedHeight / subHeight;
|
|
177
|
+
const chromaVisibleHeight = Math.ceil(result.visibleHeight / subHeight);
|
|
178
|
+
|
|
179
|
+
const lumaCodedPixels = result.codedWidth * result.codedHeight;
|
|
180
|
+
const lumaVisiblePixels = result.codedWidth * result.visibleHeight;
|
|
181
|
+
const chromaCodedPixels = chromaCodedWidth * chromaCodedHeight;
|
|
182
|
+
const chromaVisiblePixels = chromaCodedWidth * chromaVisibleHeight;
|
|
183
|
+
|
|
184
|
+
// U
|
|
185
|
+
result.frameData.set(
|
|
186
|
+
result.frameData.subarray(
|
|
187
|
+
bytesPerSample * lumaCodedPixels,
|
|
188
|
+
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
|
|
189
|
+
),
|
|
190
|
+
bytesPerSample * lumaVisiblePixels,
|
|
191
|
+
);
|
|
192
|
+
// V
|
|
193
|
+
result.frameData.set(
|
|
194
|
+
result.frameData.subarray(
|
|
195
|
+
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
|
|
196
|
+
bytesPerSample * (lumaCodedPixels + 2 * chromaCodedPixels),
|
|
197
|
+
),
|
|
198
|
+
bytesPerSample * (lumaVisiblePixels + chromaVisiblePixels),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
result.codedHeight = result.visibleHeight;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async flush() {
|
|
205
|
+
assert(this.decoder);
|
|
206
|
+
|
|
207
|
+
while (this.decoder.decodeQueueSize > 0) {
|
|
208
|
+
await this.decoder.dequeued;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async close() {
|
|
213
|
+
assert(this.decoder);
|
|
214
|
+
|
|
215
|
+
await this.decoder.close();
|
|
216
|
+
|
|
217
|
+
for (const frame of this.framePool) {
|
|
218
|
+
frame.clear();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let registered = false;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Registers an Apple ProRes decoder which Mediabunny will then use automatically when applicable. Make sure to call
|
|
227
|
+
* this function before starting any decoding task.
|
|
228
|
+
*
|
|
229
|
+
* @group \@mediabunny/prores
|
|
230
|
+
* @public
|
|
231
|
+
*/
|
|
232
|
+
export const registerProresDecoder = () => {
|
|
233
|
+
if (registered) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
registered = true;
|
|
237
|
+
|
|
238
|
+
registerDecoder(ProresDecoder);
|
|
239
|
+
};
|