You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

718 lines
20 KiB

4 years ago
/**
* mux.js
*
* Copyright (c) Brightcove
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
*
* Parse the internal MP4 structure into an equivalent javascript
* object.
*/
'use strict';
var MAX_UINT32 = Math.pow(2, 32);
var inspectMp4,
_textifyMp,
parseMp4Date = function parseMp4Date(seconds) {
return new Date(seconds * 1000 - 2082844800000);
},
parseType = require('../mp4/parse-type'),
findBox = require('../mp4/find-box'),
nalParse = function nalParse(avcStream) {
var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
result = [],
i,
length;
for (i = 0; i + 4 < avcStream.length; i += length) {
length = avcView.getUint32(i);
i += 4; // bail if this doesn't appear to be an H264 stream
if (length <= 0) {
result.push('<span style=\'color:red;\'>MALFORMED DATA</span>');
continue;
}
switch (avcStream[i] & 0x1F) {
case 0x01:
result.push('slice_layer_without_partitioning_rbsp');
break;
case 0x05:
result.push('slice_layer_without_partitioning_rbsp_idr');
break;
case 0x06:
result.push('sei_rbsp');
break;
case 0x07:
result.push('seq_parameter_set_rbsp');
break;
case 0x08:
result.push('pic_parameter_set_rbsp');
break;
case 0x09:
result.push('access_unit_delimiter_rbsp');
break;
default:
result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F);
break;
}
}
return result;
},
// registry of handlers for individual mp4 box types
parse = {
// codingname, not a first-class box type. stsd entries share the
// same format as real boxes so the parsing infrastructure can be
// shared
avc1: function avc1(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
dataReferenceIndex: view.getUint16(6),
width: view.getUint16(24),
height: view.getUint16(26),
horizresolution: view.getUint16(28) + view.getUint16(30) / 16,
vertresolution: view.getUint16(32) + view.getUint16(34) / 16,
frameCount: view.getUint16(40),
depth: view.getUint16(74),
config: inspectMp4(data.subarray(78, data.byteLength))
};
},
avcC: function avcC(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
configurationVersion: data[0],
avcProfileIndication: data[1],
profileCompatibility: data[2],
avcLevelIndication: data[3],
lengthSizeMinusOne: data[4] & 0x03,
sps: [],
pps: []
},
numOfSequenceParameterSets = data[5] & 0x1f,
numOfPictureParameterSets,
nalSize,
offset,
i; // iterate past any SPSs
offset = 6;
for (i = 0; i < numOfSequenceParameterSets; i++) {
nalSize = view.getUint16(offset);
offset += 2;
result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
offset += nalSize;
} // iterate past any PPSs
numOfPictureParameterSets = data[offset];
offset++;
for (i = 0; i < numOfPictureParameterSets; i++) {
nalSize = view.getUint16(offset);
offset += 2;
result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
offset += nalSize;
}
return result;
},
btrt: function btrt(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
bufferSizeDB: view.getUint32(0),
maxBitrate: view.getUint32(4),
avgBitrate: view.getUint32(8)
};
},
edts: function edts(data) {
return {
boxes: inspectMp4(data)
};
},
elst: function elst(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
edits: []
},
entryCount = view.getUint32(4),
i;
for (i = 8; entryCount; entryCount--) {
if (result.version === 0) {
result.edits.push({
segmentDuration: view.getUint32(i),
mediaTime: view.getInt32(i + 4),
mediaRate: view.getUint16(i + 8) + view.getUint16(i + 10) / (256 * 256)
});
i += 12;
} else {
result.edits.push({
segmentDuration: view.getUint32(i) * MAX_UINT32 + view.getUint32(i + 4),
mediaTime: view.getUint32(i + 8) * MAX_UINT32 + view.getUint32(i + 12),
mediaRate: view.getUint16(i + 16) + view.getUint16(i + 18) / (256 * 256)
});
i += 20;
}
}
return result;
},
esds: function esds(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
esId: data[6] << 8 | data[7],
streamPriority: data[8] & 0x1f,
decoderConfig: {
objectProfileIndication: data[11],
streamType: data[12] >>> 2 & 0x3f,
bufferSize: data[13] << 16 | data[14] << 8 | data[15],
maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19],
avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23],
decoderConfigDescriptor: {
tag: data[24],
length: data[25],
audioObjectType: data[26] >>> 3 & 0x1f,
samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01,
channelConfiguration: data[27] >>> 3 & 0x0f
}
}
};
},
ftyp: function ftyp(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
majorBrand: parseType(data.subarray(0, 4)),
minorVersion: view.getUint32(4),
compatibleBrands: []
},
i = 8;
while (i < data.byteLength) {
result.compatibleBrands.push(parseType(data.subarray(i, i + 4)));
i += 4;
}
return result;
},
dinf: function dinf(data) {
return {
boxes: inspectMp4(data)
};
},
dref: function dref(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
dataReferences: inspectMp4(data.subarray(8))
};
},
hdlr: function hdlr(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
handlerType: parseType(data.subarray(8, 12)),
name: ''
},
i = 8; // parse out the name field
for (i = 24; i < data.byteLength; i++) {
if (data[i] === 0x00) {
// the name field is null-terminated
i++;
break;
}
result.name += String.fromCharCode(data[i]);
} // decode UTF-8 to javascript's internal representation
// see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
result.name = decodeURIComponent(escape(result.name));
return result;
},
mdat: function mdat(data) {
return {
byteLength: data.byteLength,
nals: nalParse(data)
};
},
mdhd: function mdhd(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
i = 4,
language,
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
language: ''
};
if (result.version === 1) {
i += 4;
result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 8;
result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 4;
result.timescale = view.getUint32(i);
i += 8;
result.duration = view.getUint32(i); // truncating top 4 bytes
} else {
result.creationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.modificationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.timescale = view.getUint32(i);
i += 4;
result.duration = view.getUint32(i);
}
i += 4; // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
// each field is the packed difference between its ASCII value and 0x60
language = view.getUint16(i);
result.language += String.fromCharCode((language >> 10) + 0x60);
result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60);
result.language += String.fromCharCode((language & 0x1f) + 0x60);
return result;
},
mdia: function mdia(data) {
return {
boxes: inspectMp4(data)
};
},
mfhd: function mfhd(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
};
},
minf: function minf(data) {
return {
boxes: inspectMp4(data)
};
},
// codingname, not a first-class box type. stsd entries share the
// same format as real boxes so the parsing infrastructure can be
// shared
mp4a: function mp4a(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
// 6 bytes reserved
dataReferenceIndex: view.getUint16(6),
// 4 + 4 bytes reserved
channelcount: view.getUint16(16),
samplesize: view.getUint16(18),
// 2 bytes pre_defined
// 2 bytes reserved
samplerate: view.getUint16(24) + view.getUint16(26) / 65536
}; // if there are more bytes to process, assume this is an ISO/IEC
// 14496-14 MP4AudioSampleEntry and parse the ESDBox
if (data.byteLength > 28) {
result.streamDescriptor = inspectMp4(data.subarray(28))[0];
}
return result;
},
moof: function moof(data) {
return {
boxes: inspectMp4(data)
};
},
moov: function moov(data) {
return {
boxes: inspectMp4(data)
};
},
mvex: function mvex(data) {
return {
boxes: inspectMp4(data)
};
},
mvhd: function mvhd(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
i = 4,
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4))
};
if (result.version === 1) {
i += 4;
result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 8;
result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 4;
result.timescale = view.getUint32(i);
i += 8;
result.duration = view.getUint32(i); // truncating top 4 bytes
} else {
result.creationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.modificationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.timescale = view.getUint32(i);
i += 4;
result.duration = view.getUint32(i);
}
i += 4; // convert fixed-point, base 16 back to a number
result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16;
i += 4;
result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
i += 2;
i += 2;
i += 2 * 4;
result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
i += 9 * 4;
i += 6 * 4;
result.nextTrackId = view.getUint32(i);
return result;
},
pdin: function pdin(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
rate: view.getUint32(4),
initialDelay: view.getUint32(8)
};
},
sdtp: function sdtp(data) {
var result = {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
samples: []
},
i;
for (i = 4; i < data.byteLength; i++) {
result.samples.push({
dependsOn: (data[i] & 0x30) >> 4,
isDependedOn: (data[i] & 0x0c) >> 2,
hasRedundancy: data[i] & 0x03
});
}
return result;
},
sidx: require('./parse-sidx.js'),
smhd: function smhd(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
balance: data[4] + data[5] / 256
};
},
stbl: function stbl(data) {
return {
boxes: inspectMp4(data)
};
},
stco: function stco(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
chunkOffsets: []
},
entryCount = view.getUint32(4),
i;
for (i = 8; entryCount; i += 4, entryCount--) {
result.chunkOffsets.push(view.getUint32(i));
}
return result;
},
stsc: function stsc(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
entryCount = view.getUint32(4),
result = {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
sampleToChunks: []
},
i;
for (i = 8; entryCount; i += 12, entryCount--) {
result.sampleToChunks.push({
firstChunk: view.getUint32(i),
samplesPerChunk: view.getUint32(i + 4),
sampleDescriptionIndex: view.getUint32(i + 8)
});
}
return result;
},
stsd: function stsd(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
sampleDescriptions: inspectMp4(data.subarray(8))
};
},
stsz: function stsz(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
sampleSize: view.getUint32(4),
entries: []
},
i;
for (i = 12; i < data.byteLength; i += 4) {
result.entries.push(view.getUint32(i));
}
return result;
},
stts: function stts(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
timeToSamples: []
},
entryCount = view.getUint32(4),
i;
for (i = 8; entryCount; i += 8, entryCount--) {
result.timeToSamples.push({
sampleCount: view.getUint32(i),
sampleDelta: view.getUint32(i + 4)
});
}
return result;
},
styp: function styp(data) {
return parse.ftyp(data);
},
tfdt: require('./parse-tfdt.js'),
tfhd: require('./parse-tfhd.js'),
tkhd: function tkhd(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
i = 4,
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4))
};
if (result.version === 1) {
i += 4;
result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 8;
result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
i += 4;
result.trackId = view.getUint32(i);
i += 4;
i += 8;
result.duration = view.getUint32(i); // truncating top 4 bytes
} else {
result.creationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.modificationTime = parseMp4Date(view.getUint32(i));
i += 4;
result.trackId = view.getUint32(i);
i += 4;
i += 4;
result.duration = view.getUint32(i);
}
i += 4;
i += 2 * 4;
result.layer = view.getUint16(i);
i += 2;
result.alternateGroup = view.getUint16(i);
i += 2; // convert fixed-point, base 16 back to a number
result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
i += 2;
i += 2;
result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
i += 9 * 4;
result.width = view.getUint16(i) + view.getUint16(i + 2) / 65536;
i += 4;
result.height = view.getUint16(i) + view.getUint16(i + 2) / 65536;
return result;
},
traf: function traf(data) {
return {
boxes: inspectMp4(data)
};
},
trak: function trak(data) {
return {
boxes: inspectMp4(data)
};
},
trex: function trex(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
trackId: view.getUint32(4),
defaultSampleDescriptionIndex: view.getUint32(8),
defaultSampleDuration: view.getUint32(12),
defaultSampleSize: view.getUint32(16),
sampleDependsOn: data[20] & 0x03,
sampleIsDependedOn: (data[21] & 0xc0) >> 6,
sampleHasRedundancy: (data[21] & 0x30) >> 4,
samplePaddingValue: (data[21] & 0x0e) >> 1,
sampleIsDifferenceSample: !!(data[21] & 0x01),
sampleDegradationPriority: view.getUint16(22)
};
},
trun: require('./parse-trun.js'),
'url ': function url(data) {
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4))
};
},
vmhd: function vmhd(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
version: data[0],
flags: new Uint8Array(data.subarray(1, 4)),
graphicsmode: view.getUint16(4),
opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)])
};
}
};
/**
* Return a javascript array of box objects parsed from an ISO base
* media file.
* @param data {Uint8Array} the binary data of the media to be inspected
* @return {array} a javascript array of potentially nested box objects
*/
inspectMp4 = function inspectMp4(data) {
var i = 0,
result = [],
view,
size,
type,
end,
box; // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
var ab = new ArrayBuffer(data.length);
var v = new Uint8Array(ab);
for (var z = 0; z < data.length; ++z) {
v[z] = data[z];
}
view = new DataView(ab);
while (i < data.byteLength) {
// parse box data
size = view.getUint32(i);
type = parseType(data.subarray(i + 4, i + 8));
end = size > 1 ? i + size : data.byteLength; // parse type-specific data
box = (parse[type] || function (data) {
return {
data: data
};
})(data.subarray(i + 8, end));
box.size = size;
box.type = type; // store this box and move to the next
result.push(box);
i = end;
}
return result;
};
/**
* Returns a textual representation of the javascript represtentation
* of an MP4 file. You can use it as an alternative to
* JSON.stringify() to compare inspected MP4s.
* @param inspectedMp4 {array} the parsed array of boxes in an MP4
* file
* @param depth {number} (optional) the number of ancestor boxes of
* the elements of inspectedMp4. Assumed to be zero if unspecified.
* @return {string} a text representation of the parsed MP4
*/
_textifyMp = function textifyMp4(inspectedMp4, depth) {
var indent;
depth = depth || 0;
indent = new Array(depth * 2 + 1).join(' '); // iterate over all the boxes
return inspectedMp4.map(function (box, index) {
// list the box type first at the current indentation level
return indent + box.type + '\n' + // the type is already included and handle child boxes separately
Object.keys(box).filter(function (key) {
return key !== 'type' && key !== 'boxes'; // output all the box properties
}).map(function (key) {
var prefix = indent + ' ' + key + ': ',
value = box[key]; // print out raw bytes as hexademical
if (value instanceof Uint8Array || value instanceof Uint32Array) {
var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (byte) {
return ' ' + ('00' + byte.toString(16)).slice(-2);
}).join('').match(/.{1,24}/g);
if (!bytes) {
return prefix + '<>';
}
if (bytes.length === 1) {
return prefix + '<' + bytes.join('').slice(1) + '>';
}
return prefix + '<\n' + bytes.map(function (line) {
return indent + ' ' + line;
}).join('\n') + '\n' + indent + ' >';
} // stringify generic objects
return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) {
if (index === 0) {
return line;
}
return indent + ' ' + line;
}).join('\n');
}).join('\n') + ( // recursively textify the child boxes
box.boxes ? '\n' + _textifyMp(box.boxes, depth + 1) : '');
}).join('\n');
};
module.exports = {
inspect: inspectMp4,
textify: _textifyMp,
parseType: parseType,
findBox: findBox,
parseTraf: parse.traf,
parseTfdt: parse.tfdt,
parseHdlr: parse.hdlr,
parseTfhd: parse.tfhd,
parseTrun: parse.trun,
parseSidx: parse.sidx
};