mediabunny 1.0.2 → 1.0.4

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 (188) hide show
  1. package/README.md +4 -4
  2. package/dist/mediabunny.d.ts +4 -2
  3. package/dist/modules/codec-data.d.ts +95 -0
  4. package/dist/modules/codec-data.d.ts.map +1 -0
  5. package/dist/modules/codec-data.js +1103 -0
  6. package/dist/modules/codec.d.ts +250 -0
  7. package/dist/modules/codec.d.ts.map +1 -0
  8. package/dist/modules/codec.js +1100 -0
  9. package/dist/modules/conversion.d.ts +158 -0
  10. package/dist/modules/conversion.d.ts.map +1 -0
  11. package/dist/modules/conversion.js +866 -0
  12. package/dist/modules/custom-coder.d.ts +119 -0
  13. package/dist/modules/custom-coder.d.ts.map +1 -0
  14. package/dist/modules/custom-coder.js +91 -0
  15. package/dist/modules/demuxer.d.ts +17 -0
  16. package/dist/modules/demuxer.d.ts.map +1 -0
  17. package/dist/modules/demuxer.js +12 -0
  18. package/dist/modules/index.d.ts +25 -0
  19. package/dist/modules/index.d.ts.map +1 -0
  20. package/dist/modules/index.js +24 -0
  21. package/dist/modules/input-format.d.ts +121 -0
  22. package/dist/modules/input-format.d.ts.map +1 -0
  23. package/dist/modules/input-format.js +333 -0
  24. package/dist/modules/input-track.d.ts +138 -0
  25. package/dist/modules/input-track.d.ts.map +1 -0
  26. package/dist/modules/input-track.js +230 -0
  27. package/dist/modules/input.d.ts +55 -0
  28. package/dist/modules/input.d.ts.map +1 -0
  29. package/dist/modules/input.js +104 -0
  30. package/dist/modules/isobmff/isobmff-boxes.d.ts +202 -0
  31. package/dist/modules/isobmff/isobmff-boxes.d.ts.map +1 -0
  32. package/dist/modules/isobmff/isobmff-boxes.js +1111 -0
  33. package/dist/modules/isobmff/isobmff-demuxer.d.ts +163 -0
  34. package/dist/modules/isobmff/isobmff-demuxer.d.ts.map +1 -0
  35. package/dist/modules/isobmff/isobmff-demuxer.js +2080 -0
  36. package/dist/modules/isobmff/isobmff-misc.d.ts +14 -0
  37. package/dist/modules/isobmff/isobmff-misc.d.ts.map +1 -0
  38. package/dist/modules/isobmff/isobmff-misc.js +20 -0
  39. package/dist/modules/isobmff/isobmff-muxer.d.ts +142 -0
  40. package/dist/modules/isobmff/isobmff-muxer.d.ts.map +1 -0
  41. package/dist/modules/isobmff/isobmff-muxer.js +858 -0
  42. package/dist/modules/isobmff/isobmff-reader.d.ts +36 -0
  43. package/dist/modules/isobmff/isobmff-reader.d.ts.map +1 -0
  44. package/dist/modules/isobmff/isobmff-reader.js +105 -0
  45. package/dist/modules/matroska/ebml.d.ts +156 -0
  46. package/dist/modules/matroska/ebml.d.ts.map +1 -0
  47. package/dist/modules/matroska/ebml.js +529 -0
  48. package/dist/modules/matroska/matroska-demuxer.d.ts +119 -0
  49. package/dist/modules/matroska/matroska-demuxer.d.ts.map +1 -0
  50. package/dist/modules/matroska/matroska-demuxer.js +1330 -0
  51. package/dist/modules/matroska/matroska-misc.d.ts +14 -0
  52. package/dist/modules/matroska/matroska-misc.d.ts.map +1 -0
  53. package/dist/modules/matroska/matroska-misc.js +20 -0
  54. package/dist/modules/matroska/matroska-muxer.d.ts +71 -0
  55. package/dist/modules/matroska/matroska-muxer.d.ts.map +1 -0
  56. package/dist/modules/matroska/matroska-muxer.js +729 -0
  57. package/dist/modules/media-sink.d.ts +272 -0
  58. package/dist/modules/media-sink.d.ts.map +1 -0
  59. package/dist/modules/media-sink.js +1264 -0
  60. package/dist/modules/media-source.d.ts +230 -0
  61. package/dist/modules/media-source.d.ts.map +1 -0
  62. package/dist/modules/media-source.js +1095 -0
  63. package/dist/modules/misc.d.ts +127 -0
  64. package/dist/modules/misc.d.ts.map +1 -0
  65. package/dist/modules/misc.js +462 -0
  66. package/dist/modules/mp3/mp3-demuxer.d.ts +32 -0
  67. package/dist/modules/mp3/mp3-demuxer.d.ts.map +1 -0
  68. package/dist/modules/mp3/mp3-demuxer.js +160 -0
  69. package/dist/modules/mp3/mp3-misc.d.ts +37 -0
  70. package/dist/modules/mp3/mp3-misc.d.ts.map +1 -0
  71. package/dist/modules/mp3/mp3-misc.js +122 -0
  72. package/dist/modules/mp3/mp3-muxer.d.ts +27 -0
  73. package/dist/modules/mp3/mp3-muxer.d.ts.map +1 -0
  74. package/dist/modules/mp3/mp3-muxer.js +112 -0
  75. package/dist/modules/mp3/mp3-reader.d.ts +25 -0
  76. package/dist/modules/mp3/mp3-reader.d.ts.map +1 -0
  77. package/dist/modules/mp3/mp3-reader.js +73 -0
  78. package/dist/modules/mp3/mp3-writer.d.ts +30 -0
  79. package/dist/modules/mp3/mp3-writer.d.ts.map +1 -0
  80. package/dist/modules/mp3/mp3-writer.js +72 -0
  81. package/dist/modules/muxer.d.ts +32 -0
  82. package/dist/modules/muxer.d.ts.map +1 -0
  83. package/dist/modules/muxer.js +50 -0
  84. package/dist/modules/ogg/ogg-demuxer.d.ts +53 -0
  85. package/dist/modules/ogg/ogg-demuxer.d.ts.map +1 -0
  86. package/dist/modules/ogg/ogg-demuxer.js +701 -0
  87. package/dist/modules/ogg/ogg-misc.d.ts +27 -0
  88. package/dist/modules/ogg/ogg-misc.d.ts.map +1 -0
  89. package/dist/modules/ogg/ogg-misc.js +78 -0
  90. package/dist/modules/ogg/ogg-muxer.d.ts +58 -0
  91. package/dist/modules/ogg/ogg-muxer.d.ts.map +1 -0
  92. package/dist/modules/ogg/ogg-muxer.js +345 -0
  93. package/dist/modules/ogg/ogg-reader.d.ts +37 -0
  94. package/dist/modules/ogg/ogg-reader.d.ts.map +1 -0
  95. package/dist/modules/ogg/ogg-reader.js +104 -0
  96. package/dist/modules/output-format.d.ts +285 -0
  97. package/dist/modules/output-format.d.ts.map +1 -0
  98. package/dist/modules/output-format.js +385 -0
  99. package/dist/modules/output.d.ts +134 -0
  100. package/dist/modules/output.d.ts.map +1 -0
  101. package/dist/modules/output.js +269 -0
  102. package/dist/modules/packet.d.ts +86 -0
  103. package/dist/modules/packet.d.ts.map +1 -0
  104. package/dist/modules/packet.js +133 -0
  105. package/dist/modules/pcm.d.ts +12 -0
  106. package/dist/modules/pcm.d.ts.map +1 -0
  107. package/dist/modules/pcm.js +85 -0
  108. package/dist/modules/reader.d.ts +40 -0
  109. package/dist/modules/reader.d.ts.map +1 -0
  110. package/dist/modules/reader.js +153 -0
  111. package/dist/modules/sample.d.ts +186 -0
  112. package/dist/modules/sample.d.ts.map +1 -0
  113. package/dist/modules/sample.js +798 -0
  114. package/dist/modules/source.d.ts +76 -0
  115. package/dist/modules/source.d.ts.map +1 -0
  116. package/dist/modules/source.js +183 -0
  117. package/dist/modules/subtitles.d.ts +37 -0
  118. package/dist/modules/subtitles.d.ts.map +1 -0
  119. package/dist/modules/subtitles.js +89 -0
  120. package/dist/modules/target.d.ts +58 -0
  121. package/dist/modules/target.d.ts.map +1 -0
  122. package/dist/modules/target.js +63 -0
  123. package/dist/modules/wave/riff-reader.d.ts +19 -0
  124. package/dist/modules/wave/riff-reader.d.ts.map +1 -0
  125. package/dist/modules/wave/riff-reader.js +38 -0
  126. package/dist/modules/wave/riff-writer.d.ts +18 -0
  127. package/dist/modules/wave/riff-writer.d.ts.map +1 -0
  128. package/dist/modules/wave/riff-writer.js +25 -0
  129. package/dist/modules/wave/wave-demuxer.d.ts +42 -0
  130. package/dist/modules/wave/wave-demuxer.d.ts.map +1 -0
  131. package/dist/modules/wave/wave-demuxer.js +239 -0
  132. package/dist/modules/wave/wave-muxer.d.ts +27 -0
  133. package/dist/modules/wave/wave-muxer.d.ts.map +1 -0
  134. package/dist/modules/wave/wave-muxer.js +108 -0
  135. package/dist/modules/writer.d.ts +85 -0
  136. package/dist/modules/writer.d.ts.map +1 -0
  137. package/dist/modules/writer.js +347 -0
  138. package/package.json +14 -10
  139. package/src/codec-data.ts +1394 -0
  140. package/src/codec.ts +1392 -0
  141. package/src/conversion.ts +1199 -0
  142. package/src/custom-coder.ts +162 -0
  143. package/src/demuxer.ts +22 -0
  144. package/src/index.ts +148 -0
  145. package/src/input-format.ts +386 -0
  146. package/src/input-track.ts +341 -0
  147. package/src/input.ts +138 -0
  148. package/src/isobmff/isobmff-boxes.ts +1365 -0
  149. package/src/isobmff/isobmff-demuxer.ts +2646 -0
  150. package/src/isobmff/isobmff-misc.ts +29 -0
  151. package/src/isobmff/isobmff-muxer.ts +1210 -0
  152. package/src/isobmff/isobmff-reader.ts +137 -0
  153. package/src/matroska/ebml.ts +595 -0
  154. package/src/matroska/matroska-demuxer.ts +1677 -0
  155. package/src/matroska/matroska-misc.ts +29 -0
  156. package/src/matroska/matroska-muxer.ts +999 -0
  157. package/src/media-sink.ts +1577 -0
  158. package/src/media-source.ts +1406 -0
  159. package/src/misc.ts +584 -0
  160. package/src/mp3/mp3-demuxer.ts +228 -0
  161. package/src/mp3/mp3-misc.ts +154 -0
  162. package/src/mp3/mp3-muxer.ts +153 -0
  163. package/src/mp3/mp3-reader.ts +93 -0
  164. package/src/mp3/mp3-writer.ts +112 -0
  165. package/src/muxer.ts +88 -0
  166. package/src/ogg/ogg-demuxer.ts +985 -0
  167. package/src/ogg/ogg-misc.ts +116 -0
  168. package/src/ogg/ogg-muxer.ts +480 -0
  169. package/src/ogg/ogg-reader.ts +143 -0
  170. package/src/output-format.ts +689 -0
  171. package/src/output.ts +427 -0
  172. package/src/packet.ts +180 -0
  173. package/src/pcm.ts +112 -0
  174. package/src/reader.ts +205 -0
  175. package/src/sample.ts +1029 -0
  176. package/src/source.ts +279 -0
  177. package/src/subtitles.ts +131 -0
  178. package/src/target.ts +106 -0
  179. package/src/tsconfig.json +13 -0
  180. package/src/wave/riff-reader.ts +48 -0
  181. package/src/wave/riff-writer.ts +30 -0
  182. package/src/wave/wave-demuxer.ts +317 -0
  183. package/src/wave/wave-muxer.ts +145 -0
  184. package/src/writer.ts +461 -0
  185. /package/dist/{mediabunny.js → bundles/mediabunny.cjs} +0 -0
  186. /package/dist/{mediabunny.min.js → bundles/mediabunny.min.cjs} +0 -0
  187. /package/dist/{mediabunny.min.mjs → bundles/mediabunny.min.mjs} +0 -0
  188. /package/dist/{mediabunny.mjs → bundles/mediabunny.mjs} +0 -0
