1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 09:39:16 +02:00
bitjs/codecs/codecs.js

290 lines
9.3 KiB
JavaScript

/*
* codecs.js
*
* Licensed under the MIT License
*
* Copyright(c) 2022 Google Inc.
*/
/**
* This module helps interpret ffprobe -print_format json output.
* Its coverage is pretty sparse right now, so send me pull requests!
*/
/**
* @typedef ProbeStream ffprobe -show_streams -print_format json. Only the fields we care about.
* @property {number} index
* @property {string} codec_name
* @property {string} codec_long_name
* @property {string} profile
* @property {string} codec_type Either 'audio' or 'video'.
* @property {string} codec_tag_string
* @property {string} id
* @property {number?} level
* @property {number?} width
* @property {number?} height
* @property {string} r_frame_rate Like "60000/1001"
*/
/**
* @typedef ProbeFormat Only the fields we care about from the following command:
* ffprobe -show_format -show_streams -v quiet -print_format json -i file.mp4
* @property {string} filename
* @property {string} format_name
* @property {string} duration Number of seconds, as a string like "473.506367".
* @property {string} size Number of bytes, as a string.
* @property {string} bit_rate Bit rate, as a string.
*/
/**
* @typedef ProbeInfo ffprobe -show_format -show_streams -print_format json
* @property {ProbeStream[]} streams
* @property {ProbeFormat} format
*/
/**
* Maps the ffprobe format.format_name string to a short MIME type.
* @type {Object<string, string>}
*/
const FORMAT_NAME_TO_SHORT_TYPE = {
'avi': 'video/x-msvideo',
'flac': 'audio/flac',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
}
/**
* TODO: Reconcile this with file/sniffer.js findMimeType() which does signature matching.
* @param {ProbeInfo} info
* @returns {string}
*/
export function getShortMIMEString(info) {
if (!info) throw `Invalid ProbeInfo`;
if (!info.streams || info.streams.length === 0) throw `No streams in ProbeInfo`;
const formatName = info?.format?.format_name;
if (formatName && Object.keys(FORMAT_NAME_TO_SHORT_TYPE).includes(formatName)) {
return FORMAT_NAME_TO_SHORT_TYPE[formatName];
}
// M4A files are specifically audio/mp4.
if (info?.format?.filename?.toLowerCase().endsWith('.m4a')) {
return 'audio/mp4';
}
// Otherwise, any file with at least 1 video stream is considered video/.
// Otherwise, any file with at least 1 audio stream is considered audio/.
const type = info.streams.some(s => s.codec_type === 'video') ?
'video' :
info.streams.some(s => s.codec_type === 'audio') ? 'audio' : undefined;
if (!type) {
throw `Cannot handle media file type (no video/audio streams for ${info.format.format_name}). ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
/** @type {string} */
let subType;
switch (formatName) {
case 'mpeg':
subType = 'mpeg';
break;
case 'mov,mp4,m4a,3gp,3g2,mj2':
subType = 'mp4';
break;
case 'ogg':
subType = 'ogg';
break;
// Should we detect .mkv files as x-matroska?
case 'matroska,webm':
subType = 'webm';
break;
default:
throw `Cannot handle format ${formatName} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
return `${type}/${subType}`;
}
/**
* Accepts the ffprobe JSON output and returns an ISO MIME string with parameters (RFC6381), such
* as 'video/mp4; codecs="avc1.4D4028, mp4a.40.2"'. This string should be suitable to be used on
* the server as the Content-Type header of a media stream which can subsequently be used on the
* client as the type value of a SourceBuffer object `mediaSource.addSourceBuffer(contentType)`.
* NOTE: For now, this method fails hard (throws an error) when it encounters a format/codec it
* does not recognize. Please file a bug or send a PR.
* @param {ProbeInfo} info
* @returns {string}
*/
export function getFullMIMEString(info) {
/** A string like 'video/mp4' */
let contentType = `${getShortMIMEString(info)}`;
if (Object.values(FORMAT_NAME_TO_SHORT_TYPE).includes(contentType)) {
return contentType;
}
let codecFrags = new Set();
for (const stream of info.streams) {
if (stream.codec_type === 'audio') {
switch (stream.codec_tag_string) {
case 'mp4a': codecFrags.add(getMP4ACodecString(stream)); break;
default:
switch (stream.codec_name) {
case 'aac': codecFrags.add(getMP4ACodecString(stream)); break;
case 'vorbis': codecFrags.add('vorbis'); break;
case 'opus': codecFrags.add('opus'); break;
// I'm going off of what Chromium calls this one, with the dash.
case 'ac3': codecFrags.add('ac-3'); break;
case 'flac': codecFrags.add('flac'); break;
default:
throw `Could not handle audio codec_name ${stream.codec_name}, ` +
`codec_tag_string ${stream.codec_tag_string} for file ${info.format.filename} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
break;
}
}
else if (stream.codec_type === 'video') {
switch (stream.codec_tag_string) {
case 'avc1': codecFrags.add(getAVC1CodecString(stream)); break;
case 'vp09': codecFrags.add(getVP09CodecString(stream)); break;
// We don't handle these as video streams with codecs, so skip them.
case 'png': continue;
default:
switch (stream.codec_name) {
case 'h264': codecFrags.add(getAVC1CodecString(stream)); break;
case 'mpeg2video': codecFrags.add('mpeg2video'); break;
// Skip mjpeg as a video stream for the codecs string.
case 'mjpeg': break;
case 'vp9': codecFrags.add(getVP09CodecString(stream)); break;
default:
throw `Could not handle video codec_name ${stream.codec_name}, ` +
`codec_tag_string ${stream.codec_tag_string} for file ${info.format.filename} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
}
}
}
if (codecFrags.size === 0) return contentType;
return contentType + '; codecs="' + Array.from(codecFrags).join(',') + '"';
}
// TODO: Consider whether any of these should be exported.
/**
* AVC1 is the same thing as H264.
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#iso_base_media_file_format_mp4_quicktime_and_3gp
* @param {ProbeStream} stream
* @returns {string}
*/
function getAVC1CodecString(stream) {
if (!stream.profile) throw `No profile found in AVC1 stream`;
let frag = 'avc1';
// Add PP and CC hex digits.
switch (stream.profile) {
case 'Constrained Baseline':
frag += '.4240';
break;
case 'Baseline':
frag += '.4200';
break;
case 'Extended':
frag += '.5800';
break;
case 'Main':
frag += '.4D00';
break;
case 'High':
frag += '.6400';
break;
default:
throw `Cannot handle AVC1 stream with profile ${stream.profile} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
// Add LL hex digits.
const levelAsHex = Number(stream.level).toString(16).toUpperCase().padStart(2, '0');
if (levelAsHex.length !== 2) {
throw `Cannot handle AVC1 level ${stream.level} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
frag += levelAsHex;
return frag;
}
/**
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm
* @param {ProbeStream} stream
* @returns {string}
*/
function getVP09CodecString(stream) {
// TODO: Consider just returning 'vp9' here instead since I have so much guesswork.
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm
// The ISO format is cccc.PP.LL.DD
let frag = 'vp09';
// Add PP hex digits.
switch (stream.profile) {
case 'Profile 0':
frag += '.00';
break;
case 'Profile 1':
frag += '.01';
break;
case 'Profile 2':
frag += '.02';
break;
case 'Profile 3':
frag += '.03';
break;
default:
throw `Cannot handle VP09 stream with profile ${stream.profile} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
// Add LL hex digits.
// If ffprobe is spitting out -99 as level... Just return 'vp9'.
if (stream.level === -99) { return 'vp9'; }
else {
const levelAsHex = Number(stream.level).toString(16).toUpperCase().padStart(2, '0');
if (levelAsHex.length !== 2) {
throw `Cannot handle VP09 level ${stream.level} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
frag += `.${levelAsHex}`;
}
// Add DD hex digits.
// TODO: This is just a guess at DD (16?), need to try and extract this info from
// ffprobe JSON output instead.
frag += '.10';
return frag;
}
/**
* MP4A is the same as AAC.
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#mp4a
* @param {ProbeStream} stream
* @returns {string}
*/
function getMP4ACodecString(stream) {
let frag = 'mp4a.40';
switch (stream.profile) {
case 'LC':
frag += '.2';
break;
// TODO: more!
default:
throw `Cannot handle AAC stream with profile ${stream.profile} yet. ` +
`Please file a bug https://github.com/codedread/bitjs/issues/new`;
}
return frag;
}