utils.ts
⎇
Raw
1import { AudioCodec, MediaContainer, VideoCodec } from "./types";
2
3export function decodePath(path: string) {
4 return path
5 .split("/")
6 .map((segment) => decodeURIComponent(segment))
7 .join("/");
8}
9
10export function encodePath(path: string) {
11 return path
12 .split("/")
13 .map((segment) => encodeURIComponent(segment))
14 .join("/");
15}
16
17//scale the video down to keep the requested bitrate plausible for its resolution. each width is the
18//largest one still reasonable at that bitrate ceiling; above the last rung the source is left alone.
19//shared because the server needs it to build the scale filter and the client needs it to predict the
20//output resolution the codec level is derived from - the two strings have to agree
21export const RESOLUTION_LADDER: { upToBitrate: number; width: number }[] = [
22 { upToBitrate: 400_000, width: 640 },
23 { upToBitrate: 800_000, width: 854 },
24 { upToBitrate: 2_500_000, width: 1280 },
25 { upToBitrate: 5_000_000, width: 1920 },
26 { upToBitrate: 12_000_000, width: 2560 },
27];
28
29export function ladderMaxWidth(videoBitrate: number): number | undefined {
30 return RESOLUTION_LADDER.find((rung) => videoBitrate <= rung.upToBitrate)?.width;
31}
32
33//-b:v alone is plain ABR with no VBV cap, so the real peak runs above the target. the level limits are
34//compared against the peak, and under-stating the level is the failure mode that breaks playback
35//mid-stream, so allow headroom
36const BITRATE_PEAK_MARGIN = 1.5;
37
38export interface VideoStreamInfo {
39 //source dimensions, before the ladder scales them down
40 sourceWidth: number;
41 sourceHeight: number;
42 frameRate: number;
43 interlaced: boolean;
44 //the requested bitrate, which is what selects the ladder rung
45 bitrate: number;
46}
47
48//mirrors -vf scale='if(gt(iw,W),W,iw)':-2 - below the rung width the source passes through untouched
49export function transcodeOutputSize(sourceWidth: number, sourceHeight: number, videoBitrate: number) {
50 const maxWidth = ladderMaxWidth(videoBitrate);
51 if (maxWidth === undefined || sourceWidth <= maxWidth) return { width: sourceWidth, height: sourceHeight };
52 //-2 keeps the aspect ratio at a multiple of two. rounding up rather than to nearest can only ever
53 //over-state the level by one rung, which is the harmless direction
54 return { width: maxWidth, height: Math.ceil((sourceHeight * maxWidth) / sourceWidth / 2) * 2 };
55}
56
57//Codec levels are picked by the encoder from the resolution, frame rate and bitrate, so they can be
58//derived rather than guessed. Each table below is the level list from the relevant spec; the limits are
59//committee-chosen constants with no generating formula, so they are data. Verified against the strings
60//ffmpeg computes for its own output (its DASH muxer writes them into the MPD).
61//
62//The tables are ordered lowest level first and the first satisfying entry wins, which is what makes
63//arbitrary resolutions and fractional frame rates fall out without special cases.
64
65//ITU-T H.264 Annex A, Table A-1. FS is the frame size in macroblocks, MBPS = FS * fps. MaxBR is scaled
66//by cpbBrVclFactor, which is 1250 for High profile (the profile the encoder is pinned to).
67const H264_LEVELS = [
68 { idc: 0x0a, maxMBPS: 1485, maxFS: 99, maxBR: 64 },
69 { idc: 0x0b, maxMBPS: 3000, maxFS: 396, maxBR: 192 },
70 { idc: 0x0c, maxMBPS: 6000, maxFS: 396, maxBR: 384 },
71 { idc: 0x0d, maxMBPS: 11880, maxFS: 396, maxBR: 768 },
72 { idc: 0x14, maxMBPS: 11880, maxFS: 396, maxBR: 2000 },
73 { idc: 0x15, maxMBPS: 19800, maxFS: 792, maxBR: 4000 },
74 { idc: 0x16, maxMBPS: 20250, maxFS: 1620, maxBR: 4000 },
75 { idc: 0x1e, maxMBPS: 40500, maxFS: 1620, maxBR: 10000 },
76 { idc: 0x1f, maxMBPS: 108000, maxFS: 3600, maxBR: 14000 },
77 { idc: 0x20, maxMBPS: 216000, maxFS: 5120, maxBR: 20000 },
78 { idc: 0x28, maxMBPS: 245760, maxFS: 8192, maxBR: 20000 },
79 { idc: 0x29, maxMBPS: 245760, maxFS: 8192, maxBR: 50000 },
80 { idc: 0x2a, maxMBPS: 522240, maxFS: 8704, maxBR: 50000 },
81 { idc: 0x32, maxMBPS: 589824, maxFS: 22080, maxBR: 135000 },
82 { idc: 0x33, maxMBPS: 983040, maxFS: 36864, maxBR: 240000 },
83 { idc: 0x34, maxMBPS: 2073600, maxFS: 36864, maxBR: 240000 },
84 { idc: 0x3c, maxMBPS: 4177920, maxFS: 139264, maxBR: 240000 },
85 { idc: 0x3d, maxMBPS: 8355840, maxFS: 139264, maxBR: 480000 },
86 { idc: 0x3e, maxMBPS: 16711680, maxFS: 139264, maxBR: 800000 },
87];
88
89//ITU-T H.265 Annex A, Tables A.6 (picture size and sample rate) and A.7 (Main tier bit rate).
90//general_level_idc is the level times 30.
91const H265_LEVELS = [
92 { idc: 30, maxLumaPs: 36864, maxLumaSr: 552960, maxBR: 128 },
93 { idc: 60, maxLumaPs: 122880, maxLumaSr: 3686400, maxBR: 1500 },
94 { idc: 63, maxLumaPs: 245760, maxLumaSr: 7372800, maxBR: 3000 },
95 { idc: 90, maxLumaPs: 552960, maxLumaSr: 16588800, maxBR: 6000 },
96 { idc: 93, maxLumaPs: 983040, maxLumaSr: 33177600, maxBR: 10000 },
97 { idc: 120, maxLumaPs: 2228224, maxLumaSr: 66846720, maxBR: 12000 },
98 { idc: 123, maxLumaPs: 2228224, maxLumaSr: 133693440, maxBR: 20000 },
99 { idc: 150, maxLumaPs: 8912896, maxLumaSr: 267386880, maxBR: 25000 },
100 { idc: 153, maxLumaPs: 8912896, maxLumaSr: 534773760, maxBR: 40000 },
101 { idc: 156, maxLumaPs: 8912896, maxLumaSr: 1069547520, maxBR: 60000 },
102 { idc: 180, maxLumaPs: 35651584, maxLumaSr: 1069547520, maxBR: 60000 },
103 { idc: 183, maxLumaPs: 35651584, maxLumaSr: 2139095040, maxBR: 120000 },
104 { idc: 186, maxLumaPs: 35651584, maxLumaSr: 4278190080, maxBR: 240000 },
105];
106
107//VP9 Bitstream & Decoding Process Specification v0.6, Annex A. "code" is the level times 10.
108const VP9_LEVELS = [
109 { code: 10, maxSr: 829440, maxPs: 36864, maxBR: 200, maxDim: 512 },
110 { code: 11, maxSr: 2764800, maxPs: 73728, maxBR: 800, maxDim: 768 },
111 { code: 20, maxSr: 4608000, maxPs: 122880, maxBR: 1800, maxDim: 960 },
112 { code: 21, maxSr: 9216000, maxPs: 245760, maxBR: 3600, maxDim: 1344 },
113 { code: 30, maxSr: 20736000, maxPs: 552960, maxBR: 7200, maxDim: 2048 },
114 { code: 31, maxSr: 36864000, maxPs: 983040, maxBR: 12000, maxDim: 2752 },
115 { code: 40, maxSr: 83558400, maxPs: 2228224, maxBR: 18000, maxDim: 4160 },
116 { code: 41, maxSr: 160432128, maxPs: 2228224, maxBR: 30000, maxDim: 4160 },
117 { code: 50, maxSr: 311951360, maxPs: 8912896, maxBR: 60000, maxDim: 8384 },
118 { code: 51, maxSr: 588251136, maxPs: 8912896, maxBR: 120000, maxDim: 8384 },
119 { code: 52, maxSr: 1176502272, maxPs: 8912896, maxBR: 180000, maxDim: 8384 },
120 { code: 60, maxSr: 1176502272, maxPs: 35651584, maxBR: 180000, maxDim: 16832 },
121 { code: 61, maxSr: 2353004544, maxPs: 35651584, maxBR: 240000, maxDim: 16832 },
122 { code: 62, maxSr: 4706009088, maxPs: 35651584, maxBR: 480000, maxDim: 16832 },
123];
124
125//AV1 Bitstream & Decoding Process Specification, Annex A, Table A.1. seq_level_idx is
126//(major - 2) * 4 + minor, so the numbering has deliberate gaps. mbps is the Main tier column.
127const AV1_LEVELS = [
128 { idx: 0, maxPicSize: 147456, maxH: 2048, maxV: 1152, maxDisplayRate: 4423680, mbps: 1.5 },
129 { idx: 1, maxPicSize: 278784, maxH: 2816, maxV: 1584, maxDisplayRate: 8363520, mbps: 3.0 },
130 { idx: 4, maxPicSize: 665856, maxH: 4352, maxV: 2448, maxDisplayRate: 19975680, mbps: 6.0 },
131 { idx: 5, maxPicSize: 1065024, maxH: 5504, maxV: 3096, maxDisplayRate: 31950720, mbps: 10.0 },
132 { idx: 8, maxPicSize: 2359296, maxH: 6144, maxV: 3456, maxDisplayRate: 70778880, mbps: 12.0 },
133 { idx: 9, maxPicSize: 2359296, maxH: 6144, maxV: 3456, maxDisplayRate: 141557760, mbps: 20.0 },
134 { idx: 12, maxPicSize: 8912896, maxH: 8192, maxV: 4352, maxDisplayRate: 267386880, mbps: 30.0 },
135 { idx: 13, maxPicSize: 8912896, maxH: 8192, maxV: 4352, maxDisplayRate: 534773760, mbps: 40.0 },
136 { idx: 14, maxPicSize: 8912896, maxH: 8192, maxV: 4352, maxDisplayRate: 1069547520, mbps: 60.0 },
137 { idx: 16, maxPicSize: 35651584, maxH: 16384, maxV: 8704, maxDisplayRate: 1069547520, mbps: 60.0 },
138 { idx: 17, maxPicSize: 35651584, maxH: 16384, maxV: 8704, maxDisplayRate: 2139095040, mbps: 100.0 },
139 { idx: 18, maxPicSize: 35651584, maxH: 16384, maxV: 8704, maxDisplayRate: 4278190080, mbps: 160.0 },
140];
141
142//an input beyond the last level, or missing source metadata, falls back to the highest level. claiming
143//more than the stream needs can only make a weak client refuse a type it would have managed, and it
144//then falls back to progressive playback; claiming less lets it start and fail part-way through
145function highest<T>(table: T[]): T {
146 return table[table.length - 1];
147}
148
149export function videoCodecToken(videoCodec: VideoCodec, video: VideoStreamInfo | undefined): string {
150 //the encoder sees one frame per field once yadif has run
151 const fps = video ? (video.interlaced ? video.frameRate * 2 : video.frameRate) : 0;
152 const size = video ? transcodeOutputSize(video.sourceWidth, video.sourceHeight, video.bitrate) : undefined;
153 const kbps = video ? (video.bitrate * BITRATE_PEAK_MARGIN) / 1000 : 0;
154
155 switch (videoCodec) {
156 case VideoCodec.av1: {
157 const pixels = size ? size.width * size.height : 0;
158 const level =
159 (size &&
160 AV1_LEVELS.find(
161 (l) =>
162 pixels <= l.maxPicSize &&
163 size.width <= l.maxH &&
164 size.height <= l.maxV &&
165 pixels * fps <= l.maxDisplayRate &&
166 kbps / 1000 <= l.mbps,
167 )) ||
168 highest(AV1_LEVELS);
169 //profile 0 (Main), Main tier, 8 bit
170 return `av01.0.${level.idx.toString().padStart(2, "0")}M.08`;
171 }
172 case VideoCodec.h264: {
173 const mbWidth = size ? Math.ceil(size.width / 16) : 0;
174 const mbHeight = size ? Math.ceil(size.height / 16) : 0;
175 const fs = mbWidth * mbHeight;
176 const level =
177 (size &&
178 H264_LEVELS.find(
179 (l) =>
180 fs <= l.maxFS &&
181 fs * fps <= l.maxMBPS &&
182 //Annex A.3.1 also caps each dimension individually
183 mbWidth <= Math.sqrt(l.maxFS * 8) &&
184 mbHeight <= Math.sqrt(l.maxFS * 8) &&
185 kbps <= l.maxBR * 1.25,
186 )) ||
187 highest(H264_LEVELS);
188 //High profile (profile_idc 100 = 0x64) with no constraint flags set
189 return `avc1.6400${level.idc.toString(16).padStart(2, "0")}`;
190 }
191 case VideoCodec.h265: {
192 const pixels = size ? size.width * size.height : 0;
193 const level =
194 (size &&
195 H265_LEVELS.find(
196 (l) =>
197 pixels <= l.maxLumaPs &&
198 pixels * fps <= l.maxLumaSr &&
199 size.width <= Math.sqrt(l.maxLumaPs * 8) &&
200 size.height <= Math.sqrt(l.maxLumaPs * 8) &&
201 kbps <= l.maxBR,
202 )) ||
203 highest(H265_LEVELS);
204 //Main profile: profile_space 0, profile_idc 1, compatibility flags 0x60000000 written
205 //bit-reversed as hex ("6"), Main tier ("L"). B0 = progressive, non-packed, frame-only.
206 return `hev1.1.6.L${level.idc}.B0`;
207 }
208 case VideoCodec.vp9: {
209 const pixels = size ? size.width * size.height : 0;
210 const level =
211 (size &&
212 VP9_LEVELS.find(
213 (l) =>
214 pixels <= l.maxPs &&
215 pixels * fps <= l.maxSr &&
216 Math.max(size.width, size.height) <= l.maxDim &&
217 kbps <= l.maxBR,
218 )) ||
219 highest(VP9_LEVELS);
220 //profile 0 (4:2:0), 8 bit
221 return `vp09.00.${level.code.toString().padStart(2, "0")}.08`;
222 }
223 default:
224 throw new Error(`Unsupported video codec: ${videoCodec}`);
225 }
226}
227
228export function audioCodecToken(audioCodec: AudioCodec): string {
229 switch (audioCodec) {
230 case AudioCodec.opus:
231 return "opus";
232 case AudioCodec.aac:
233 //bare "aac" is not a codec string any browser accepts. 0x40 is the MP4RA object type for
234 //ISO/IEC 14496-3 audio and 2 is the AAC-LC audio object type within it
235 return "mp4a.40.2";
236 case AudioCodec.vorbis:
237 return "vorbis";
238 case AudioCodec.mp3:
239 //"mp3" is not a registered codec string and was rejected everywhere. the MP4RA object type for
240 //MPEG-1 layer III audio is 0x6B (0x69 is the MPEG-2 variant), and only means anything inside mp4 -
241 //the mp3 container names no codec at all. chromium plays mp4a.6B progressively but refuses it in a
242 //SourceBuffer, which is a distinction the old token could not express because it failed both
243 return "mp4a.6B";
244 case AudioCodec.flac:
245 return "flac";
246 default:
247 throw new Error(`Unsupported audio codec: ${audioCodec}`);
248 }
249}
250
251//the MPEG Audio byte stream format spec is explicit that the codecs parameter MUST NOT be used with
252//audio/mpeg, and there is exactly one codec it could ever name anyway. every other container needs it
253export function containerNamesCodecs(container: MediaContainer): boolean {
254 return container !== MediaContainer.mp3;
255}
256
257//an audio-only stream is not a video resource, and the video/ prefix on one is not merely cosmetic:
258//it changes what the byte stream format expects to find
259export function containerMimeType(container: MediaContainer, hasVideo: boolean): string {
260 const prefix = hasVideo ? "video" : "audio";
261 switch (container) {
262 case MediaContainer.mp4:
263 return `${prefix}/mp4`;
264 case MediaContainer.webm:
265 return `${prefix}/webm`;
266 case MediaContainer.mp3:
267 return "audio/mpeg";
268 case MediaContainer.ogg:
269 return "audio/ogg";
270 default:
271 throw new Error(`Unsupported container: ${container}`);
272 }
273}
274
275export function audioCodecHasBitrate(audioCodec: AudioCodec): boolean {
276 return audioCodec !== AudioCodec.flac;
277}
278
279export function createRFC6381String(
280 container: MediaContainer,
281 videoCodec: VideoCodec,
282 audioCodec: AudioCodec,
283 video?: VideoStreamInfo,
284): string {
285 const hasVideo = videoCodec !== VideoCodec.none;
286 const mimeType = containerMimeType(container, hasVideo);
287 if (!containerNamesCodecs(container)) return mimeType;
288
289 const codecs = hasVideo
290 ? `${videoCodecToken(videoCodec, video)}, ${audioCodecToken(audioCodec)}`
291 : audioCodecToken(audioCodec);
292 return `${mimeType}; codecs="${codecs}"`;
293}
294