Skip to content

Commit 623e99c

Browse files
committed
Add a package for detect audio/video codec information from ffprobe JSON output
1 parent d4d8548 commit 623e99c

File tree

3 files changed

+495
-0
lines changed

3 files changed

+495
-0
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ const zippedArrayBuffer = await zipper.start(
9696
true /* isLastFile */);
9797
```
9898

99+
### bitjs.codecs
100+
101+
This package includes code for dealing with media files (audio/video). It is useful for deriving
102+
ISO RFC6381 MIME type strings, including the codec information. Currently supports a limited subset
103+
of MP4 and WEBM.
104+
105+
How to use:
106+
107+
```javascript
108+
109+
import { getFullMIMEString } from 'bitjs/codecs/codecs.js';
110+
/**
111+
* @typedef {import('bitjs/codecs/codecs.js').ProbeInfo} ProbeInfo
112+
*/
113+
114+
const cmd = 'ffprobe -show_format -show_streams -print_format json -v quiet foo.mp4';
115+
exec(cmd, (error, stdout) => {
116+
/** @type {ProbeInfo} */
117+
const info = JSON.parse(stdout);
118+
// 'video/mp4; codecs="avc1.4D4028, mp4a.40.2"'
119+
const contentType = getFullMIMEString(info);
120+
```
121+
99122
### bitjs.file
100123
101124
This package includes code for dealing with files. It includes a sniffer which detects the type of file, given an ArrayBuffer.

codecs/codecs.js

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* codecs.js
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* Copyright(c) 2022 Google Inc.
7+
*/
8+
9+
/**
10+
* This module helps interpret ffprobe -print_format json output.
11+
* Its coverage is pretty sparse right now, so send me pull requests!
12+
*/
13+
14+
/**
15+
* @typdef ProbeStream ffprobe -show_streams -print_format json. Only the fields we care about.
16+
* @property {number} index
17+
* @property {string} codec_name
18+
* @property {string} codec_long_name
19+
* @property {string} profile
20+
* @property {string} codec_type Either 'audio' or 'video'.
21+
* @property {string} codec_tag_string
22+
* @property {string} id
23+
* @property {number?} level
24+
* @property {number?} width
25+
* @property {number?} height
26+
* @property {string} r_frame_rate Like "60000/1001"
27+
*/
28+
29+
/**
30+
* @typedef ProbeFormat ffprobe -show_format -print_format json. Only the fields we care about.
31+
* @property {string} filename
32+
* @property {string} format_name
33+
* @property {string} duration Number of seconds, as a string like "473.506367".
34+
* @property {string} size Number of bytes, as a string.
35+
* @property {string} bit_rate Bit rate, as a string.
36+
*/
37+
38+
/**
39+
* @typedef ProbeInfo ffprobe -show_format -show_streams -print_format json
40+
* @property {ProbeStream[]} streams
41+
* @property {ProbeFormat} format
42+
*/
43+
44+
/**
45+
* TODO: Reconcile this with file/sniffer.js findMimeType() which does signature matching.
46+
* @param {ProbeInfo} info
47+
* @returns {string}
48+
*/
49+
export function getShortMIMEString(info) {
50+
if (!info) throw `Invalid ProbeInfo`;
51+
if (!info.streams || info.streams.length === 0) throw `No streams in ProbeInfo`;
52+
53+
const type = info.streams.some(s => s.codec_type === 'video') ?
54+
'video' :
55+
info.streams.some(s => s.codec_type === 'audio') ? 'audio' : undefined;
56+
if (!type) {
57+
throw `Cannot handle media file type (no video/audio streams for ${info.format.format_name}). ` +
58+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
59+
}
60+
61+
/** @type {string} */
62+
let subType;
63+
switch (info.format.format_name) {
64+
case 'avi':
65+
subType = 'x-msvideo';
66+
break;
67+
case 'mpeg':
68+
subType = 'mpeg';
69+
break;
70+
case 'mov,mp4,m4a,3gp,3g2,mj2':
71+
subType = 'mp4';
72+
break;
73+
case 'ogg':
74+
subType = 'ogg';
75+
break;
76+
// Should we detect .mkv files as x-matroska?
77+
case 'matroska,webm':
78+
subType = 'webm';
79+
break;
80+
default:
81+
throw `Cannot handle format ${info.format.format_name} yet. ` +
82+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
83+
}
84+
85+
return `${type}/${subType}`;
86+
}
87+
88+
/**
89+
* Accepts the ffprobe JSON output and returns an ISO MIME string with parameters (RFC6381), such
90+
* as 'video/mp4; codecs="avc1.4D4028, mp4a.40.2"'. This string should be suitable to be used on
91+
* the server as the Content-Type header of a media stream which can subsequently be used on the
92+
* client as the type value of a SourceBuffer object `mediaSource.addSourceBuffer(contentType)`.
93+
* NOTE: For now, this method fails hard (throws an error) when it encounters a format/codec it
94+
* does not recognize. Please file a bug or send a PR.
95+
* @param {ProbeInfo} info
96+
* @returns {string}
97+
*/
98+
export function getFullMIMEString(info) {
99+
/** A string like 'video/mp4' */
100+
let contentType = `${getShortMIMEString(info)}`;
101+
let codecFrags = new Set();
102+
103+
for (const stream of info.streams) {
104+
if (stream.codec_type === 'audio') {
105+
// TODO! At least mp4a!
106+
}
107+
else if (stream.codec_type === 'video') {
108+
switch (stream.codec_tag_string) {
109+
case 'avc1': codecFrags.add(getAVC1CodecString(stream)); break;
110+
case 'vp09': codecFrags.add(getVP09CodecString(stream)); break;
111+
default:
112+
throw `Could not handle codec_tag_string ${stream.codec_tag_string} yet. ` +
113+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
114+
}
115+
}
116+
}
117+
118+
if (codecFrags.length === 0) return contentType;
119+
120+
return contentType + '; codecs="' + Array.from(codecFrags).join(',') + '"';
121+
}
122+
123+
// TODO: Consider whether any of these should be exported.
124+
125+
/**
126+
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#iso_base_media_file_format_mp4_quicktime_and_3gp
127+
* @param {ProbeStream} stream
128+
* @returns {string}
129+
*/
130+
function getAVC1CodecString(stream) {
131+
if (!stream.profile) throw `No profile found in AVC1 stream`;
132+
133+
let frag = 'avc1';
134+
135+
// Add PP and CC hex digits.
136+
switch (stream.profile) {
137+
case 'Constrained Baseline':
138+
frag += '.4240';
139+
break;
140+
case 'Baseline':
141+
frag += '.4200';
142+
break;
143+
case 'Extended':
144+
frag += '.5800';
145+
break;
146+
case 'Main':
147+
frag += '.4D00';
148+
break;
149+
case 'High':
150+
frag += '.6400';
151+
break;
152+
default:
153+
throw `Cannot handle AVC1 stream with profile ${stream.profile} yet. ` +
154+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
155+
}
156+
157+
// Add LL hex digits.
158+
const levelAsHex = Number(stream.level).toString(16).toUpperCase().padStart(2, '0');
159+
if (levelAsHex.length !== 2) {
160+
throw `Cannot handle AVC1 level ${stream.level} yet. ` +
161+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
162+
}
163+
frag += levelAsHex;
164+
165+
return frag;
166+
}
167+
168+
/**
169+
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm
170+
* @param {ProbeStream} stream
171+
* @returns {string}
172+
*/
173+
function getVP09CodecString(stream) {
174+
// TODO: Consider just returning 'vp9' here instead since I have so much guesswork.
175+
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm
176+
177+
// The ISO format is cccc.PP.LL.DD
178+
let frag = 'vp09';
179+
180+
// Add PP hex digits.
181+
switch (stream.profile) {
182+
case 'Profile 0':
183+
frag += '.00';
184+
break;
185+
case 'Profile 1':
186+
frag += '.01';
187+
break;
188+
case 'Profile 2':
189+
frag += '.02';
190+
break;
191+
case 'Profile 3':
192+
frag += '.03';
193+
break;
194+
default:
195+
throw `Cannot handle VP09 stream with profile ${stream.profile} yet. ` +
196+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
197+
}
198+
199+
// Add LL hex digits.
200+
// TODO: ffprobe is spitting out -99 as level... I'm guessing on LL here.
201+
if (stream.level === -99) { frag += '.FF'; }
202+
else {
203+
const levelAsHex = Number(stream.level).toString(16).toUpperCase().padStart(2, '0');
204+
if (levelAsHex.length !== 2) {
205+
throw `Cannot handle VP09 level ${stream.level} yet. ` +
206+
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
207+
}
208+
frag += `.${levelAsHex}`;
209+
}
210+
211+
// Add DD hex digits.
212+
// TODO: This is just a guess at DD (16?), need to try and extract this info from
213+
// ffprobe JSON output instead.
214+
frag += '.10';
215+
216+
return frag;
217+
}

0 commit comments

Comments
 (0)