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,1100 @@
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 { customAudioEncoders, customVideoEncoders } from './custom-coder.js';
9
+ import { Bitstream, COLOR_PRIMARIES_MAP, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP, assert, bytesToHexString, isAllowSharedBufferSource, last, reverseBitsU32, toDataView, } from './misc.js';
10
+ /**
11
+ * List of known video codecs, ordered by encoding preference.
12
+ * @public
13
+ */
14
+ export const VIDEO_CODECS = [
15
+ 'avc',
16
+ 'hevc',
17
+ 'vp9',
18
+ 'av1',
19
+ 'vp8',
20
+ ];
21
+ /**
22
+ * List of known PCM (uncompressed) audio codecs, ordered by encoding preference.
23
+ * @public
24
+ */
25
+ export const PCM_AUDIO_CODECS = [
26
+ 'pcm-s16', // We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings
27
+ 'pcm-s16be',
28
+ 'pcm-s24',
29
+ 'pcm-s24be',
30
+ 'pcm-s32',
31
+ 'pcm-s32be',
32
+ 'pcm-f32',
33
+ 'pcm-f32be',
34
+ 'pcm-f64',
35
+ 'pcm-f64be',
36
+ 'pcm-u8',
37
+ 'pcm-s8',
38
+ 'ulaw',
39
+ 'alaw',
40
+ ];
41
+ /**
42
+ * List of known compressed audio codecs, ordered by encoding preference.
43
+ * @public
44
+ */
45
+ export const NON_PCM_AUDIO_CODECS = [
46
+ 'aac',
47
+ 'opus',
48
+ 'mp3',
49
+ 'vorbis',
50
+ 'flac',
51
+ ];
52
+ /**
53
+ * List of known audio codecs, ordered by encoding preference.
54
+ * @public
55
+ */
56
+ export const AUDIO_CODECS = [
57
+ ...NON_PCM_AUDIO_CODECS,
58
+ ...PCM_AUDIO_CODECS,
59
+ ];
60
+ /**
61
+ * List of known subtitle codecs, ordered by encoding preference.
62
+ * @public
63
+ */
64
+ export const SUBTITLE_CODECS = [
65
+ 'webvtt',
66
+ ]; // TODO add the rest
67
+ // https://en.wikipedia.org/wiki/Advanced_Video_Coding
68
+ const AVC_LEVEL_TABLE = [
69
+ { maxMacroblocks: 99, maxBitrate: 64000, level: 0x0A }, // Level 1
70
+ { maxMacroblocks: 396, maxBitrate: 192000, level: 0x0B }, // Level 1.1
71
+ { maxMacroblocks: 396, maxBitrate: 384000, level: 0x0C }, // Level 1.2
72
+ { maxMacroblocks: 396, maxBitrate: 768000, level: 0x0D }, // Level 1.3
73
+ { maxMacroblocks: 396, maxBitrate: 2000000, level: 0x14 }, // Level 2
74
+ { maxMacroblocks: 792, maxBitrate: 4000000, level: 0x15 }, // Level 2.1
75
+ { maxMacroblocks: 1620, maxBitrate: 4000000, level: 0x16 }, // Level 2.2
76
+ { maxMacroblocks: 1620, maxBitrate: 10000000, level: 0x1E }, // Level 3
77
+ { maxMacroblocks: 3600, maxBitrate: 14000000, level: 0x1F }, // Level 3.1
78
+ { maxMacroblocks: 5120, maxBitrate: 20000000, level: 0x20 }, // Level 3.2
79
+ { maxMacroblocks: 8192, maxBitrate: 20000000, level: 0x28 }, // Level 4
80
+ { maxMacroblocks: 8192, maxBitrate: 50000000, level: 0x29 }, // Level 4.1
81
+ { maxMacroblocks: 8704, maxBitrate: 50000000, level: 0x2A }, // Level 4.2
82
+ { maxMacroblocks: 22080, maxBitrate: 135000000, level: 0x32 }, // Level 5
83
+ { maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x33 }, // Level 5.1
84
+ { maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x34 }, // Level 5.2
85
+ { maxMacroblocks: 139264, maxBitrate: 240000000, level: 0x3C }, // Level 6
86
+ { maxMacroblocks: 139264, maxBitrate: 480000000, level: 0x3D }, // Level 6.1
87
+ { maxMacroblocks: 139264, maxBitrate: 800000000, level: 0x3E }, // Level 6.2
88
+ ];
89
+ // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding
90
+ const HEVC_LEVEL_TABLE = [
91
+ { maxPictureSize: 36864, maxBitrate: 128000, tier: 'L', level: 30 }, // Level 1 (Low Tier)
92
+ { maxPictureSize: 122880, maxBitrate: 1500000, tier: 'L', level: 60 }, // Level 2 (Low Tier)
93
+ { maxPictureSize: 245760, maxBitrate: 3000000, tier: 'L', level: 63 }, // Level 2.1 (Low Tier)
94
+ { maxPictureSize: 552960, maxBitrate: 6000000, tier: 'L', level: 90 }, // Level 3 (Low Tier)
95
+ { maxPictureSize: 983040, maxBitrate: 10000000, tier: 'L', level: 93 }, // Level 3.1 (Low Tier)
96
+ { maxPictureSize: 2228224, maxBitrate: 12000000, tier: 'L', level: 120 }, // Level 4 (Low Tier)
97
+ { maxPictureSize: 2228224, maxBitrate: 30000000, tier: 'H', level: 120 }, // Level 4 (High Tier)
98
+ { maxPictureSize: 2228224, maxBitrate: 20000000, tier: 'L', level: 123 }, // Level 4.1 (Low Tier)
99
+ { maxPictureSize: 2228224, maxBitrate: 50000000, tier: 'H', level: 123 }, // Level 4.1 (High Tier)
100
+ { maxPictureSize: 8912896, maxBitrate: 25000000, tier: 'L', level: 150 }, // Level 5 (Low Tier)
101
+ { maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 150 }, // Level 5 (High Tier)
102
+ { maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'L', level: 153 }, // Level 5.1 (Low Tier)
103
+ { maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 153 }, // Level 5.1 (High Tier)
104
+ { maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'L', level: 156 }, // Level 5.2 (Low Tier)
105
+ { maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 156 }, // Level 5.2 (High Tier)
106
+ { maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'L', level: 180 }, // Level 6 (Low Tier)
107
+ { maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 180 }, // Level 6 (High Tier)
108
+ { maxPictureSize: 35651584, maxBitrate: 120000000, tier: 'L', level: 183 }, // Level 6.1 (Low Tier)
109
+ { maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 183 }, // Level 6.1 (High Tier)
110
+ { maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'L', level: 186 }, // Level 6.2 (Low Tier)
111
+ { maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 186 }, // Level 6.2 (High Tier)
112
+ ];
113
+ // https://en.wikipedia.org/wiki/VP9
114
+ export const VP9_LEVEL_TABLE = [
115
+ { maxPictureSize: 36864, maxBitrate: 200000, level: 10 }, // Level 1
116
+ { maxPictureSize: 73728, maxBitrate: 800000, level: 11 }, // Level 1.1
117
+ { maxPictureSize: 122880, maxBitrate: 1800000, level: 20 }, // Level 2
118
+ { maxPictureSize: 245760, maxBitrate: 3600000, level: 21 }, // Level 2.1
119
+ { maxPictureSize: 552960, maxBitrate: 7200000, level: 30 }, // Level 3
120
+ { maxPictureSize: 983040, maxBitrate: 12000000, level: 31 }, // Level 3.1
121
+ { maxPictureSize: 2228224, maxBitrate: 18000000, level: 40 }, // Level 4
122
+ { maxPictureSize: 2228224, maxBitrate: 30000000, level: 41 }, // Level 4.1
123
+ { maxPictureSize: 8912896, maxBitrate: 60000000, level: 50 }, // Level 5
124
+ { maxPictureSize: 8912896, maxBitrate: 120000000, level: 51 }, // Level 5.1
125
+ { maxPictureSize: 8912896, maxBitrate: 180000000, level: 52 }, // Level 5.2
126
+ { maxPictureSize: 35651584, maxBitrate: 180000000, level: 60 }, // Level 6
127
+ { maxPictureSize: 35651584, maxBitrate: 240000000, level: 61 }, // Level 6.1
128
+ { maxPictureSize: 35651584, maxBitrate: 480000000, level: 62 }, // Level 6.2
129
+ ];
130
+ // https://en.wikipedia.org/wiki/AV1
131
+ const AV1_LEVEL_TABLE = [
132
+ { maxPictureSize: 147456, maxBitrate: 1500000, tier: 'M', level: 0 }, // Level 2.0 (Main Tier)
133
+ { maxPictureSize: 278784, maxBitrate: 3000000, tier: 'M', level: 1 }, // Level 2.1 (Main Tier)
134
+ { maxPictureSize: 665856, maxBitrate: 6000000, tier: 'M', level: 4 }, // Level 3.0 (Main Tier)
135
+ { maxPictureSize: 1065024, maxBitrate: 10000000, tier: 'M', level: 5 }, // Level 3.1 (Main Tier)
136
+ { maxPictureSize: 2359296, maxBitrate: 12000000, tier: 'M', level: 8 }, // Level 4.0 (Main Tier)
137
+ { maxPictureSize: 2359296, maxBitrate: 30000000, tier: 'H', level: 8 }, // Level 4.0 (High Tier)
138
+ { maxPictureSize: 2359296, maxBitrate: 20000000, tier: 'M', level: 9 }, // Level 4.1 (Main Tier)
139
+ { maxPictureSize: 2359296, maxBitrate: 50000000, tier: 'H', level: 9 }, // Level 4.1 (High Tier)
140
+ { maxPictureSize: 8912896, maxBitrate: 30000000, tier: 'M', level: 12 }, // Level 5.0 (Main Tier)
141
+ { maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 12 }, // Level 5.0 (High Tier)
142
+ { maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'M', level: 13 }, // Level 5.1 (Main Tier)
143
+ { maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 13 }, // Level 5.1 (High Tier)
144
+ { maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'M', level: 14 }, // Level 5.2 (Main Tier)
145
+ { maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 14 }, // Level 5.2 (High Tier)
146
+ { maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 15 }, // Level 5.3 (Main Tier)
147
+ { maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 15 }, // Level 5.3 (High Tier)
148
+ { maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 16 }, // Level 6.0 (Main Tier)
149
+ { maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 16 }, // Level 6.0 (High Tier)
150
+ { maxPictureSize: 35651584, maxBitrate: 100000000, tier: 'M', level: 17 }, // Level 6.1 (Main Tier)
151
+ { maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 17 }, // Level 6.1 (High Tier)
152
+ { maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 18 }, // Level 6.2 (Main Tier)
153
+ { maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 18 }, // Level 6.2 (High Tier)
154
+ { maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 19 }, // Level 6.3 (Main Tier)
155
+ { maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 19 }, // Level 6.3 (High Tier)
156
+ ];
157
+ const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00';
158
+ const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0';
159
+ export const buildVideoCodecString = (codec, width, height, bitrate) => {
160
+ if (codec === 'avc') {
161
+ const profileIndication = 0x64; // High Profile
162
+ const totalMacroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
163
+ // Determine the level based on the table
164
+ const levelInfo = AVC_LEVEL_TABLE.find(level => totalMacroblocks <= level.maxMacroblocks && bitrate <= level.maxBitrate) ?? last(AVC_LEVEL_TABLE);
165
+ const levelIndication = levelInfo ? levelInfo.level : 0;
166
+ const hexProfileIndication = profileIndication.toString(16).padStart(2, '0');
167
+ const hexProfileCompatibility = '00';
168
+ const hexLevelIndication = levelIndication.toString(16).padStart(2, '0');
169
+ return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
170
+ }
171
+ else if (codec === 'hevc') {
172
+ const profilePrefix = ''; // Profile space 0
173
+ const profileIdc = 1; // Main Profile
174
+ const compatibilityFlags = '6'; // Taken from the example in ISO 14496-15
175
+ const pictureSize = width * height;
176
+ const levelInfo = HEVC_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(HEVC_LEVEL_TABLE);
177
+ const constraintFlags = 'B0'; // Progressive source flag
178
+ return 'hev1.'
179
+ + `${profilePrefix}${profileIdc}.`
180
+ + `${compatibilityFlags}.`
181
+ + `${levelInfo.tier}${levelInfo.level}.`
182
+ + `${constraintFlags}`;
183
+ }
184
+ else if (codec === 'vp8') {
185
+ return 'vp8'; // Easy, this one
186
+ }
187
+ else if (codec === 'vp9') {
188
+ const profile = '00'; // Profile 0
189
+ const pictureSize = width * height;
190
+ const levelInfo = VP9_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(VP9_LEVEL_TABLE);
191
+ const bitDepth = '08'; // 8-bit
192
+ return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}`;
193
+ }
194
+ else if (codec === 'av1') {
195
+ const profile = 0; // Main Profile, single digit
196
+ const pictureSize = width * height;
197
+ const levelInfo = AV1_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(AV1_LEVEL_TABLE);
198
+ const level = levelInfo.level.toString().padStart(2, '0');
199
+ const bitDepth = '08'; // 8-bit
200
+ return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
201
+ }
202
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
203
+ throw new TypeError(`Unhandled codec '${codec}'.`);
204
+ };
205
+ export const generateVp9CodecConfigurationFromCodecString = (codecString) => {
206
+ // Reference: https://www.webmproject.org/docs/container/#vp9-codec-feature-metadata-codecprivate
207
+ const parts = codecString.split('.'); // We can derive the required values from the codec string
208
+ const profile = Number(parts[1]);
209
+ const level = Number(parts[2]);
210
+ const bitDepth = Number(parts[3]);
211
+ const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;
212
+ return [
213
+ 1, 1, profile,
214
+ 2, 1, level,
215
+ 3, 1, bitDepth,
216
+ 4, 1, chromaSubsampling,
217
+ ];
218
+ };
219
+ export const generateAv1CodecConfigurationFromCodecString = (codecString) => {
220
+ // Reference: https://aomediacodec.github.io/av1-isobmff/
221
+ const parts = codecString.split('.'); // We can derive the required values from the codec string
222
+ const marker = 1;
223
+ const version = 1;
224
+ const firstByte = (marker << 7) + version;
225
+ const profile = Number(parts[1]);
226
+ const levelAndTier = parts[2];
227
+ const level = Number(levelAndTier.slice(0, -1));
228
+ const secondByte = (profile << 5) + level;
229
+ const tier = levelAndTier.slice(-1) === 'H' ? 1 : 0;
230
+ const bitDepth = Number(parts[3]);
231
+ const highBitDepth = bitDepth === 8 ? 0 : 1;
232
+ const twelveBit = 0;
233
+ const monochrome = parts[4] ? Number(parts[4]) : 0;
234
+ const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1;
235
+ const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1;
236
+ const chromaSamplePosition = parts[5] ? Number(parts[5][2]) : 0; // CSP_UNKNOWN
237
+ const thirdByte = (tier << 7)
238
+ + (highBitDepth << 6)
239
+ + (twelveBit << 5)
240
+ + (monochrome << 4)
241
+ + (chromaSubsamplingX << 3)
242
+ + (chromaSubsamplingY << 2)
243
+ + chromaSamplePosition;
244
+ const initialPresentationDelayPresent = 0; // Should be fine
245
+ const fourthByte = initialPresentationDelayPresent;
246
+ return [firstByte, secondByte, thirdByte, fourthByte];
247
+ };
248
+ export const extractVideoCodecString = (trackInfo) => {
249
+ const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
250
+ if (codec === 'avc') {
251
+ if (avcCodecInfo) {
252
+ const bytes = new Uint8Array([
253
+ avcCodecInfo.avcProfileIndication,
254
+ avcCodecInfo.profileCompatibility,
255
+ avcCodecInfo.avcLevelIndication,
256
+ ]);
257
+ return `avc1.${bytesToHexString(bytes)}`;
258
+ }
259
+ if (!codecDescription || codecDescription.byteLength < 4) {
260
+ throw new TypeError('AVC decoder description is not provided or is not at least 4 bytes long.');
261
+ }
262
+ return `avc1.${bytesToHexString(codecDescription.subarray(1, 4))}`;
263
+ }
264
+ else if (codec === 'hevc') {
265
+ let generalProfileSpace;
266
+ let generalProfileIdc;
267
+ let compatibilityFlags;
268
+ let generalTierFlag;
269
+ let generalLevelIdc;
270
+ let constraintFlags;
271
+ if (hevcCodecInfo) {
272
+ generalProfileSpace = hevcCodecInfo.generalProfileSpace;
273
+ generalProfileIdc = hevcCodecInfo.generalProfileIdc;
274
+ compatibilityFlags = reverseBitsU32(hevcCodecInfo.generalProfileCompatibilityFlags);
275
+ generalTierFlag = hevcCodecInfo.generalTierFlag;
276
+ generalLevelIdc = hevcCodecInfo.generalLevelIdc;
277
+ constraintFlags = [...hevcCodecInfo.generalConstraintIndicatorFlags];
278
+ }
279
+ else {
280
+ if (!codecDescription || codecDescription.byteLength < 23) {
281
+ throw new TypeError('HEVC decoder description is not provided or is not at least 23 bytes long.');
282
+ }
283
+ const view = toDataView(codecDescription);
284
+ const profileByte = view.getUint8(1);
285
+ generalProfileSpace = (profileByte >> 6) & 0x03;
286
+ generalProfileIdc = profileByte & 0x1F;
287
+ compatibilityFlags = reverseBitsU32(view.getUint32(2));
288
+ generalTierFlag = (profileByte >> 5) & 0x01;
289
+ generalLevelIdc = view.getUint8(12);
290
+ constraintFlags = [];
291
+ for (let i = 0; i < 6; i++) {
292
+ constraintFlags.push(view.getUint8(6 + i));
293
+ }
294
+ }
295
+ let codecString = 'hev1.';
296
+ codecString += ['', 'A', 'B', 'C'][generalProfileSpace] + generalProfileIdc;
297
+ codecString += '.';
298
+ codecString += compatibilityFlags.toString(16).toUpperCase();
299
+ codecString += '.';
300
+ codecString += generalTierFlag === 0 ? 'L' : 'H';
301
+ codecString += generalLevelIdc;
302
+ while (constraintFlags.length > 0 && constraintFlags[constraintFlags.length - 1] === 0) {
303
+ constraintFlags.pop();
304
+ }
305
+ if (constraintFlags.length > 0) {
306
+ codecString += '.';
307
+ codecString += constraintFlags.map(x => x.toString(16).toUpperCase()).join('.');
308
+ }
309
+ return codecString;
310
+ }
311
+ else if (codec === 'vp8') {
312
+ return 'vp8'; // Easy, this one
313
+ }
314
+ else if (codec === 'vp9') {
315
+ if (!vp9CodecInfo) {
316
+ // Calculate level based on dimensions
317
+ const pictureSize = trackInfo.width * trackInfo.height;
318
+ let level = last(VP9_LEVEL_TABLE).level; // Default to highest level
319
+ for (const entry of VP9_LEVEL_TABLE) {
320
+ if (pictureSize <= entry.maxPictureSize) {
321
+ level = entry.level;
322
+ break;
323
+ }
324
+ }
325
+ // We don't really know better, so let's return a general-purpose, common codec string and hope for the best
326
+ return `vp09.00.${level.toString().padStart(2, '0')}.08`;
327
+ }
328
+ const profile = vp9CodecInfo.profile.toString().padStart(2, '0');
329
+ const level = vp9CodecInfo.level.toString().padStart(2, '0');
330
+ const bitDepth = vp9CodecInfo.bitDepth.toString().padStart(2, '0');
331
+ const chromaSubsampling = vp9CodecInfo.chromaSubsampling.toString().padStart(2, '0');
332
+ const colourPrimaries = vp9CodecInfo.colourPrimaries.toString().padStart(2, '0');
333
+ const transferCharacteristics = vp9CodecInfo.transferCharacteristics.toString().padStart(2, '0');
334
+ const matrixCoefficients = vp9CodecInfo.matrixCoefficients.toString().padStart(2, '0');
335
+ const videoFullRangeFlag = vp9CodecInfo.videoFullRangeFlag.toString().padStart(2, '0');
336
+ let string = `vp09.${profile}.${level}.${bitDepth}.${chromaSubsampling}`;
337
+ string += `.${colourPrimaries}.${transferCharacteristics}.${matrixCoefficients}.${videoFullRangeFlag}`;
338
+ if (string.endsWith(VP9_DEFAULT_SUFFIX)) {
339
+ string = string.slice(0, -VP9_DEFAULT_SUFFIX.length);
340
+ }
341
+ return string;
342
+ }
343
+ else if (codec === 'av1') {
344
+ if (!av1CodecInfo) {
345
+ // Calculate level based on dimensions
346
+ const pictureSize = trackInfo.width * trackInfo.height;
347
+ let level = last(VP9_LEVEL_TABLE).level; // Default to highest level
348
+ for (const entry of VP9_LEVEL_TABLE) {
349
+ if (pictureSize <= entry.maxPictureSize) {
350
+ level = entry.level;
351
+ break;
352
+ }
353
+ }
354
+ // We don't really know better, so let's return a general-purpose, common codec string and hope for the best
355
+ return `av01.0.${level.toString().padStart(2, '0')}M.08`;
356
+ }
357
+ // https://aomediacodec.github.io/av1-isobmff/#codecsparam
358
+ const profile = av1CodecInfo.profile; // Single digit
359
+ const level = av1CodecInfo.level.toString().padStart(2, '0');
360
+ const tier = av1CodecInfo.tier ? 'H' : 'M';
361
+ const bitDepth = av1CodecInfo.bitDepth.toString().padStart(2, '0');
362
+ const monochrome = av1CodecInfo.monochrome ? '1' : '0';
363
+ const chromaSubsampling = 100 * av1CodecInfo.chromaSubsamplingX
364
+ + 10 * av1CodecInfo.chromaSubsamplingY
365
+ + 1 * (av1CodecInfo.chromaSubsamplingX && av1CodecInfo.chromaSubsamplingY
366
+ ? av1CodecInfo.chromaSamplePosition
367
+ : 0);
368
+ // The defaults are 1 (ITU-R BT.709)
369
+ const colorPrimaries = colorSpace?.primaries ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 1;
370
+ const transferCharacteristics = colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 1;
371
+ const matrixCoefficients = colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 1;
372
+ const videoFullRangeFlag = colorSpace?.fullRange ? 1 : 0;
373
+ let string = `av01.${profile}.${level}${tier}.${bitDepth}`;
374
+ string += `.${monochrome}.${chromaSubsampling.toString().padStart(3, '0')}`;
375
+ string += `.${colorPrimaries.toString().padStart(2, '0')}`;
376
+ string += `.${transferCharacteristics.toString().padStart(2, '0')}`;
377
+ string += `.${matrixCoefficients.toString().padStart(2, '0')}`;
378
+ string += `.${videoFullRangeFlag}`;
379
+ if (string.endsWith(AV1_DEFAULT_SUFFIX)) {
380
+ string = string.slice(0, -AV1_DEFAULT_SUFFIX.length);
381
+ }
382
+ return string;
383
+ }
384
+ throw new TypeError(`Unhandled codec '${codec}'.`);
385
+ };
386
+ export const buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
387
+ if (codec === 'aac') {
388
+ // If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS
389
+ if (numberOfChannels >= 2 && sampleRate <= 24000) {
390
+ return 'mp4a.40.29'; // HE-AAC v2 (AAC LC + SBR + PS)
391
+ }
392
+ // If sample rate is low, likely using HE-AAC v1 with SBR
393
+ if (sampleRate <= 24000) {
394
+ return 'mp4a.40.5'; // HE-AAC v1 (AAC LC + SBR)
395
+ }
396
+ // Default to standard AAC-LC for higher sample rates
397
+ return 'mp4a.40.2'; // AAC-LC
398
+ }
399
+ else if (codec === 'mp3') {
400
+ return 'mp3';
401
+ }
402
+ else if (codec === 'opus') {
403
+ return 'opus';
404
+ }
405
+ else if (codec === 'vorbis') {
406
+ return 'vorbis';
407
+ }
408
+ else if (codec === 'flac') {
409
+ return 'flac';
410
+ }
411
+ else if (PCM_AUDIO_CODECS.includes(codec)) {
412
+ return codec;
413
+ }
414
+ throw new TypeError(`Unhandled codec '${codec}'.`);
415
+ };
416
+ export const extractAudioCodecString = (trackInfo) => {
417
+ const { codec, codecDescription, aacCodecInfo } = trackInfo;
418
+ if (codec === 'aac') {
419
+ if (!aacCodecInfo) {
420
+ throw new TypeError('AAC codec info must be provided.');
421
+ }
422
+ if (aacCodecInfo.isMpeg2) {
423
+ return 'mp4a.67';
424
+ }
425
+ else {
426
+ const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
427
+ return `mp4a.40.${audioSpecificConfig.objectType}`;
428
+ }
429
+ }
430
+ else if (codec === 'mp3') {
431
+ return 'mp3';
432
+ }
433
+ else if (codec === 'opus') {
434
+ return 'opus';
435
+ }
436
+ else if (codec === 'vorbis') {
437
+ return 'vorbis';
438
+ }
439
+ else if (codec === 'flac') {
440
+ return 'flac';
441
+ }
442
+ else if (codec && PCM_AUDIO_CODECS.includes(codec)) {
443
+ return codec;
444
+ }
445
+ throw new TypeError(`Unhandled codec '${codec}'.`);
446
+ };
447
+ export const parseAacAudioSpecificConfig = (bytes) => {
448
+ if (!bytes || bytes.byteLength < 2) {
449
+ throw new TypeError('AAC description must be at least 2 bytes long.');
450
+ }
451
+ const bitstream = new Bitstream(bytes);
452
+ let objectType = bitstream.readBits(5);
453
+ if (objectType === 31) {
454
+ objectType = 32 + bitstream.readBits(6);
455
+ }
456
+ const frequencyIndex = bitstream.readBits(4);
457
+ let sampleRate = null;
458
+ if (frequencyIndex === 15) {
459
+ sampleRate = bitstream.readBits(24);
460
+ }
461
+ else {
462
+ const freqTable = [
463
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
464
+ 16000, 12000, 11025, 8000, 7350,
465
+ ];
466
+ if (frequencyIndex < freqTable.length) {
467
+ sampleRate = freqTable[frequencyIndex];
468
+ }
469
+ }
470
+ const channelConfiguration = bitstream.readBits(4);
471
+ let numberOfChannels = null;
472
+ if (channelConfiguration >= 1 && channelConfiguration <= 7) {
473
+ const channelMap = {
474
+ 1: 1,
475
+ 2: 2,
476
+ 3: 3,
477
+ 4: 4,
478
+ 5: 5,
479
+ 6: 6,
480
+ 7: 8,
481
+ };
482
+ numberOfChannels = channelMap[channelConfiguration];
483
+ }
484
+ return {
485
+ objectType,
486
+ frequencyIndex,
487
+ sampleRate,
488
+ channelConfiguration,
489
+ numberOfChannels,
490
+ };
491
+ };
492
+ export const OPUS_INTERNAL_SAMPLE_RATE = 48000;
493
+ const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
494
+ export const parsePcmCodec = (codec) => {
495
+ assert(PCM_AUDIO_CODECS.includes(codec));
496
+ if (codec === 'ulaw') {
497
+ return { dataType: 'ulaw', sampleSize: 1, littleEndian: true, silentValue: 255 };
498
+ }
499
+ else if (codec === 'alaw') {
500
+ return { dataType: 'alaw', sampleSize: 1, littleEndian: true, silentValue: 213 };
501
+ }
502
+ const match = PCM_CODEC_REGEX.exec(codec);
503
+ assert(match);
504
+ let dataType;
505
+ if (match[1] === 'u') {
506
+ dataType = 'unsigned';
507
+ }
508
+ else if (match[1] === 's') {
509
+ dataType = 'signed';
510
+ }
511
+ else {
512
+ dataType = 'float';
513
+ }
514
+ const sampleSize = (Number(match[2]) / 8);
515
+ const littleEndian = match[3] !== 'be';
516
+ const silentValue = codec === 'pcm-u8' ? 2 ** 7 : 0;
517
+ return { dataType, sampleSize, littleEndian, silentValue };
518
+ };
519
+ export const inferCodecFromCodecString = (codecString) => {
520
+ // Video codecs
521
+ if (codecString.startsWith('avc1') || codecString.startsWith('avc3')) {
522
+ return 'avc';
523
+ }
524
+ else if (codecString.startsWith('hev1') || codecString.startsWith('hvc1')) {
525
+ return 'hevc';
526
+ }
527
+ else if (codecString === 'vp8') {
528
+ return 'vp8';
529
+ }
530
+ else if (codecString.startsWith('vp09')) {
531
+ return 'vp9';
532
+ }
533
+ else if (codecString.startsWith('av01')) {
534
+ return 'av1';
535
+ }
536
+ // Audio codecs
537
+ if (codecString.startsWith('mp4a.40') || codecString === 'mp4a.67') {
538
+ return 'aac';
539
+ }
540
+ else if (codecString === 'mp3'
541
+ || codecString === 'mp4a.69'
542
+ || codecString === 'mp4a.6B'
543
+ || codecString === 'mp4a.6b') {
544
+ return 'mp3';
545
+ }
546
+ else if (codecString === 'opus') {
547
+ return 'opus';
548
+ }
549
+ else if (codecString === 'vorbis') {
550
+ return 'vorbis';
551
+ }
552
+ else if (codecString === 'flac') {
553
+ return 'flac';
554
+ }
555
+ else if (codecString === 'ulaw') {
556
+ return 'ulaw';
557
+ }
558
+ else if (codecString === 'alaw') {
559
+ return 'alaw';
560
+ }
561
+ else if (PCM_CODEC_REGEX.test(codecString)) {
562
+ return codecString;
563
+ }
564
+ // Subtitle codecs
565
+ if (codecString === 'webvtt') {
566
+ return 'webvtt';
567
+ }
568
+ return null;
569
+ };
570
+ export const getVideoEncoderConfigExtension = (codec) => {
571
+ if (codec === 'avc') {
572
+ return {
573
+ avc: {
574
+ format: 'avc', // Ensure the format is not Annex B
575
+ },
576
+ };
577
+ }
578
+ else if (codec === 'hevc') {
579
+ return {
580
+ hevc: {
581
+ format: 'hevc', // Ensure the format is not Annex B
582
+ },
583
+ };
584
+ }
585
+ return {};
586
+ };
587
+ export const getAudioEncoderConfigExtension = (codec) => {
588
+ if (codec === 'aac') {
589
+ return {
590
+ aac: {
591
+ format: 'aac', // Ensure the format is not ADTS
592
+ },
593
+ };
594
+ }
595
+ else if (codec === 'opus') {
596
+ return {
597
+ opus: {
598
+ format: 'opus',
599
+ },
600
+ };
601
+ }
602
+ return {};
603
+ };
604
+ /**
605
+ * Represents a subjective media quality level.
606
+ * @public
607
+ */
608
+ export class Quality {
609
+ /** @internal */
610
+ constructor(factor) {
611
+ this._factor = factor;
612
+ }
613
+ /** @internal */
614
+ _toVideoBitrate(codec, width, height) {
615
+ const pixels = width * height;
616
+ const codecEfficiencyFactors = {
617
+ avc: 1.0, // H.264/AVC (baseline)
618
+ hevc: 0.6, // H.265/HEVC (~40% more efficient than AVC)
619
+ vp9: 0.6, // Similar to HEVC
620
+ av1: 0.4, // ~60% more efficient than AVC
621
+ vp8: 1.2, // Slightly less efficient than AVC
622
+ };
623
+ const referencePixels = 1920 * 1080;
624
+ const referenceBitrate = 3000000;
625
+ const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling
626
+ const baseBitrate = referenceBitrate * scaleFactor;
627
+ const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
628
+ const finalBitrate = codecAdjustedBitrate * this._factor;
629
+ return Math.ceil(finalBitrate / 1000) * 1000;
630
+ }
631
+ /** @internal */
632
+ _toAudioBitrate(codec) {
633
+ if (PCM_AUDIO_CODECS.includes(codec) || codec === 'flac') {
634
+ return undefined;
635
+ }
636
+ const baseRates = {
637
+ aac: 128000, // 128kbps base for AAC
638
+ opus: 64000, // 64kbps base for Opus
639
+ mp3: 160000, // 160kbps base for MP3
640
+ vorbis: 64000, // 64kbps base for Vorbis
641
+ };
642
+ const baseBitrate = baseRates[codec];
643
+ if (!baseBitrate) {
644
+ throw new Error(`Unhandled codec: ${codec}`);
645
+ }
646
+ let finalBitrate = baseBitrate * this._factor;
647
+ if (codec === 'aac') {
648
+ // AAC only works with specific bitrates, let's find the closest
649
+ const validRates = [96000, 128000, 160000, 192000];
650
+ finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
651
+ }
652
+ else if (codec === 'opus' || codec === 'vorbis') {
653
+ finalBitrate = Math.max(6000, finalBitrate);
654
+ }
655
+ else if (codec === 'mp3') {
656
+ const validRates = [
657
+ 8000, 16000, 24000, 32000, 40000, 48000, 64000, 80000,
658
+ 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000,
659
+ ];
660
+ finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
661
+ }
662
+ return Math.round(finalBitrate / 1000) * 1000;
663
+ }
664
+ }
665
+ /**
666
+ * Represents a very low media quality.
667
+ * @public
668
+ */
669
+ export const QUALITY_VERY_LOW = new Quality(0.3);
670
+ /**
671
+ * Represents a low media quality.
672
+ * @public
673
+ */
674
+ export const QUALITY_LOW = new Quality(0.6);
675
+ /**
676
+ * Represents a medium media quality.
677
+ * @public
678
+ */
679
+ export const QUALITY_MEDIUM = new Quality(1);
680
+ /**
681
+ * Represents a high media quality.
682
+ * @public
683
+ */
684
+ export const QUALITY_HIGH = new Quality(2);
685
+ /**
686
+ * Represents a very high media quality.
687
+ * @public
688
+ */
689
+ export const QUALITY_VERY_HIGH = new Quality(4);
690
+ const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01'];
691
+ const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/;
692
+ const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
693
+ const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
694
+ const AV1_CODEC_STRING_REGEX = /^av01\.\d\.\d{2}[MH]\.\d{2}(?:\.\d\.\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d)?$/;
695
+ export const validateVideoChunkMetadata = (metadata) => {
696
+ if (!metadata) {
697
+ throw new TypeError('Video chunk metadata must be provided.');
698
+ }
699
+ if (typeof metadata !== 'object') {
700
+ throw new TypeError('Video chunk metadata must be an object.');
701
+ }
702
+ if (!metadata.decoderConfig) {
703
+ throw new TypeError('Video chunk metadata must include a decoder configuration.');
704
+ }
705
+ if (typeof metadata.decoderConfig !== 'object') {
706
+ throw new TypeError('Video chunk metadata decoder configuration must be an object.');
707
+ }
708
+ if (typeof metadata.decoderConfig.codec !== 'string') {
709
+ throw new TypeError('Video chunk metadata decoder configuration must specify a codec string.');
710
+ }
711
+ if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig.codec.startsWith(prefix))) {
712
+ throw new TypeError('Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
713
+ + ' the WebCodecs Codec Registry.');
714
+ }
715
+ if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth <= 0) {
716
+ throw new TypeError('Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).');
717
+ }
718
+ if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight <= 0) {
719
+ throw new TypeError('Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).');
720
+ }
721
+ if (metadata.decoderConfig.description !== undefined) {
722
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
723
+ throw new TypeError('Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
724
+ + ' ArrayBuffer view.');
725
+ }
726
+ }
727
+ if (metadata.decoderConfig.colorSpace !== undefined) {
728
+ const { colorSpace } = metadata.decoderConfig;
729
+ if (typeof colorSpace !== 'object') {
730
+ throw new TypeError('Video chunk metadata decoder configuration colorSpace, when provided, must be an object.');
731
+ }
732
+ const primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
733
+ if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
734
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of`
735
+ + ` ${primariesValues.join(', ')}.`);
736
+ }
737
+ const transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
738
+ if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
739
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of`
740
+ + ` ${transferValues.join(', ')}.`);
741
+ }
742
+ const matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
743
+ if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
744
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of`
745
+ + ` ${matrixValues.join(', ')}.`);
746
+ }
747
+ if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== 'boolean') {
748
+ throw new TypeError('Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.');
749
+ }
750
+ }
751
+ if (metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) {
752
+ // AVC-specific validation
753
+ if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
754
+ throw new TypeError('Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as'
755
+ + ' specified in Section 3.4 of RFC 6381.');
756
+ }
757
+ // `description` may or may not be set, depending on if the format is AVCC or Annex B, so don't perform any
758
+ // validation for it.
759
+ // https://www.w3.org/TR/webcodecs-avc-codec-registration
760
+ }
761
+ else if (metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) {
762
+ // HEVC-specific validation
763
+ if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
764
+ throw new TypeError('Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as'
765
+ + ' specified in Section E.3 of ISO 14496-15.');
766
+ }
767
+ // `description` may or may not be set, depending on if the format is HEVC or Annex B, so don't perform any
768
+ // validation for it.
769
+ // https://www.w3.org/TR/webcodecs-hevc-codec-registration
770
+ }
771
+ else if (metadata.decoderConfig.codec.startsWith('vp8')) {
772
+ // VP8-specific validation
773
+ if (metadata.decoderConfig.codec !== 'vp8') {
774
+ throw new TypeError('Video chunk metadata decoder configuration codec string for VP8 must be "vp8".');
775
+ }
776
+ }
777
+ else if (metadata.decoderConfig.codec.startsWith('vp09')) {
778
+ // VP9-specific validation
779
+ if (!VP9_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
780
+ throw new TypeError('Video chunk metadata decoder configuration codec string for VP9 must be a valid VP9 codec string as'
781
+ + ' specified in Section "Codecs Parameter String" of https://www.webmproject.org/vp9/mp4/.');
782
+ }
783
+ }
784
+ else if (metadata.decoderConfig.codec.startsWith('av01')) {
785
+ // AV1-specific validation
786
+ if (!AV1_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
787
+ throw new TypeError('Video chunk metadata decoder configuration codec string for AV1 must be a valid AV1 codec string as'
788
+ + ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.');
789
+ }
790
+ }
791
+ };
792
+ const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm'];
793
+ export const validateAudioChunkMetadata = (metadata) => {
794
+ if (!metadata) {
795
+ throw new TypeError('Audio chunk metadata must be provided.');
796
+ }
797
+ if (typeof metadata !== 'object') {
798
+ throw new TypeError('Audio chunk metadata must be an object.');
799
+ }
800
+ if (!metadata.decoderConfig) {
801
+ throw new TypeError('Audio chunk metadata must include a decoder configuration.');
802
+ }
803
+ if (typeof metadata.decoderConfig !== 'object') {
804
+ throw new TypeError('Audio chunk metadata decoder configuration must be an object.');
805
+ }
806
+ if (typeof metadata.decoderConfig.codec !== 'string') {
807
+ throw new TypeError('Audio chunk metadata decoder configuration must specify a codec string.');
808
+ }
809
+ if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig.codec.startsWith(prefix))) {
810
+ throw new TypeError('Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in'
811
+ + ' the WebCodecs Codec Registry.');
812
+ }
813
+ if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
814
+ throw new TypeError('Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).');
815
+ }
816
+ if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
817
+ throw new TypeError('Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).');
818
+ }
819
+ if (metadata.decoderConfig.description !== undefined) {
820
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
821
+ throw new TypeError('Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
822
+ + ' ArrayBuffer view.');
823
+ }
824
+ }
825
+ if (metadata.decoderConfig.codec.startsWith('mp4a')
826
+ // These three refer to MP3:
827
+ && metadata.decoderConfig.codec !== 'mp4a.69'
828
+ && metadata.decoderConfig.codec !== 'mp4a.6B'
829
+ && metadata.decoderConfig.codec !== 'mp4a.6b') {
830
+ // AAC-specific validation
831
+ const validStrings = ['mp4a.40.2', 'mp4a.40.02', 'mp4a.40.5', 'mp4a.40.05', 'mp4a.40.29', 'mp4a.67'];
832
+ if (!validStrings.includes(metadata.decoderConfig.codec)) {
833
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as'
834
+ + ' specified in https://www.w3.org/TR/webcodecs-aac-codec-registration/.');
835
+ }
836
+ if (!metadata.decoderConfig.description) {
837
+ throw new TypeError('Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be'
838
+ + ' an AudioSpecificConfig as specified in ISO 14496-3.');
839
+ }
840
+ }
841
+ else if (metadata.decoderConfig.codec.startsWith('mp3') || metadata.decoderConfig.codec.startsWith('mp4a')) {
842
+ // MP3-specific validation
843
+ if (metadata.decoderConfig.codec !== 'mp3'
844
+ && metadata.decoderConfig.codec !== 'mp4a.69'
845
+ && metadata.decoderConfig.codec !== 'mp4a.6B'
846
+ && metadata.decoderConfig.codec !== 'mp4a.6b') {
847
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for MP3 must be "mp3", "mp4a.69" or'
848
+ + ' "mp4a.6B".');
849
+ }
850
+ }
851
+ else if (metadata.decoderConfig.codec.startsWith('opus')) {
852
+ // Opus-specific validation
853
+ if (metadata.decoderConfig.codec !== 'opus') {
854
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for Opus must be "opus".');
855
+ }
856
+ if (metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
857
+ // Description is optional for Opus per-spec, so we shouldn't enforce it
858
+ throw new TypeError('Audio chunk metadata decoder configuration description, when specified, is expected to be an'
859
+ + ' Identification Header as specified in Section 5.1 of RFC 7845.');
860
+ }
861
+ }
862
+ else if (metadata.decoderConfig.codec.startsWith('vorbis')) {
863
+ // Vorbis-specific validation
864
+ if (metadata.decoderConfig.codec !== 'vorbis') {
865
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for Vorbis must be "vorbis".');
866
+ }
867
+ if (!metadata.decoderConfig.description) {
868
+ throw new TypeError('Audio chunk metadata decoder configuration for Vorbis must include a description, which is expected to'
869
+ + ' adhere to the format described in https://www.w3.org/TR/webcodecs-vorbis-codec-registration/.');
870
+ }
871
+ }
872
+ else if (metadata.decoderConfig.codec.startsWith('flac')) {
873
+ // FLAC-specific validation
874
+ if (metadata.decoderConfig.codec !== 'flac') {
875
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for FLAC must be "flac".');
876
+ }
877
+ const minDescriptionSize = 4 + 4 + 34; // 'fLaC' + metadata block header + STREAMINFO block
878
+ if (!metadata.decoderConfig.description || metadata.decoderConfig.description.byteLength < minDescriptionSize) {
879
+ throw new TypeError('Audio chunk metadata decoder configuration for FLAC must include a description, which is expected to'
880
+ + ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.');
881
+ }
882
+ }
883
+ else if (metadata.decoderConfig.codec.startsWith('pcm')
884
+ || metadata.decoderConfig.codec.startsWith('ulaw')
885
+ || metadata.decoderConfig.codec.startsWith('alaw')) {
886
+ // PCM-specific validation
887
+ if (!PCM_AUDIO_CODECS.includes(metadata.decoderConfig.codec)) {
888
+ throw new TypeError('Audio chunk metadata decoder configuration codec string for PCM must be one of the supported PCM'
889
+ + ` codecs (${PCM_AUDIO_CODECS.join(', ')}).`);
890
+ }
891
+ }
892
+ };
893
+ export const validateSubtitleMetadata = (metadata) => {
894
+ if (!metadata) {
895
+ throw new TypeError('Subtitle metadata must be provided.');
896
+ }
897
+ if (typeof metadata !== 'object') {
898
+ throw new TypeError('Subtitle metadata must be an object.');
899
+ }
900
+ if (!metadata.config) {
901
+ throw new TypeError('Subtitle metadata must include a config object.');
902
+ }
903
+ if (typeof metadata.config !== 'object') {
904
+ throw new TypeError('Subtitle metadata config must be an object.');
905
+ }
906
+ if (typeof metadata.config.description !== 'string') {
907
+ throw new TypeError('Subtitle metadata config description must be a string.');
908
+ }
909
+ };
910
+ /**
911
+ * Checks if the browser is able to encode the given codec.
912
+ * @public
913
+ */
914
+ export const canEncode = (codec) => {
915
+ if (VIDEO_CODECS.includes(codec)) {
916
+ return canEncodeVideo(codec);
917
+ }
918
+ else if (AUDIO_CODECS.includes(codec)) {
919
+ return canEncodeAudio(codec);
920
+ }
921
+ else if (SUBTITLE_CODECS.includes(codec)) {
922
+ return canEncodeSubtitles(codec);
923
+ }
924
+ throw new TypeError(`Unknown codec '${codec}'.`);
925
+ };
926
+ /**
927
+ * Checks if the browser is able to encode the given video codec with the given parameters.
928
+ * @public
929
+ */
930
+ export const canEncodeVideo = async (codec, { width = 1280, height = 720, bitrate = 1e6 } = {}) => {
931
+ if (!VIDEO_CODECS.includes(codec)) {
932
+ return false;
933
+ }
934
+ if (!Number.isInteger(width) || width <= 0) {
935
+ throw new TypeError('width must be a positive integer.');
936
+ }
937
+ if (!Number.isInteger(height) || height <= 0) {
938
+ throw new TypeError('height must be a positive integer.');
939
+ }
940
+ if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
941
+ throw new TypeError('bitrate must be a positive integer or a quality.');
942
+ }
943
+ const resolvedBitrate = bitrate instanceof Quality
944
+ ? bitrate._toVideoBitrate(codec, width, height)
945
+ : bitrate;
946
+ if (customVideoEncoders.length > 0) {
947
+ const encoderConfig = {
948
+ codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
949
+ width,
950
+ height,
951
+ bitrate: resolvedBitrate,
952
+ ...getVideoEncoderConfigExtension(codec),
953
+ };
954
+ if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
955
+ // There's a custom encoder
956
+ return true;
957
+ }
958
+ }
959
+ if (typeof VideoEncoder === 'undefined') {
960
+ return false;
961
+ }
962
+ const support = await VideoEncoder.isConfigSupported({
963
+ codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
964
+ width,
965
+ height,
966
+ bitrate: resolvedBitrate,
967
+ ...getVideoEncoderConfigExtension(codec),
968
+ });
969
+ return support.supported === true;
970
+ };
971
+ /**
972
+ * Checks if the browser is able to encode the given audio codec with the given parameters.
973
+ * @public
974
+ */
975
+ export const canEncodeAudio = async (codec, { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3 } = {}) => {
976
+ if (!AUDIO_CODECS.includes(codec)) {
977
+ return false;
978
+ }
979
+ if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
980
+ throw new TypeError('numberOfChannels must be a positive integer.');
981
+ }
982
+ if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
983
+ throw new TypeError('sampleRate must be a positive integer.');
984
+ }
985
+ if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
986
+ throw new TypeError('bitrate must be a positive integer.');
987
+ }
988
+ const resolvedBitrate = bitrate instanceof Quality
989
+ ? bitrate._toAudioBitrate(codec)
990
+ : bitrate;
991
+ if (customAudioEncoders.length > 0) {
992
+ const encoderConfig = {
993
+ codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
994
+ numberOfChannels,
995
+ sampleRate,
996
+ bitrate: resolvedBitrate,
997
+ ...getAudioEncoderConfigExtension(codec),
998
+ };
999
+ if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
1000
+ // There's a custom encoder
1001
+ return true;
1002
+ }
1003
+ }
1004
+ if (PCM_AUDIO_CODECS.includes(codec)) {
1005
+ return true; // Because we encode these ourselves
1006
+ }
1007
+ if (typeof AudioEncoder === 'undefined') {
1008
+ return false;
1009
+ }
1010
+ const support = await AudioEncoder.isConfigSupported({
1011
+ codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
1012
+ numberOfChannels,
1013
+ sampleRate,
1014
+ bitrate: resolvedBitrate,
1015
+ ...getAudioEncoderConfigExtension(codec),
1016
+ });
1017
+ return support.supported === true;
1018
+ };
1019
+ /**
1020
+ * Checks if the browser is able to encode the given subtitle codec.
1021
+ * @public
1022
+ */
1023
+ export const canEncodeSubtitles = async (codec) => {
1024
+ if (!SUBTITLE_CODECS.includes(codec)) {
1025
+ return false;
1026
+ }
1027
+ return true;
1028
+ };
1029
+ /**
1030
+ * Returns the list of all media codecs that can be encoded by the browser.
1031
+ * @public
1032
+ */
1033
+ export const getEncodableCodecs = async () => {
1034
+ const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
1035
+ getEncodableVideoCodecs(),
1036
+ getEncodableAudioCodecs(),
1037
+ getEncodableSubtitleCodecs(),
1038
+ ]);
1039
+ return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
1040
+ };
1041
+ /**
1042
+ * Returns the list of all video codecs that can be encoded by the browser.
1043
+ * @public
1044
+ */
1045
+ export const getEncodableVideoCodecs = async (checkedCodecs = VIDEO_CODECS, options) => {
1046
+ const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
1047
+ return checkedCodecs.filter((_, i) => bools[i]);
1048
+ };
1049
+ /**
1050
+ * Returns the list of all audio codecs that can be encoded by the browser.
1051
+ * @public
1052
+ */
1053
+ export const getEncodableAudioCodecs = async (checkedCodecs = AUDIO_CODECS, options) => {
1054
+ const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
1055
+ return checkedCodecs.filter((_, i) => bools[i]);
1056
+ };
1057
+ /**
1058
+ * Returns the list of all subtitle codecs that can be encoded by the browser.
1059
+ * @public
1060
+ */
1061
+ export const getEncodableSubtitleCodecs = async (checkedCodecs = SUBTITLE_CODECS) => {
1062
+ const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
1063
+ return checkedCodecs.filter((_, i) => bools[i]);
1064
+ };
1065
+ /**
1066
+ * Returns the first video codec from the given list that can be encoded by the browser.
1067
+ * @public
1068
+ */
1069
+ export const getFirstEncodableVideoCodec = async (checkedCodecs, options) => {
1070
+ for (const codec of checkedCodecs) {
1071
+ if (await canEncodeVideo(codec, options)) {
1072
+ return codec;
1073
+ }
1074
+ }
1075
+ return null;
1076
+ };
1077
+ /**
1078
+ * Returns the first audio codec from the given list that can be encoded by the browser.
1079
+ * @public
1080
+ */
1081
+ export const getFirstEncodableAudioCodec = async (checkedCodecs, options) => {
1082
+ for (const codec of checkedCodecs) {
1083
+ if (await canEncodeAudio(codec, options)) {
1084
+ return codec;
1085
+ }
1086
+ }
1087
+ return null;
1088
+ };
1089
+ /**
1090
+ * Returns the first subtitle codec from the given list that can be encoded by the browser.
1091
+ * @public
1092
+ */
1093
+ export const getFirstEncodableSubtitleCodec = async (checkedCodecs) => {
1094
+ for (const codec of checkedCodecs) {
1095
+ if (await canEncodeSubtitles(codec)) {
1096
+ return codec;
1097
+ }
1098
+ }
1099
+ return null;
1100
+ };