@@ -0,0 +1,798 @@
1
+ /*!
2
+ * Copyright (c) 2025-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
+ import { assert, clamp, isAllowSharedBufferSource, SECOND_TO_MICROSECOND_FACTOR, toDataView, toUint8Array, } from './misc.js';
9
+ /**
10
+ * Represents a raw, unencoded video sample (frame). Mainly used as an expressive wrapper around WebCodecs API's
11
+ * VideoFrame, but can also be used standalone.
12
+ * @public
13
+ */
14
+ export class VideoSample {
15
+ /** The width of the frame in pixels after rotation. */
16
+ get displayWidth() {
17
+ return this.rotation % 180 === 0 ? this.codedWidth : this.codedHeight;
18
+ }
19
+ /** The height of the frame in pixels after rotation. */
20
+ get displayHeight() {
21
+ return this.rotation % 180 === 0 ? this.codedHeight : this.codedWidth;
22
+ }
23
+ /** The presentation timestamp of the frame in microseconds. */
24
+ get microsecondTimestamp() {
25
+ return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
26
+ }
27
+ /** The duration of the frame in microseconds. */
28
+ get microsecondDuration() {
29
+ return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
30
+ }
31
+ constructor(data, init) {
32
+ /** @internal */
33
+ this._closed = false;
34
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
35
+ if (!init || typeof init !== 'object') {
36
+ throw new TypeError('init must be an object.');
37
+ }
38
+ if (!('format' in init) || typeof init.format !== 'string') {
39
+ throw new TypeError('init.format must be a string.');
40
+ }
41
+ if (!Number.isInteger(init.codedWidth) || init.codedWidth <= 0) {
42
+ throw new TypeError('init.codedWidth must be a positive integer.');
43
+ }
44
+ if (!Number.isInteger(init.codedHeight) || init.codedHeight <= 0) {
45
+ throw new TypeError('init.codedHeight must be a positive integer.');
46
+ }
47
+ if (init.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) {
48
+ throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.');
49
+ }
50
+ if (!Number.isFinite(init.timestamp)) {
51
+ throw new TypeError('init.timestamp must be a number.');
52
+ }
53
+ if (init.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) {
54
+ throw new TypeError('init.duration, when provided, must be a non-negative number.');
55
+ }
56
+ this._data = toUint8Array(data).slice(); // Copy it
57
+ this.format = init.format;
58
+ this.codedWidth = init.codedWidth;
59
+ this.codedHeight = init.codedHeight;
60
+ this.rotation = init.rotation ?? 0;
61
+ this.timestamp = init.timestamp;
62
+ this.duration = init.duration ?? 0;
63
+ this.colorSpace = new VideoColorSpace(init.colorSpace);
64
+ }
65
+ else if (typeof VideoFrame !== 'undefined' && data instanceof VideoFrame) {
66
+ if (init?.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) {
67
+ throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.');
68
+ }
69
+ if (init?.timestamp !== undefined && !Number.isFinite(init?.timestamp)) {
70
+ throw new TypeError('init.timestamp, when provided, must be a number.');
71
+ }
72
+ if (init?.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) {
73
+ throw new TypeError('init.duration, when provided, must be a non-negative number.');
74
+ }
75
+ this._data = data;
76
+ this.format = data.format;
77
+ this.codedWidth = data.codedWidth;
78
+ this.codedHeight = data.codedHeight;
79
+ this.rotation = init?.rotation ?? 0;
80
+ this.timestamp = init?.timestamp ?? data.timestamp / 1e6;
81
+ this.duration = init?.duration ?? (data.duration ?? 0) / 1e6;
82
+ this.colorSpace = data.colorSpace;
83
+ }
84
+ else if ((typeof HTMLImageElement !== 'undefined' && data instanceof HTMLImageElement)
85
+ || (typeof SVGImageElement !== 'undefined' && data instanceof SVGImageElement)
86
+ || (typeof ImageBitmap !== 'undefined' && data instanceof ImageBitmap)
87
+ || (typeof HTMLVideoElement !== 'undefined' && data instanceof HTMLVideoElement)
88
+ || (typeof HTMLCanvasElement !== 'undefined' && data instanceof HTMLCanvasElement)
89
+ || (typeof OffscreenCanvas !== 'undefined' && data instanceof OffscreenCanvas)) {
90
+ if (!init || typeof init !== 'object') {
91
+ throw new TypeError('init must be an object.');
92
+ }
93
+ if (init.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) {
94
+ throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.');
95
+ }
96
+ if (!Number.isFinite(init.timestamp)) {
97
+ throw new TypeError('init.timestamp must be a number.');
98
+ }
99
+ if (init.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) {
100
+ throw new TypeError('init.duration, when provided, must be a non-negative number.');
101
+ }
102
+ if (typeof VideoFrame !== 'undefined') {
103
+ return new VideoSample(new VideoFrame(data, {
104
+ timestamp: Math.trunc(init.timestamp * SECOND_TO_MICROSECOND_FACTOR),
105
+ duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR),
106
+ }), init);
107
+ }
108
+ let width = 0;
109
+ let height = 0;
110
+ // Determine the dimensions of the thing
111
+ if ('naturalWidth' in data) {
112
+ width = data.naturalWidth;
113
+ height = data.naturalHeight;
114
+ }
115
+ else if ('videoWidth' in data) {
116
+ width = data.videoWidth;
117
+ height = data.videoHeight;
118
+ }
119
+ else if ('width' in data) {
120
+ width = Number(data.width);
121
+ height = Number(data.height);
122
+ }
123
+ if (!width || !height) {
124
+ throw new TypeError('Could not determine dimensions.');
125
+ }
126
+ const canvas = new OffscreenCanvas(width, height);
127
+ const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true });
128
+ assert(context);
129
+ // Draw it to a canvas
130
+ context.drawImage(data, 0, 0);
131
+ this._data = canvas;
132
+ this.format = 'RGBX';
133
+ this.codedWidth = width;
134
+ this.codedHeight = height;
135
+ this.rotation = init.rotation ?? 0;
136
+ this.timestamp = init.timestamp;
137
+ this.duration = init.duration ?? 0;
138
+ this.colorSpace = new VideoColorSpace({
139
+ matrix: 'rgb',
140
+ primaries: 'bt709',
141
+ transfer: 'iec61966-2-1',
142
+ fullRange: true,
143
+ });
144
+ }
145
+ else {
146
+ throw new TypeError('Invalid data type: Must be a BufferSource or CanvasImageSource.');
147
+ }
148
+ }
149
+ /** Clones this video sample. */
150
+ clone() {
151
+ if (this._closed) {
152
+ throw new Error('VideoSample is closed.');
153
+ }
154
+ assert(this._data !== null);
155
+ if (isVideoFrame(this._data)) {
156
+ return new VideoSample(this._data.clone(), {
157
+ timestamp: this.timestamp,
158
+ duration: this.duration,
159
+ });
160
+ }
161
+ else if (this._data instanceof Uint8Array) {
162
+ return new VideoSample(this._data.slice(), {
163
+ format: this.format,
164
+ codedWidth: this.codedWidth,
165
+ codedHeight: this.codedHeight,
166
+ timestamp: this.timestamp,
167
+ duration: this.duration,
168
+ colorSpace: this.colorSpace,
169
+ });
170
+ }
171
+ else {
172
+ return new VideoSample(this._data, {
173
+ format: this.format,
174
+ codedWidth: this.codedWidth,
175
+ codedHeight: this.codedHeight,
176
+ timestamp: this.timestamp,
177
+ duration: this.duration,
178
+ colorSpace: this.colorSpace,
179
+ });
180
+ }
181
+ }
182
+ /**
183
+ * Closes this video sample, releasing held resources. Video samples should be closed as soon as they are not
184
+ * needed anymore.
185
+ */
186
+ close() {
187
+ if (this._closed) {
188
+ return;
189
+ }
190
+ if (isVideoFrame(this._data)) {
191
+ this._data.close();
192
+ }
193
+ else {
194
+ this._data = null; // GC that shit
195
+ }
196
+ this._closed = true;
197
+ }
198
+ /** Returns the number of bytes required to hold this video sample's pixel data. */
199
+ allocationSize() {
200
+ if (this._closed) {
201
+ throw new Error('VideoSample is closed.');
202
+ }
203
+ assert(this._data !== null);
204
+ if (isVideoFrame(this._data)) {
205
+ return this._data.allocationSize();
206
+ }
207
+ else if (this._data instanceof Uint8Array) {
208
+ return this._data.byteLength;
209
+ }
210
+ else {
211
+ return this.codedWidth * this.codedHeight * 4; // RGBX
212
+ }
213
+ }
214
+ /** Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView. */
215
+ async copyTo(destination) {
216
+ if (!isAllowSharedBufferSource(destination)) {
217
+ throw new TypeError('destination must be an ArrayBuffer or an ArrayBuffer view.');
218
+ }
219
+ if (this._closed) {
220
+ throw new Error('VideoSample is closed.');
221
+ }
222
+ assert(this._data !== null);
223
+ if (isVideoFrame(this._data)) {
224
+ await this._data.copyTo(destination);
225
+ }
226
+ else if (this._data instanceof Uint8Array) {
227
+ const dest = toUint8Array(destination);
228
+ dest.set(this._data);
229
+ }
230
+ else {
231
+ const canvas = this._data;
232
+ const context = canvas.getContext('2d', { alpha: false });
233
+ assert(context);
234
+ const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
235
+ const dest = toUint8Array(destination);
236
+ dest.set(imageData.data);
237
+ }
238
+ }
239
+ /**
240
+ * Converts this video sample to a VideoFrame for use with the WebCodecs API. The VideoFrame returned by this
241
+ * method *must* be closed separately from this video sample.
242
+ */
243
+ toVideoFrame() {
244
+ if (this._closed) {
245
+ throw new Error('VideoSample is closed.');
246
+ }
247
+ assert(this._data !== null);
248
+ if (isVideoFrame(this._data)) {
249
+ return new VideoFrame(this._data, {
250
+ timestamp: this.microsecondTimestamp,
251
+ duration: this.microsecondDuration || undefined, // Drag 0 duration to undefined, glitches some codecs
252
+ });
253
+ }
254
+ else if (this._data instanceof Uint8Array) {
255
+ return new VideoFrame(this._data, {
256
+ format: this.format,
257
+ codedWidth: this.codedWidth,
258
+ codedHeight: this.codedHeight,
259
+ timestamp: this.microsecondTimestamp,
260
+ duration: this.microsecondDuration,
261
+ colorSpace: this.colorSpace,
262
+ });
263
+ }
264
+ else {
265
+ return new VideoFrame(this._data, {
266
+ timestamp: this.microsecondTimestamp,
267
+ duration: this.microsecondDuration,
268
+ });
269
+ }
270
+ }
271
+ /**
272
+ * Draws the video sample to a 2D canvas context. Rotation metadata will be taken into account.
273
+ *
274
+ * @param dx - The x-coordinate in the destination canvas at which to place the top-left corner of the source image.
275
+ * @param dy - The y-coordinate in the destination canvas at which to place the top-left corner of the source image.
276
+ * @param dWidth - The width in pixels with which to draw the image in the destination canvas.
277
+ * @param dHeight - The height in pixels with which to draw the image in the destination canvas.
278
+ */
279
+ draw(context, dx, dy, dWidth = this.displayWidth, dHeight = this.displayHeight) {
280
+ if (!((typeof CanvasRenderingContext2D !== 'undefined' && context instanceof CanvasRenderingContext2D)
281
+ || (typeof OffscreenCanvasRenderingContext2D !== 'undefined'
282
+ && context instanceof OffscreenCanvasRenderingContext2D))) {
283
+ throw new TypeError('context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.');
284
+ }
285
+ if (!Number.isFinite(dx)) {
286
+ throw new TypeError('dx must be a number.');
287
+ }
288
+ if (!Number.isFinite(dy)) {
289
+ throw new TypeError('dy must be a number.');
290
+ }
291
+ if (!Number.isFinite(dWidth) || dWidth < 0) {
292
+ throw new TypeError('dWidth must be a non-negative number.');
293
+ }
294
+ if (!Number.isFinite(dHeight) || dHeight < 0) {
295
+ throw new TypeError('dHeight must be a non-negative number.');
296
+ }
297
+ if (this._closed) {
298
+ throw new Error('VideoSample is closed.');
299
+ }
300
+ const source = this.toCanvasImageSource();
301
+ context.save();
302
+ const centerX = dx + dWidth / 2;
303
+ const centerY = dy + dHeight / 2;
304
+ context.translate(centerX, centerY);
305
+ context.rotate(this.rotation * Math.PI / 180);
306
+ const aspectRatioChange = this.rotation % 180 === 0 ? 1 : dWidth / dHeight;
307
+ // Scale to compensate for aspect ratio changes when rotated
308
+ context.scale(1 / aspectRatioChange, aspectRatioChange);
309
+ context.drawImage(source, -dWidth / 2, -dHeight / 2, dWidth, dHeight);
310
+ // Restore the previous transformation state
311
+ context.restore();
312
+ }
313
+ /**
314
+ * Converts this video sample to a CanvasImageSource for drawing to a canvas.
315
+ *
316
+ * You must use the value returned by this method immediately, as any VideoFrame created internally will
317
+ * automatically be closed in the next microtask.
318
+ */
319
+ toCanvasImageSource() {
320
+ if (this._closed) {
321
+ throw new Error('VideoSample is closed.');
322
+ }
323
+ assert(this._data !== null);
324
+ if (this._data instanceof Uint8Array) {
325
+ // Requires VideoFrame to be defined
326
+ const videoFrame = this.toVideoFrame();
327
+ queueMicrotask(() => videoFrame.close()); // Let's automatically close the frame in the next microtask
328
+ return videoFrame;
329
+ }
330
+ else {
331
+ return this._data;
332
+ }
333
+ }
334
+ /** Sets the rotation metadata of this video sample. */
335
+ setRotation(newRotation) {
336
+ if (![0, 90, 180, 270].includes(newRotation)) {
337
+ throw new TypeError('newRotation must be 0, 90, 180, or 270.');
338
+ }
339
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
340
+ this.rotation = newRotation;
341
+ }
342
+ /** Sets the presentation timestamp of this video sample, in seconds. */
343
+ setTimestamp(newTimestamp) {
344
+ if (!Number.isFinite(newTimestamp)) {
345
+ throw new TypeError('newTimestamp must be a number.');
346
+ }
347
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
348
+ this.timestamp = newTimestamp;
349
+ }
350
+ /** Sets the duration of this video sample, in seconds. */
351
+ setDuration(newDuration) {
352
+ if (!Number.isFinite(newDuration) || newDuration < 0) {
353
+ throw new TypeError('newDuration must be a non-negative number.');
354
+ }
355
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
356
+ this.duration = newDuration;
357
+ }
358
+ }
359
+ const isVideoFrame = (x) => {
360
+ return typeof VideoFrame !== 'undefined' && x instanceof VideoFrame;
361
+ };
362
+ const AUDIO_SAMPLE_FORMATS = new Set(['f32', 'f32-planar', 's16', 's16-planar', 's32', 's32-planar', 'u8', 'u8-planar']);
363
+ /**
364
+ * Represents a raw, unencoded audio sample. Mainly used as an expressive wrapper around WebCodecs API's AudioData,
365
+ * but can also be used standalone.
366
+ * @public
367
+ */
368
+ export class AudioSample {
369
+ /** The presentation timestamp of the sample in microseconds. */
370
+ get microsecondTimestamp() {
371
+ return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
372
+ }
373
+ /** The duration of the sample in microseconds. */
374
+ get microsecondDuration() {
375
+ return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
376
+ }
377
+ constructor(init) {
378
+ /** @internal */
379
+ this._closed = false;
380
+ if (isAudioData(init)) {
381
+ if (init.format === null) {
382
+ throw new TypeError('AudioData with null format is not supported.');
383
+ }
384
+ this._data = init;
385
+ this.format = init.format;
386
+ this.sampleRate = init.sampleRate;
387
+ this.numberOfFrames = init.numberOfFrames;
388
+ this.numberOfChannels = init.numberOfChannels;
389
+ this.timestamp = init.timestamp / 1e6;
390
+ this.duration = init.numberOfFrames / init.sampleRate;
391
+ }
392
+ else {
393
+ if (!init || typeof init !== 'object') {
394
+ throw new TypeError('Invalid AudioDataInit: must be an object.');
395
+ }
396
+ if (!AUDIO_SAMPLE_FORMATS.has(init.format)) {
397
+ throw new TypeError('Invalid AudioDataInit: invalid format.');
398
+ }
399
+ if (!Number.isFinite(init.sampleRate) || init.sampleRate <= 0) {
400
+ throw new TypeError('Invalid AudioDataInit: sampleRate must be > 0.');
401
+ }
402
+ if (!Number.isInteger(init.numberOfChannels) || init.numberOfChannels === 0) {
403
+ throw new TypeError('Invalid AudioDataInit: numberOfChannels must be an integer > 0.');
404
+ }
405
+ if (!Number.isFinite(init?.timestamp)) {
406
+ throw new TypeError('init.timestamp must be a number.');
407
+ }
408
+ const numberOfFrames = init.data.byteLength / (getBytesPerSample(init.format) * init.numberOfChannels);
409
+ if (!Number.isInteger(numberOfFrames)) {
410
+ throw new TypeError('Invalid AudioDataInit: data size is not a multiple of frame size.');
411
+ }
412
+ this.format = init.format;
413
+ this.sampleRate = init.sampleRate;
414
+ this.numberOfFrames = numberOfFrames;
415
+ this.numberOfChannels = init.numberOfChannels;
416
+ this.timestamp = init.timestamp;
417
+ this.duration = numberOfFrames / init.sampleRate;
418
+ let dataBuffer;
419
+ if (init.data instanceof ArrayBuffer) {
420
+ dataBuffer = new Uint8Array(init.data);
421
+ }
422
+ else if (ArrayBuffer.isView(init.data)) {
423
+ dataBuffer = new Uint8Array(init.data.buffer, init.data.byteOffset, init.data.byteLength);
424
+ }
425
+ else {
426
+ throw new TypeError('Invalid AudioDataInit: data is not a BufferSource.');
427
+ }
428
+ const expectedSize = this.numberOfFrames * this.numberOfChannels * getBytesPerSample(this.format);
429
+ if (dataBuffer.byteLength < expectedSize) {
430
+ throw new TypeError('Invalid AudioDataInit: insufficient data size.');
431
+ }
432
+ this._data = dataBuffer;
433
+ }
434
+ }
435
+ /** Returns the number of bytes required to hold the audio sample's data as specified by the given options. */
436
+ allocationSize(options) {
437
+ if (!options || typeof options !== 'object') {
438
+ throw new TypeError('options must be an object.');
439
+ }
440
+ if (!Number.isInteger(options.planeIndex) || options.planeIndex < 0) {
441
+ throw new TypeError('planeIndex must be a non-negative integer.');
442
+ }
443
+ if (options.format !== undefined && !AUDIO_SAMPLE_FORMATS.has(options.format)) {
444
+ throw new TypeError('Invalid format.');
445
+ }
446
+ if (options.frameOffset !== undefined && (!Number.isInteger(options.frameOffset) || options.frameOffset < 0)) {
447
+ throw new TypeError('frameOffset must be a non-negative integer.');
448
+ }
449
+ if (options.frameCount !== undefined && (!Number.isInteger(options.frameCount) || options.frameCount < 0)) {
450
+ throw new TypeError('frameCount must be a non-negative integer.');
451
+ }
452
+ if (this._closed) {
453
+ throw new Error('AudioSample is closed.');
454
+ }
455
+ const destFormat = options.format ?? this.format;
456
+ const frameOffset = options.frameOffset ?? 0;
457
+ if (frameOffset >= this.numberOfFrames) {
458
+ throw new RangeError('frameOffset out of range');
459
+ }
460
+ const copyFrameCount = options.frameCount !== undefined ? options.frameCount : (this.numberOfFrames - frameOffset);
461
+ if (copyFrameCount > (this.numberOfFrames - frameOffset)) {
462
+ throw new RangeError('frameCount out of range');
463
+ }
464
+ const bytesPerSample = getBytesPerSample(destFormat);
465
+ const isPlanar = formatIsPlanar(destFormat);
466
+ if (isPlanar && options.planeIndex >= this.numberOfChannels) {
467
+ throw new RangeError('planeIndex out of range');
468
+ }
469
+ if (!isPlanar && options.planeIndex !== 0) {
470
+ throw new RangeError('planeIndex out of range');
471
+ }
472
+ const elementCount = isPlanar ? copyFrameCount : copyFrameCount * this.numberOfChannels;
473
+ return elementCount * bytesPerSample;
474
+ }
475
+ /** Copies the audio sample's data to an ArrayBuffer or ArrayBufferView as specified by the given options. */
476
+ copyTo(destination, options) {
477
+ if (!isAllowSharedBufferSource(destination)) {
478
+ throw new TypeError('destination must be an ArrayBuffer or an ArrayBuffer view.');
479
+ }
480
+ if (!options || typeof options !== 'object') {
481
+ throw new TypeError('options must be an object.');
482
+ }
483
+ if (!Number.isInteger(options.planeIndex) || options.planeIndex < 0) {
484
+ throw new TypeError('planeIndex must be a non-negative integer.');
485
+ }
486
+ if (options.format !== undefined && !AUDIO_SAMPLE_FORMATS.has(options.format)) {
487
+ throw new TypeError('Invalid format.');
488
+ }
489
+ if (options.frameOffset !== undefined && (!Number.isInteger(options.frameOffset) || options.frameOffset < 0)) {
490
+ throw new TypeError('frameOffset must be a non-negative integer.');
491
+ }
492
+ if (options.frameCount !== undefined && (!Number.isInteger(options.frameCount) || options.frameCount < 0)) {
493
+ throw new TypeError('frameCount must be a non-negative integer.');
494
+ }
495
+ if (this._closed) {
496
+ throw new Error('AudioSample is closed.');
497
+ }
498
+ const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
499
+ const destFormat = format ?? this.format;
500
+ if (!destFormat)
501
+ throw new Error('Destination format not determined');
502
+ const numFrames = this.numberOfFrames;
503
+ const numChannels = this.numberOfChannels;
504
+ const frameOffset = optFrameOffset ?? 0;
505
+ if (frameOffset >= numFrames) {
506
+ throw new RangeError('frameOffset out of range');
507
+ }
508
+ const copyFrameCount = optFrameCount !== undefined ? optFrameCount : (numFrames - frameOffset);
509
+ if (copyFrameCount > (numFrames - frameOffset)) {
510
+ throw new RangeError('frameCount out of range');
511
+ }
512
+ const destBytesPerSample = getBytesPerSample(destFormat);
513
+ const destIsPlanar = formatIsPlanar(destFormat);
514
+ if (destIsPlanar && planeIndex >= numChannels) {
515
+ throw new RangeError('planeIndex out of range');
516
+ }
517
+ if (!destIsPlanar && planeIndex !== 0) {
518
+ throw new RangeError('planeIndex out of range');
519
+ }
520
+ const destElementCount = destIsPlanar ? copyFrameCount : copyFrameCount * numChannels;
521
+ const requiredSize = destElementCount * destBytesPerSample;
522
+ if (destination.byteLength < requiredSize) {
523
+ throw new RangeError('Destination buffer is too small');
524
+ }
525
+ const destView = toDataView(destination);
526
+ const writeFn = getWriteFunction(destFormat);
527
+ if (isAudioData(this._data)) {
528
+ if (destIsPlanar) {
529
+ if (destFormat === 'f32-planar') {
530
+ // Simple, since the browser must support f32-planar, we can just delegate here
531
+ this._data.copyTo(destination, {
532
+ planeIndex,
533
+ frameOffset,
534
+ frameCount: copyFrameCount,
535
+ format: 'f32-planar',
536
+ });
537
+ }
538
+ else {
539
+ // Allocate temporary buffer for f32-planar data
540
+ const tempBuffer = new ArrayBuffer(copyFrameCount * 4);
541
+ const tempArray = new Float32Array(tempBuffer);
542
+ this._data.copyTo(tempArray, {
543
+ planeIndex,
544
+ frameOffset,
545
+ frameCount: copyFrameCount,
546
+ format: 'f32-planar',
547
+ });
548
+ // Convert each f32 sample to destination format
549
+ const tempView = new DataView(tempBuffer);
550
+ for (let i = 0; i < copyFrameCount; i++) {
551
+ const destOffset = i * destBytesPerSample;
552
+ const sample = tempView.getFloat32(i * 4, true);
553
+ writeFn(destView, destOffset, sample);
554
+ }
555
+ }
556
+ }
557
+ else {
558
+ // Destination is interleaved.
559
+ // Allocate a temporary Float32Array to hold one channel's worth of data.
560
+ const numCh = numChannels;
561
+ const temp = new Float32Array(copyFrameCount);
562
+ for (let ch = 0; ch < numCh; ch++) {
563
+ this._data.copyTo(temp, {
564
+ planeIndex: ch,
565
+ frameOffset,
566
+ frameCount: copyFrameCount,
567
+ format: 'f32-planar',
568
+ });
569
+ for (let i = 0; i < copyFrameCount; i++) {
570
+ const destIndex = i * numCh + ch;
571
+ const destOffset = destIndex * destBytesPerSample;
572
+ writeFn(destView, destOffset, temp[i]);
573
+ }
574
+ }
575
+ }
576
+ }
577
+ else {
578
+ // Branch for Uint8Array data (non-AudioData)
579
+ const uint8Data = this._data;
580
+ const srcView = new DataView(uint8Data.buffer, uint8Data.byteOffset, uint8Data.byteLength);
581
+ const srcFormat = this.format;
582
+ const readFn = getReadFunction(srcFormat);
583
+ const srcBytesPerSample = getBytesPerSample(srcFormat);
584
+ const srcIsPlanar = formatIsPlanar(srcFormat);
585
+ for (let i = 0; i < copyFrameCount; i++) {
586
+ if (destIsPlanar) {
587
+ const destOffset = i * destBytesPerSample;
588
+ let srcOffset;
589
+ if (srcIsPlanar) {
590
+ srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample;
591
+ }
592
+ else {
593
+ srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample;
594
+ }
595
+ const normalized = readFn(srcView, srcOffset);
596
+ writeFn(destView, destOffset, normalized);
597
+ }
598
+ else {
599
+ for (let ch = 0; ch < numChannels; ch++) {
600
+ const destIndex = i * numChannels + ch;
601
+ const destOffset = destIndex * destBytesPerSample;
602
+ let srcOffset;
603
+ if (srcIsPlanar) {
604
+ srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample;
605
+ }
606
+ else {
607
+ srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample;
608
+ }
609
+ const normalized = readFn(srcView, srcOffset);
610
+ writeFn(destView, destOffset, normalized);
611
+ }
612
+ }
613
+ }
614
+ }
615
+ }
616
+ /** Clones this audio sample. */
617
+ clone() {
618
+ if (this._closed) {
619
+ throw new Error('AudioSample is closed.');
620
+ }
621
+ if (isAudioData(this._data)) {
622
+ const sample = new AudioSample(this._data.clone());
623
+ sample.setTimestamp(this.timestamp); // Make sure the timestamp is precise (beyond microsecond accuracy)
624
+ return sample;
625
+ }
626
+ else {
627
+ return new AudioSample({
628
+ format: this.format,
629
+ sampleRate: this.sampleRate,
630
+ numberOfFrames: this.numberOfFrames,
631
+ numberOfChannels: this.numberOfChannels,
632
+ timestamp: this.timestamp,
633
+ data: this._data,
634
+ });
635
+ }
636
+ }
637
+ /**
638
+ * Closes this audio sample, releasing held resources. Audio samples should be closed as soon as they are not
639
+ * needed anymore.
640
+ */
641
+ close() {
642
+ if (this._closed) {
643
+ return;
644
+ }
645
+ if (isAudioData(this._data)) {
646
+ this._data.close();
647
+ }
648
+ else {
649
+ this._data = new Uint8Array(0);
650
+ }
651
+ this._closed = true;
652
+ }
653
+ /**
654
+ * Converts this audio sample to an AudioData for use with the WebCodecs API. The AudioData returned by this
655
+ * method *must* be closed separately from this audio sample.
656
+ */
657
+ toAudioData() {
658
+ if (this._closed) {
659
+ throw new Error('AudioSample is closed.');
660
+ }
661
+ if (isAudioData(this._data)) {
662
+ if (this._data.timestamp === this.microsecondTimestamp) {
663
+ // Timestamp matches, let's just return the data (but cloned)
664
+ return this._data.clone();
665
+ }
666
+ else {
667
+ // It's impossible to simply change an AudioData's timestamp, so we'll need to create a new one
668
+ if (formatIsPlanar(this.format)) {
669
+ const size = this.allocationSize({ planeIndex: 0, format: this.format });
670
+ const data = new ArrayBuffer(size * this.numberOfChannels);
671
+ // We gotta read out each plane individually
672
+ for (let i = 0; i < this.numberOfChannels; i++) {
673
+ this.copyTo(new Uint8Array(data, i * size, size), { planeIndex: i, format: this.format });
674
+ }
675
+ return new AudioData({
676
+ format: this.format,
677
+ sampleRate: this.sampleRate,
678
+ numberOfFrames: this.numberOfFrames,
679
+ numberOfChannels: this.numberOfChannels,
680
+ timestamp: this.microsecondTimestamp,
681
+ data,
682
+ });
683
+ }
684
+ else {
685
+ const data = new ArrayBuffer(this.allocationSize({ planeIndex: 0, format: this.format }));
686
+ this.copyTo(data, { planeIndex: 0, format: this.format });
687
+ return new AudioData({
688
+ format: this.format,
689
+ sampleRate: this.sampleRate,
690
+ numberOfFrames: this.numberOfFrames,
691
+ numberOfChannels: this.numberOfChannels,
692
+ timestamp: this.microsecondTimestamp,
693
+ data,
694
+ });
695
+ }
696
+ }
697
+ }
698
+ else {
699
+ return new AudioData({
700
+ format: this.format,
701
+ sampleRate: this.sampleRate,
702
+ numberOfFrames: this.numberOfFrames,
703
+ numberOfChannels: this.numberOfChannels,
704
+ timestamp: this.microsecondTimestamp,
705
+ data: this._data,
706
+ });
707
+ }
708
+ }
709
+ /** Convert this audio sample to an AudioBuffer for use with the Web Audio API. */
710
+ toAudioBuffer() {
711
+ if (this._closed) {
712
+ throw new Error('AudioSample is closed.');
713
+ }
714
+ const audioBuffer = new AudioBuffer({
715
+ numberOfChannels: this.numberOfChannels,
716
+ length: this.numberOfFrames,
717
+ sampleRate: this.sampleRate,
718
+ });
719
+ const dataBytes = new Float32Array(this.allocationSize({ planeIndex: 0, format: 'f32-planar' }) / 4);
720
+ for (let i = 0; i < this.numberOfChannels; i++) {
721
+ this.copyTo(dataBytes, { planeIndex: i, format: 'f32-planar' });
722
+ audioBuffer.copyToChannel(dataBytes, i);
723
+ }
724
+ return audioBuffer;
725
+ }
726
+ /** Sets the presentation timestamp of this audio sample, in seconds. */
727
+ setTimestamp(newTimestamp) {
728
+ if (!Number.isFinite(newTimestamp)) {
729
+ throw new TypeError('newTimestamp must be a number.');
730
+ }
731
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
732
+ this.timestamp = newTimestamp;
733
+ }
734
+ }
735
+ const getBytesPerSample = (format) => {
736
+ switch (format) {
737
+ case 'u8':
738
+ case 'u8-planar':
739
+ return 1;
740
+ case 's16':
741
+ case 's16-planar':
742
+ return 2;
743
+ case 's32':
744
+ case 's32-planar':
745
+ return 4;
746
+ case 'f32':
747
+ case 'f32-planar':
748
+ return 4;
749
+ default:
750
+ throw new Error('Unknown AudioSampleFormat');
751
+ }
752
+ };
753
+ const formatIsPlanar = (format) => {
754
+ switch (format) {
755
+ case 'u8-planar':
756
+ case 's16-planar':
757
+ case 's32-planar':
758
+ case 'f32-planar':
759
+ return true;
760
+ default:
761
+ return false;
762
+ }
763
+ };
764
+ const getReadFunction = (format) => {
765
+ switch (format) {
766
+ case 'u8':
767
+ case 'u8-planar':
768
+ return (view, offset) => (view.getUint8(offset) - 128) / 128;
769
+ case 's16':
770
+ case 's16-planar':
771
+ return (view, offset) => view.getInt16(offset, true) / 32768;
772
+ case 's32':
773
+ case 's32-planar':
774
+ return (view, offset) => view.getInt32(offset, true) / 2147483648;
775
+ case 'f32':
776
+ case 'f32-planar':
777
+ return (view, offset) => view.getFloat32(offset, true);
778
+ }
779
+ };
780
+ const getWriteFunction = (format) => {
781
+ switch (format) {
782
+ case 'u8':
783
+ case 'u8-planar':
784
+ return (view, offset, value) => view.setUint8(offset, clamp((value + 1) * 127.5, 0, 255));
785
+ case 's16':
786
+ case 's16-planar':
787
+ return (view, offset, value) => view.setInt16(offset, clamp(Math.round(value * 32767), -32768, 32767), true);
788
+ case 's32':
789
+ case 's32-planar':
790
+ return (view, offset, value) => view.setInt32(offset, clamp(Math.round(value * 2147483647), -2147483648, 2147483647), true);
791
+ case 'f32':
792
+ case 'f32-planar':
793
+ return (view, offset, value) => view.setFloat32(offset, value, true);
794
+ }
795
+ };
796
+ const isAudioData = (x) => {
797
+ return typeof AudioData !== 'undefined' && x instanceof AudioData;
798
+ };