mirror of
https://github.com/DanielnetoDotCom/YouPHPTube
synced 2025-10-05 02:39:46 +02:00
Also check the lang in case insensitive
This commit is contained in:
parent
33e7f7384e
commit
2a9630258f
22658 changed files with 3562773 additions and 3562767 deletions
314
node_modules/mux.js/lib/mp4/audio-frame-utils.js
generated
vendored
314
node_modules/mux.js/lib/mp4/audio-frame-utils.js
generated
vendored
|
@ -1,157 +1,157 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
var coneOfSilence = require('../data/silence');
|
||||
var clock = require('../utils/clock');
|
||||
|
||||
/**
|
||||
* Sum the `byteLength` properties of the data in each AAC frame
|
||||
*/
|
||||
var sumFrameByteLengths = function(array) {
|
||||
var
|
||||
i,
|
||||
currentObj,
|
||||
sum = 0;
|
||||
|
||||
// sum the byteLength's all each nal unit in the frame
|
||||
for (i = 0; i < array.length; i++) {
|
||||
currentObj = array[i];
|
||||
sum += currentObj.data.byteLength;
|
||||
}
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
||||
// Possibly pad (prefix) the audio track with silence if appending this track
|
||||
// would lead to the introduction of a gap in the audio buffer
|
||||
var prefixWithSilence = function(
|
||||
track,
|
||||
frames,
|
||||
audioAppendStartTs,
|
||||
videoBaseMediaDecodeTime
|
||||
) {
|
||||
var
|
||||
baseMediaDecodeTimeTs,
|
||||
frameDuration = 0,
|
||||
audioGapDuration = 0,
|
||||
audioFillFrameCount = 0,
|
||||
audioFillDuration = 0,
|
||||
silentFrame,
|
||||
i,
|
||||
firstFrame;
|
||||
|
||||
if (!frames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseMediaDecodeTimeTs =
|
||||
clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate);
|
||||
// determine frame clock duration based on sample rate, round up to avoid overfills
|
||||
frameDuration = Math.ceil(clock.ONE_SECOND_IN_TS / (track.samplerate / 1024));
|
||||
|
||||
if (audioAppendStartTs && videoBaseMediaDecodeTime) {
|
||||
// insert the shortest possible amount (audio gap or audio to video gap)
|
||||
audioGapDuration =
|
||||
baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime);
|
||||
// number of full frames in the audio gap
|
||||
audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
|
||||
audioFillDuration = audioFillFrameCount * frameDuration;
|
||||
}
|
||||
|
||||
// don't attempt to fill gaps smaller than a single frame or larger
|
||||
// than a half second
|
||||
if (audioFillFrameCount < 1 || audioFillDuration > clock.ONE_SECOND_IN_TS / 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
silentFrame = coneOfSilence()[track.samplerate];
|
||||
|
||||
if (!silentFrame) {
|
||||
// we don't have a silent frame pregenerated for the sample rate, so use a frame
|
||||
// from the content instead
|
||||
silentFrame = frames[0].data;
|
||||
}
|
||||
|
||||
for (i = 0; i < audioFillFrameCount; i++) {
|
||||
firstFrame = frames[0];
|
||||
|
||||
frames.splice(0, 0, {
|
||||
data: silentFrame,
|
||||
dts: firstFrame.dts - frameDuration,
|
||||
pts: firstFrame.pts - frameDuration
|
||||
});
|
||||
}
|
||||
|
||||
track.baseMediaDecodeTime -=
|
||||
Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate));
|
||||
|
||||
return audioFillDuration;
|
||||
};
|
||||
|
||||
// If the audio segment extends before the earliest allowed dts
|
||||
// value, remove AAC frames until starts at or after the earliest
|
||||
// allowed DTS so that we don't end up with a negative baseMedia-
|
||||
// DecodeTime for the audio track
|
||||
var trimAdtsFramesByEarliestDts = function(adtsFrames, track, earliestAllowedDts) {
|
||||
if (track.minSegmentDts >= earliestAllowedDts) {
|
||||
return adtsFrames;
|
||||
}
|
||||
|
||||
// We will need to recalculate the earliest segment Dts
|
||||
track.minSegmentDts = Infinity;
|
||||
|
||||
return adtsFrames.filter(function(currentFrame) {
|
||||
// If this is an allowed frame, keep it and record it's Dts
|
||||
if (currentFrame.dts >= earliestAllowedDts) {
|
||||
track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
|
||||
track.minSegmentPts = track.minSegmentDts;
|
||||
return true;
|
||||
}
|
||||
// Otherwise, discard it
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from an array of frames
|
||||
var generateSampleTable = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
samples = [];
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
samples.push({
|
||||
size: currentFrame.data.byteLength,
|
||||
duration: 1024 // For AAC audio, all samples contain 1024 samples
|
||||
});
|
||||
}
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's sample table from an array of frames
|
||||
var concatenateFrameData = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
dataOffset = 0,
|
||||
data = new Uint8Array(sumFrameByteLengths(frames));
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
|
||||
data.set(currentFrame.data, dataOffset);
|
||||
dataOffset += currentFrame.data.byteLength;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prefixWithSilence: prefixWithSilence,
|
||||
trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts,
|
||||
generateSampleTable: generateSampleTable,
|
||||
concatenateFrameData: concatenateFrameData
|
||||
};
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
var coneOfSilence = require('../data/silence');
|
||||
var clock = require('../utils/clock');
|
||||
|
||||
/**
|
||||
* Sum the `byteLength` properties of the data in each AAC frame
|
||||
*/
|
||||
var sumFrameByteLengths = function(array) {
|
||||
var
|
||||
i,
|
||||
currentObj,
|
||||
sum = 0;
|
||||
|
||||
// sum the byteLength's all each nal unit in the frame
|
||||
for (i = 0; i < array.length; i++) {
|
||||
currentObj = array[i];
|
||||
sum += currentObj.data.byteLength;
|
||||
}
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
||||
// Possibly pad (prefix) the audio track with silence if appending this track
|
||||
// would lead to the introduction of a gap in the audio buffer
|
||||
var prefixWithSilence = function(
|
||||
track,
|
||||
frames,
|
||||
audioAppendStartTs,
|
||||
videoBaseMediaDecodeTime
|
||||
) {
|
||||
var
|
||||
baseMediaDecodeTimeTs,
|
||||
frameDuration = 0,
|
||||
audioGapDuration = 0,
|
||||
audioFillFrameCount = 0,
|
||||
audioFillDuration = 0,
|
||||
silentFrame,
|
||||
i,
|
||||
firstFrame;
|
||||
|
||||
if (!frames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseMediaDecodeTimeTs =
|
||||
clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate);
|
||||
// determine frame clock duration based on sample rate, round up to avoid overfills
|
||||
frameDuration = Math.ceil(clock.ONE_SECOND_IN_TS / (track.samplerate / 1024));
|
||||
|
||||
if (audioAppendStartTs && videoBaseMediaDecodeTime) {
|
||||
// insert the shortest possible amount (audio gap or audio to video gap)
|
||||
audioGapDuration =
|
||||
baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime);
|
||||
// number of full frames in the audio gap
|
||||
audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
|
||||
audioFillDuration = audioFillFrameCount * frameDuration;
|
||||
}
|
||||
|
||||
// don't attempt to fill gaps smaller than a single frame or larger
|
||||
// than a half second
|
||||
if (audioFillFrameCount < 1 || audioFillDuration > clock.ONE_SECOND_IN_TS / 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
silentFrame = coneOfSilence()[track.samplerate];
|
||||
|
||||
if (!silentFrame) {
|
||||
// we don't have a silent frame pregenerated for the sample rate, so use a frame
|
||||
// from the content instead
|
||||
silentFrame = frames[0].data;
|
||||
}
|
||||
|
||||
for (i = 0; i < audioFillFrameCount; i++) {
|
||||
firstFrame = frames[0];
|
||||
|
||||
frames.splice(0, 0, {
|
||||
data: silentFrame,
|
||||
dts: firstFrame.dts - frameDuration,
|
||||
pts: firstFrame.pts - frameDuration
|
||||
});
|
||||
}
|
||||
|
||||
track.baseMediaDecodeTime -=
|
||||
Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate));
|
||||
|
||||
return audioFillDuration;
|
||||
};
|
||||
|
||||
// If the audio segment extends before the earliest allowed dts
|
||||
// value, remove AAC frames until starts at or after the earliest
|
||||
// allowed DTS so that we don't end up with a negative baseMedia-
|
||||
// DecodeTime for the audio track
|
||||
var trimAdtsFramesByEarliestDts = function(adtsFrames, track, earliestAllowedDts) {
|
||||
if (track.minSegmentDts >= earliestAllowedDts) {
|
||||
return adtsFrames;
|
||||
}
|
||||
|
||||
// We will need to recalculate the earliest segment Dts
|
||||
track.minSegmentDts = Infinity;
|
||||
|
||||
return adtsFrames.filter(function(currentFrame) {
|
||||
// If this is an allowed frame, keep it and record it's Dts
|
||||
if (currentFrame.dts >= earliestAllowedDts) {
|
||||
track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
|
||||
track.minSegmentPts = track.minSegmentDts;
|
||||
return true;
|
||||
}
|
||||
// Otherwise, discard it
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from an array of frames
|
||||
var generateSampleTable = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
samples = [];
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
samples.push({
|
||||
size: currentFrame.data.byteLength,
|
||||
duration: 1024 // For AAC audio, all samples contain 1024 samples
|
||||
});
|
||||
}
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's sample table from an array of frames
|
||||
var concatenateFrameData = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
dataOffset = 0,
|
||||
data = new Uint8Array(sumFrameByteLengths(frames));
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
|
||||
data.set(currentFrame.data, dataOffset);
|
||||
dataOffset += currentFrame.data.byteLength;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prefixWithSilence: prefixWithSilence,
|
||||
trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts,
|
||||
generateSampleTable: generateSampleTable,
|
||||
concatenateFrameData: concatenateFrameData
|
||||
};
|
||||
|
|
956
node_modules/mux.js/lib/mp4/caption-parser.js
generated
vendored
956
node_modules/mux.js/lib/mp4/caption-parser.js
generated
vendored
|
@ -1,478 +1,478 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*
|
||||
* Reads in-band CEA-708 captions out of FMP4 segments.
|
||||
* @see https://en.wikipedia.org/wiki/CEA-708
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var discardEmulationPreventionBytes = require('../tools/caption-packet-parser').discardEmulationPreventionBytes;
|
||||
var CaptionStream = require('../m2ts/caption-stream').CaptionStream;
|
||||
var findBox = require('../mp4/find-box.js');
|
||||
var parseTfdt = require('../tools/parse-tfdt.js');
|
||||
var parseTrun = require('../tools/parse-trun.js');
|
||||
var parseTfhd = require('../tools/parse-tfhd.js');
|
||||
|
||||
/**
|
||||
* Maps an offset in the mdat to a sample based on the the size of the samples.
|
||||
* Assumes that `parseSamples` has been called first.
|
||||
*
|
||||
* @param {Number} offset - The offset into the mdat
|
||||
* @param {Object[]} samples - An array of samples, parsed using `parseSamples`
|
||||
* @return {?Object} The matching sample, or null if no match was found.
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.8.8
|
||||
**/
|
||||
var mapToSample = function(offset, samples) {
|
||||
var approximateOffset = offset;
|
||||
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
var sample = samples[i];
|
||||
|
||||
if (approximateOffset < sample.size) {
|
||||
return sample;
|
||||
}
|
||||
|
||||
approximateOffset -= sample.size;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds SEI nal units contained in a Media Data Box.
|
||||
* Assumes that `parseSamples` has been called first.
|
||||
*
|
||||
* @param {Uint8Array} avcStream - The bytes of the mdat
|
||||
* @param {Object[]} samples - The samples parsed out by `parseSamples`
|
||||
* @param {Number} trackId - The trackId of this video track
|
||||
* @return {Object[]} seiNals - the parsed SEI NALUs found.
|
||||
* The contents of the seiNal should match what is expected by
|
||||
* CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.1.1
|
||||
* @see Rec. ITU-T H.264, 7.3.2.3.1
|
||||
**/
|
||||
var findSeiNals = function(avcStream, samples, trackId) {
|
||||
var
|
||||
avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
|
||||
result = {
|
||||
logs: [],
|
||||
seiNals: []
|
||||
},
|
||||
seiNal,
|
||||
i,
|
||||
length,
|
||||
lastMatchedSample;
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (avcStream[i] & 0x1F) {
|
||||
case 0x06:
|
||||
var data = avcStream.subarray(i + 1, i + 1 + length);
|
||||
var matchingSample = mapToSample(i, samples);
|
||||
|
||||
seiNal = {
|
||||
nalUnitType: 'sei_rbsp',
|
||||
size: length,
|
||||
data: data,
|
||||
escapedRBSP: discardEmulationPreventionBytes(data),
|
||||
trackId: trackId
|
||||
};
|
||||
|
||||
if (matchingSample) {
|
||||
seiNal.pts = matchingSample.pts;
|
||||
seiNal.dts = matchingSample.dts;
|
||||
lastMatchedSample = matchingSample;
|
||||
} else if (lastMatchedSample) {
|
||||
// If a matching sample cannot be found, use the last
|
||||
// sample's values as they should be as close as possible
|
||||
seiNal.pts = lastMatchedSample.pts;
|
||||
seiNal.dts = lastMatchedSample.dts;
|
||||
} else {
|
||||
result.logs.push({
|
||||
level: 'warn',
|
||||
message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.'
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
result.seiNals.push(seiNal);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses sample information out of Track Run Boxes and calculates
|
||||
* the absolute presentation and decode timestamps of each sample.
|
||||
*
|
||||
* @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
|
||||
* @param {Number} baseMediaDecodeTime - base media decode time from tfdt
|
||||
@see ISO-BMFF-12/2015, Section 8.8.12
|
||||
* @param {Object} tfhd - The parsed Track Fragment Header
|
||||
* @see inspect.parseTfhd
|
||||
* @return {Object[]} the parsed samples
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.8.8
|
||||
**/
|
||||
var parseSamples = function(truns, baseMediaDecodeTime, tfhd) {
|
||||
var currentDts = baseMediaDecodeTime;
|
||||
var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
|
||||
var defaultSampleSize = tfhd.defaultSampleSize || 0;
|
||||
var trackId = tfhd.trackId;
|
||||
var allSamples = [];
|
||||
|
||||
truns.forEach(function(trun) {
|
||||
// Note: We currently do not parse the sample table as well
|
||||
// as the trun. It's possible some sources will require this.
|
||||
// moov > trak > mdia > minf > stbl
|
||||
var trackRun = parseTrun(trun);
|
||||
var samples = trackRun.samples;
|
||||
|
||||
samples.forEach(function(sample) {
|
||||
if (sample.duration === undefined) {
|
||||
sample.duration = defaultSampleDuration;
|
||||
}
|
||||
if (sample.size === undefined) {
|
||||
sample.size = defaultSampleSize;
|
||||
}
|
||||
sample.trackId = trackId;
|
||||
sample.dts = currentDts;
|
||||
if (sample.compositionTimeOffset === undefined) {
|
||||
sample.compositionTimeOffset = 0;
|
||||
}
|
||||
sample.pts = currentDts + sample.compositionTimeOffset;
|
||||
|
||||
currentDts += sample.duration;
|
||||
});
|
||||
|
||||
allSamples = allSamples.concat(samples);
|
||||
});
|
||||
|
||||
return allSamples;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out caption nals from an FMP4 segment's video tracks.
|
||||
*
|
||||
* @param {Uint8Array} segment - The bytes of a single segment
|
||||
* @param {Number} videoTrackId - The trackId of a video track in the segment
|
||||
* @return {Object.<Number, Object[]>} A mapping of video trackId to
|
||||
* a list of seiNals found in that track
|
||||
**/
|
||||
var parseCaptionNals = function(segment, videoTrackId) {
|
||||
// To get the samples
|
||||
var trafs = findBox(segment, ['moof', 'traf']);
|
||||
// To get SEI NAL units
|
||||
var mdats = findBox(segment, ['mdat']);
|
||||
var captionNals = {};
|
||||
var mdatTrafPairs = [];
|
||||
|
||||
// Pair up each traf with a mdat as moofs and mdats are in pairs
|
||||
mdats.forEach(function(mdat, index) {
|
||||
var matchingTraf = trafs[index];
|
||||
mdatTrafPairs.push({
|
||||
mdat: mdat,
|
||||
traf: matchingTraf
|
||||
});
|
||||
});
|
||||
|
||||
mdatTrafPairs.forEach(function(pair) {
|
||||
var mdat = pair.mdat;
|
||||
var traf = pair.traf;
|
||||
var tfhd = findBox(traf, ['tfhd']);
|
||||
// Exactly 1 tfhd per traf
|
||||
var headerInfo = parseTfhd(tfhd[0]);
|
||||
var trackId = headerInfo.trackId;
|
||||
var tfdt = findBox(traf, ['tfdt']);
|
||||
// Either 0 or 1 tfdt per traf
|
||||
var baseMediaDecodeTime = (tfdt.length > 0) ? parseTfdt(tfdt[0]).baseMediaDecodeTime : 0;
|
||||
var truns = findBox(traf, ['trun']);
|
||||
var samples;
|
||||
var result;
|
||||
|
||||
// Only parse video data for the chosen video track
|
||||
if (videoTrackId === trackId && truns.length > 0) {
|
||||
samples = parseSamples(truns, baseMediaDecodeTime, headerInfo);
|
||||
|
||||
result = findSeiNals(mdat, samples, trackId);
|
||||
|
||||
if (!captionNals[trackId]) {
|
||||
captionNals[trackId] = {seiNals: [], logs: []};
|
||||
}
|
||||
|
||||
captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals);
|
||||
captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs);
|
||||
}
|
||||
});
|
||||
|
||||
return captionNals;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out inband captions from an MP4 container and returns
|
||||
* caption objects that can be used by WebVTT and the TextTrack API.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
|
||||
* Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
|
||||
*
|
||||
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
||||
* @param {Number} trackId - The id of the video track to parse
|
||||
* @param {Number} timescale - The timescale for the video track from the init segment
|
||||
*
|
||||
* @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
|
||||
* @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
|
||||
* @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
|
||||
* @return {String} parsedCaptions[].text - The visible content of the caption
|
||||
**/
|
||||
var parseEmbeddedCaptions = function(segment, trackId, timescale) {
|
||||
var captionNals;
|
||||
|
||||
// the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
||||
if (trackId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
captionNals = parseCaptionNals(segment, trackId);
|
||||
|
||||
var trackNals = captionNals[trackId] || {};
|
||||
|
||||
return {
|
||||
seiNals: trackNals.seiNals,
|
||||
logs: trackNals.logs,
|
||||
timescale: timescale
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts SEI NALUs into captions that can be used by video.js
|
||||
**/
|
||||
var CaptionParser = function() {
|
||||
var isInitialized = false;
|
||||
var captionStream;
|
||||
|
||||
// Stores segments seen before trackId and timescale are set
|
||||
var segmentCache;
|
||||
// Stores video track ID of the track being parsed
|
||||
var trackId;
|
||||
// Stores the timescale of the track being parsed
|
||||
var timescale;
|
||||
// Stores captions parsed so far
|
||||
var parsedCaptions;
|
||||
// Stores whether we are receiving partial data or not
|
||||
var parsingPartial;
|
||||
|
||||
/**
|
||||
* A method to indicate whether a CaptionParser has been initalized
|
||||
* @returns {Boolean}
|
||||
**/
|
||||
this.isInitialized = function() {
|
||||
return isInitialized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the underlying CaptionStream, SEI NAL parsing
|
||||
* and management, and caption collection
|
||||
**/
|
||||
this.init = function(options) {
|
||||
captionStream = new CaptionStream();
|
||||
isInitialized = true;
|
||||
parsingPartial = options ? options.isPartial : false;
|
||||
|
||||
// Collect dispatched captions
|
||||
captionStream.on('data', function(event) {
|
||||
// Convert to seconds in the source's timescale
|
||||
event.startTime = event.startPts / timescale;
|
||||
event.endTime = event.endPts / timescale;
|
||||
|
||||
parsedCaptions.captions.push(event);
|
||||
parsedCaptions.captionStreams[event.stream] = true;
|
||||
});
|
||||
|
||||
captionStream.on('log', function(log) {
|
||||
parsedCaptions.logs.push(log);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a new video track will be selected
|
||||
* or if the timescale changed
|
||||
* @return {Boolean}
|
||||
**/
|
||||
this.isNewInit = function(videoTrackIds, timescales) {
|
||||
if ((videoTrackIds && videoTrackIds.length === 0) ||
|
||||
(timescales && typeof timescales === 'object' &&
|
||||
Object.keys(timescales).length === 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trackId !== videoTrackIds[0] ||
|
||||
timescale !== timescales[trackId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out SEI captions and interacts with underlying
|
||||
* CaptionStream to return dispatched captions
|
||||
*
|
||||
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
||||
* @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
|
||||
* @param {Object.<Number, Number>} timescales - The timescales found in the init segment
|
||||
* @see parseEmbeddedCaptions
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.parse = function(segment, videoTrackIds, timescales) {
|
||||
var parsedData;
|
||||
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
|
||||
// This is not likely to be a video segment
|
||||
} else if (!videoTrackIds || !timescales) {
|
||||
return null;
|
||||
|
||||
} else if (this.isNewInit(videoTrackIds, timescales)) {
|
||||
// Use the first video track only as there is no
|
||||
// mechanism to switch to other video tracks
|
||||
trackId = videoTrackIds[0];
|
||||
timescale = timescales[trackId];
|
||||
|
||||
// If an init segment has not been seen yet, hold onto segment
|
||||
// data until we have one.
|
||||
// the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
||||
} else if (trackId === null || !timescale) {
|
||||
segmentCache.push(segment);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now that a timescale and trackId is set, parse cached segments
|
||||
while (segmentCache.length > 0) {
|
||||
var cachedSegment = segmentCache.shift();
|
||||
|
||||
this.parse(cachedSegment, videoTrackIds, timescales);
|
||||
}
|
||||
|
||||
parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
|
||||
|
||||
if (parsedData && parsedData.logs) {
|
||||
parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs);
|
||||
}
|
||||
|
||||
if (parsedData === null || !parsedData.seiNals) {
|
||||
if (parsedCaptions.logs.length) {
|
||||
return {logs: parsedCaptions.logs, captions: [], captionStreams: []};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.pushNals(parsedData.seiNals);
|
||||
// Force the parsed captions to be dispatched
|
||||
this.flushStream();
|
||||
|
||||
return parsedCaptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes SEI NALUs onto CaptionStream
|
||||
* @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
|
||||
* Assumes that `parseCaptionNals` has been called first
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.pushNals = function(nals) {
|
||||
if (!this.isInitialized() || !nals || nals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nals.forEach(function(nal) {
|
||||
captionStream.push(nal);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Flushes underlying CaptionStream to dispatch processed, displayable captions
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.flushStream = function() {
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsingPartial) {
|
||||
captionStream.flush();
|
||||
} else {
|
||||
captionStream.partialFlush();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset caption buckets for new data
|
||||
**/
|
||||
this.clearParsedCaptions = function() {
|
||||
parsedCaptions.captions = [];
|
||||
parsedCaptions.captionStreams = {};
|
||||
parsedCaptions.logs = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets underlying CaptionStream
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.resetCaptionStream = function() {
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
captionStream.reset();
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience method to clear all captions flushed from the
|
||||
* CaptionStream and still being parsed
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.clearAllCaptions = function() {
|
||||
this.clearParsedCaptions();
|
||||
this.resetCaptionStream();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset caption parser
|
||||
**/
|
||||
this.reset = function() {
|
||||
segmentCache = [];
|
||||
trackId = null;
|
||||
timescale = null;
|
||||
|
||||
if (!parsedCaptions) {
|
||||
parsedCaptions = {
|
||||
captions: [],
|
||||
// CC1, CC2, CC3, CC4
|
||||
captionStreams: {},
|
||||
logs: []
|
||||
};
|
||||
} else {
|
||||
this.clearParsedCaptions();
|
||||
}
|
||||
|
||||
this.resetCaptionStream();
|
||||
};
|
||||
|
||||
this.reset();
|
||||
};
|
||||
|
||||
module.exports = CaptionParser;
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*
|
||||
* Reads in-band CEA-708 captions out of FMP4 segments.
|
||||
* @see https://en.wikipedia.org/wiki/CEA-708
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var discardEmulationPreventionBytes = require('../tools/caption-packet-parser').discardEmulationPreventionBytes;
|
||||
var CaptionStream = require('../m2ts/caption-stream').CaptionStream;
|
||||
var findBox = require('../mp4/find-box.js');
|
||||
var parseTfdt = require('../tools/parse-tfdt.js');
|
||||
var parseTrun = require('../tools/parse-trun.js');
|
||||
var parseTfhd = require('../tools/parse-tfhd.js');
|
||||
|
||||
/**
|
||||
* Maps an offset in the mdat to a sample based on the the size of the samples.
|
||||
* Assumes that `parseSamples` has been called first.
|
||||
*
|
||||
* @param {Number} offset - The offset into the mdat
|
||||
* @param {Object[]} samples - An array of samples, parsed using `parseSamples`
|
||||
* @return {?Object} The matching sample, or null if no match was found.
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.8.8
|
||||
**/
|
||||
var mapToSample = function(offset, samples) {
|
||||
var approximateOffset = offset;
|
||||
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
var sample = samples[i];
|
||||
|
||||
if (approximateOffset < sample.size) {
|
||||
return sample;
|
||||
}
|
||||
|
||||
approximateOffset -= sample.size;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds SEI nal units contained in a Media Data Box.
|
||||
* Assumes that `parseSamples` has been called first.
|
||||
*
|
||||
* @param {Uint8Array} avcStream - The bytes of the mdat
|
||||
* @param {Object[]} samples - The samples parsed out by `parseSamples`
|
||||
* @param {Number} trackId - The trackId of this video track
|
||||
* @return {Object[]} seiNals - the parsed SEI NALUs found.
|
||||
* The contents of the seiNal should match what is expected by
|
||||
* CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.1.1
|
||||
* @see Rec. ITU-T H.264, 7.3.2.3.1
|
||||
**/
|
||||
var findSeiNals = function(avcStream, samples, trackId) {
|
||||
var
|
||||
avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
|
||||
result = {
|
||||
logs: [],
|
||||
seiNals: []
|
||||
},
|
||||
seiNal,
|
||||
i,
|
||||
length,
|
||||
lastMatchedSample;
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (avcStream[i] & 0x1F) {
|
||||
case 0x06:
|
||||
var data = avcStream.subarray(i + 1, i + 1 + length);
|
||||
var matchingSample = mapToSample(i, samples);
|
||||
|
||||
seiNal = {
|
||||
nalUnitType: 'sei_rbsp',
|
||||
size: length,
|
||||
data: data,
|
||||
escapedRBSP: discardEmulationPreventionBytes(data),
|
||||
trackId: trackId
|
||||
};
|
||||
|
||||
if (matchingSample) {
|
||||
seiNal.pts = matchingSample.pts;
|
||||
seiNal.dts = matchingSample.dts;
|
||||
lastMatchedSample = matchingSample;
|
||||
} else if (lastMatchedSample) {
|
||||
// If a matching sample cannot be found, use the last
|
||||
// sample's values as they should be as close as possible
|
||||
seiNal.pts = lastMatchedSample.pts;
|
||||
seiNal.dts = lastMatchedSample.dts;
|
||||
} else {
|
||||
result.logs.push({
|
||||
level: 'warn',
|
||||
message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.'
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
result.seiNals.push(seiNal);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses sample information out of Track Run Boxes and calculates
|
||||
* the absolute presentation and decode timestamps of each sample.
|
||||
*
|
||||
* @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
|
||||
* @param {Number} baseMediaDecodeTime - base media decode time from tfdt
|
||||
@see ISO-BMFF-12/2015, Section 8.8.12
|
||||
* @param {Object} tfhd - The parsed Track Fragment Header
|
||||
* @see inspect.parseTfhd
|
||||
* @return {Object[]} the parsed samples
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.8.8
|
||||
**/
|
||||
var parseSamples = function(truns, baseMediaDecodeTime, tfhd) {
|
||||
var currentDts = baseMediaDecodeTime;
|
||||
var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
|
||||
var defaultSampleSize = tfhd.defaultSampleSize || 0;
|
||||
var trackId = tfhd.trackId;
|
||||
var allSamples = [];
|
||||
|
||||
truns.forEach(function(trun) {
|
||||
// Note: We currently do not parse the sample table as well
|
||||
// as the trun. It's possible some sources will require this.
|
||||
// moov > trak > mdia > minf > stbl
|
||||
var trackRun = parseTrun(trun);
|
||||
var samples = trackRun.samples;
|
||||
|
||||
samples.forEach(function(sample) {
|
||||
if (sample.duration === undefined) {
|
||||
sample.duration = defaultSampleDuration;
|
||||
}
|
||||
if (sample.size === undefined) {
|
||||
sample.size = defaultSampleSize;
|
||||
}
|
||||
sample.trackId = trackId;
|
||||
sample.dts = currentDts;
|
||||
if (sample.compositionTimeOffset === undefined) {
|
||||
sample.compositionTimeOffset = 0;
|
||||
}
|
||||
sample.pts = currentDts + sample.compositionTimeOffset;
|
||||
|
||||
currentDts += sample.duration;
|
||||
});
|
||||
|
||||
allSamples = allSamples.concat(samples);
|
||||
});
|
||||
|
||||
return allSamples;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out caption nals from an FMP4 segment's video tracks.
|
||||
*
|
||||
* @param {Uint8Array} segment - The bytes of a single segment
|
||||
* @param {Number} videoTrackId - The trackId of a video track in the segment
|
||||
* @return {Object.<Number, Object[]>} A mapping of video trackId to
|
||||
* a list of seiNals found in that track
|
||||
**/
|
||||
var parseCaptionNals = function(segment, videoTrackId) {
|
||||
// To get the samples
|
||||
var trafs = findBox(segment, ['moof', 'traf']);
|
||||
// To get SEI NAL units
|
||||
var mdats = findBox(segment, ['mdat']);
|
||||
var captionNals = {};
|
||||
var mdatTrafPairs = [];
|
||||
|
||||
// Pair up each traf with a mdat as moofs and mdats are in pairs
|
||||
mdats.forEach(function(mdat, index) {
|
||||
var matchingTraf = trafs[index];
|
||||
mdatTrafPairs.push({
|
||||
mdat: mdat,
|
||||
traf: matchingTraf
|
||||
});
|
||||
});
|
||||
|
||||
mdatTrafPairs.forEach(function(pair) {
|
||||
var mdat = pair.mdat;
|
||||
var traf = pair.traf;
|
||||
var tfhd = findBox(traf, ['tfhd']);
|
||||
// Exactly 1 tfhd per traf
|
||||
var headerInfo = parseTfhd(tfhd[0]);
|
||||
var trackId = headerInfo.trackId;
|
||||
var tfdt = findBox(traf, ['tfdt']);
|
||||
// Either 0 or 1 tfdt per traf
|
||||
var baseMediaDecodeTime = (tfdt.length > 0) ? parseTfdt(tfdt[0]).baseMediaDecodeTime : 0;
|
||||
var truns = findBox(traf, ['trun']);
|
||||
var samples;
|
||||
var result;
|
||||
|
||||
// Only parse video data for the chosen video track
|
||||
if (videoTrackId === trackId && truns.length > 0) {
|
||||
samples = parseSamples(truns, baseMediaDecodeTime, headerInfo);
|
||||
|
||||
result = findSeiNals(mdat, samples, trackId);
|
||||
|
||||
if (!captionNals[trackId]) {
|
||||
captionNals[trackId] = {seiNals: [], logs: []};
|
||||
}
|
||||
|
||||
captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals);
|
||||
captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs);
|
||||
}
|
||||
});
|
||||
|
||||
return captionNals;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out inband captions from an MP4 container and returns
|
||||
* caption objects that can be used by WebVTT and the TextTrack API.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
|
||||
* Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
|
||||
*
|
||||
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
||||
* @param {Number} trackId - The id of the video track to parse
|
||||
* @param {Number} timescale - The timescale for the video track from the init segment
|
||||
*
|
||||
* @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
|
||||
* @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
|
||||
* @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
|
||||
* @return {String} parsedCaptions[].text - The visible content of the caption
|
||||
**/
|
||||
var parseEmbeddedCaptions = function(segment, trackId, timescale) {
|
||||
var captionNals;
|
||||
|
||||
// the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
||||
if (trackId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
captionNals = parseCaptionNals(segment, trackId);
|
||||
|
||||
var trackNals = captionNals[trackId] || {};
|
||||
|
||||
return {
|
||||
seiNals: trackNals.seiNals,
|
||||
logs: trackNals.logs,
|
||||
timescale: timescale
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts SEI NALUs into captions that can be used by video.js
|
||||
**/
|
||||
var CaptionParser = function() {
|
||||
var isInitialized = false;
|
||||
var captionStream;
|
||||
|
||||
// Stores segments seen before trackId and timescale are set
|
||||
var segmentCache;
|
||||
// Stores video track ID of the track being parsed
|
||||
var trackId;
|
||||
// Stores the timescale of the track being parsed
|
||||
var timescale;
|
||||
// Stores captions parsed so far
|
||||
var parsedCaptions;
|
||||
// Stores whether we are receiving partial data or not
|
||||
var parsingPartial;
|
||||
|
||||
/**
|
||||
* A method to indicate whether a CaptionParser has been initalized
|
||||
* @returns {Boolean}
|
||||
**/
|
||||
this.isInitialized = function() {
|
||||
return isInitialized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the underlying CaptionStream, SEI NAL parsing
|
||||
* and management, and caption collection
|
||||
**/
|
||||
this.init = function(options) {
|
||||
captionStream = new CaptionStream();
|
||||
isInitialized = true;
|
||||
parsingPartial = options ? options.isPartial : false;
|
||||
|
||||
// Collect dispatched captions
|
||||
captionStream.on('data', function(event) {
|
||||
// Convert to seconds in the source's timescale
|
||||
event.startTime = event.startPts / timescale;
|
||||
event.endTime = event.endPts / timescale;
|
||||
|
||||
parsedCaptions.captions.push(event);
|
||||
parsedCaptions.captionStreams[event.stream] = true;
|
||||
});
|
||||
|
||||
captionStream.on('log', function(log) {
|
||||
parsedCaptions.logs.push(log);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a new video track will be selected
|
||||
* or if the timescale changed
|
||||
* @return {Boolean}
|
||||
**/
|
||||
this.isNewInit = function(videoTrackIds, timescales) {
|
||||
if ((videoTrackIds && videoTrackIds.length === 0) ||
|
||||
(timescales && typeof timescales === 'object' &&
|
||||
Object.keys(timescales).length === 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trackId !== videoTrackIds[0] ||
|
||||
timescale !== timescales[trackId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses out SEI captions and interacts with underlying
|
||||
* CaptionStream to return dispatched captions
|
||||
*
|
||||
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
||||
* @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
|
||||
* @param {Object.<Number, Number>} timescales - The timescales found in the init segment
|
||||
* @see parseEmbeddedCaptions
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.parse = function(segment, videoTrackIds, timescales) {
|
||||
var parsedData;
|
||||
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
|
||||
// This is not likely to be a video segment
|
||||
} else if (!videoTrackIds || !timescales) {
|
||||
return null;
|
||||
|
||||
} else if (this.isNewInit(videoTrackIds, timescales)) {
|
||||
// Use the first video track only as there is no
|
||||
// mechanism to switch to other video tracks
|
||||
trackId = videoTrackIds[0];
|
||||
timescale = timescales[trackId];
|
||||
|
||||
// If an init segment has not been seen yet, hold onto segment
|
||||
// data until we have one.
|
||||
// the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
||||
} else if (trackId === null || !timescale) {
|
||||
segmentCache.push(segment);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now that a timescale and trackId is set, parse cached segments
|
||||
while (segmentCache.length > 0) {
|
||||
var cachedSegment = segmentCache.shift();
|
||||
|
||||
this.parse(cachedSegment, videoTrackIds, timescales);
|
||||
}
|
||||
|
||||
parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
|
||||
|
||||
if (parsedData && parsedData.logs) {
|
||||
parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs);
|
||||
}
|
||||
|
||||
if (parsedData === null || !parsedData.seiNals) {
|
||||
if (parsedCaptions.logs.length) {
|
||||
return {logs: parsedCaptions.logs, captions: [], captionStreams: []};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.pushNals(parsedData.seiNals);
|
||||
// Force the parsed captions to be dispatched
|
||||
this.flushStream();
|
||||
|
||||
return parsedCaptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes SEI NALUs onto CaptionStream
|
||||
* @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
|
||||
* Assumes that `parseCaptionNals` has been called first
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.pushNals = function(nals) {
|
||||
if (!this.isInitialized() || !nals || nals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nals.forEach(function(nal) {
|
||||
captionStream.push(nal);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Flushes underlying CaptionStream to dispatch processed, displayable captions
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.flushStream = function() {
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsingPartial) {
|
||||
captionStream.flush();
|
||||
} else {
|
||||
captionStream.partialFlush();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset caption buckets for new data
|
||||
**/
|
||||
this.clearParsedCaptions = function() {
|
||||
parsedCaptions.captions = [];
|
||||
parsedCaptions.captionStreams = {};
|
||||
parsedCaptions.logs = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets underlying CaptionStream
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.resetCaptionStream = function() {
|
||||
if (!this.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
captionStream.reset();
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience method to clear all captions flushed from the
|
||||
* CaptionStream and still being parsed
|
||||
* @see m2ts/caption-stream.js
|
||||
**/
|
||||
this.clearAllCaptions = function() {
|
||||
this.clearParsedCaptions();
|
||||
this.resetCaptionStream();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset caption parser
|
||||
**/
|
||||
this.reset = function() {
|
||||
segmentCache = [];
|
||||
trackId = null;
|
||||
timescale = null;
|
||||
|
||||
if (!parsedCaptions) {
|
||||
parsedCaptions = {
|
||||
captions: [],
|
||||
// CC1, CC2, CC3, CC4
|
||||
captionStreams: {},
|
||||
logs: []
|
||||
};
|
||||
} else {
|
||||
this.clearParsedCaptions();
|
||||
}
|
||||
|
||||
this.resetCaptionStream();
|
||||
};
|
||||
|
||||
this.reset();
|
||||
};
|
||||
|
||||
module.exports = CaptionParser;
|
||||
|
|
88
node_modules/mux.js/lib/mp4/find-box.js
generated
vendored
88
node_modules/mux.js/lib/mp4/find-box.js
generated
vendored
|
@ -1,44 +1,44 @@
|
|||
var toUnsigned = require('../utils/bin').toUnsigned;
|
||||
var parseType = require('./parse-type.js');
|
||||
|
||||
var findBox = function(data, path) {
|
||||
var results = [],
|
||||
i, size, type, end, subresults;
|
||||
|
||||
if (!path.length) {
|
||||
// short-circuit the search for empty paths
|
||||
return null;
|
||||
}
|
||||
|
||||
for (i = 0; i < data.byteLength;) {
|
||||
size = toUnsigned(data[i] << 24 |
|
||||
data[i + 1] << 16 |
|
||||
data[i + 2] << 8 |
|
||||
data[i + 3]);
|
||||
|
||||
type = parseType(data.subarray(i + 4, i + 8));
|
||||
|
||||
end = size > 1 ? i + size : data.byteLength;
|
||||
|
||||
if (type === path[0]) {
|
||||
if (path.length === 1) {
|
||||
// this is the end of the path and we've found the box we were
|
||||
// looking for
|
||||
results.push(data.subarray(i + 8, end));
|
||||
} else {
|
||||
// recursively search for the next box along the path
|
||||
subresults = findBox(data.subarray(i + 8, end), path.slice(1));
|
||||
if (subresults.length) {
|
||||
results = results.concat(subresults);
|
||||
}
|
||||
}
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
|
||||
// we've finished searching all of data
|
||||
return results;
|
||||
};
|
||||
|
||||
module.exports = findBox;
|
||||
|
||||
var toUnsigned = require('../utils/bin').toUnsigned;
|
||||
var parseType = require('./parse-type.js');
|
||||
|
||||
var findBox = function(data, path) {
|
||||
var results = [],
|
||||
i, size, type, end, subresults;
|
||||
|
||||
if (!path.length) {
|
||||
// short-circuit the search for empty paths
|
||||
return null;
|
||||
}
|
||||
|
||||
for (i = 0; i < data.byteLength;) {
|
||||
size = toUnsigned(data[i] << 24 |
|
||||
data[i + 1] << 16 |
|
||||
data[i + 2] << 8 |
|
||||
data[i + 3]);
|
||||
|
||||
type = parseType(data.subarray(i + 4, i + 8));
|
||||
|
||||
end = size > 1 ? i + size : data.byteLength;
|
||||
|
||||
if (type === path[0]) {
|
||||
if (path.length === 1) {
|
||||
// this is the end of the path and we've found the box we were
|
||||
// looking for
|
||||
results.push(data.subarray(i + 8, end));
|
||||
} else {
|
||||
// recursively search for the next box along the path
|
||||
subresults = findBox(data.subarray(i + 8, end), path.slice(1));
|
||||
if (subresults.length) {
|
||||
results = results.concat(subresults);
|
||||
}
|
||||
}
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
|
||||
// we've finished searching all of data
|
||||
return results;
|
||||
};
|
||||
|
||||
module.exports = findBox;
|
||||
|
||||
|
|
638
node_modules/mux.js/lib/mp4/frame-utils.js
generated
vendored
638
node_modules/mux.js/lib/mp4/frame-utils.js
generated
vendored
|
@ -1,319 +1,319 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
// Convert an array of nal units into an array of frames with each frame being
|
||||
// composed of the nal units that make up that frame
|
||||
// Also keep track of cummulative data about the frame from the nal units such
|
||||
// as the frame duration, starting pts, etc.
|
||||
var groupNalsIntoFrames = function(nalUnits) {
|
||||
var
|
||||
i,
|
||||
currentNal,
|
||||
currentFrame = [],
|
||||
frames = [];
|
||||
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength = 0;
|
||||
frames.nalCount = 0;
|
||||
frames.duration = 0;
|
||||
|
||||
currentFrame.byteLength = 0;
|
||||
|
||||
for (i = 0; i < nalUnits.length; i++) {
|
||||
currentNal = nalUnits[i];
|
||||
|
||||
// Split on 'aud'-type nal units
|
||||
if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
|
||||
// Since the very first nal unit is expected to be an AUD
|
||||
// only push to the frames array when currentFrame is not empty
|
||||
if (currentFrame.length) {
|
||||
currentFrame.duration = currentNal.dts - currentFrame.dts;
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength += currentFrame.byteLength;
|
||||
frames.nalCount += currentFrame.length;
|
||||
frames.duration += currentFrame.duration;
|
||||
frames.push(currentFrame);
|
||||
}
|
||||
currentFrame = [currentNal];
|
||||
currentFrame.byteLength = currentNal.data.byteLength;
|
||||
currentFrame.pts = currentNal.pts;
|
||||
currentFrame.dts = currentNal.dts;
|
||||
} else {
|
||||
// Specifically flag key frames for ease of use later
|
||||
if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
|
||||
currentFrame.keyFrame = true;
|
||||
}
|
||||
currentFrame.duration = currentNal.dts - currentFrame.dts;
|
||||
currentFrame.byteLength += currentNal.data.byteLength;
|
||||
currentFrame.push(currentNal);
|
||||
}
|
||||
}
|
||||
|
||||
// For the last frame, use the duration of the previous frame if we
|
||||
// have nothing better to go on
|
||||
if (frames.length &&
|
||||
(!currentFrame.duration ||
|
||||
currentFrame.duration <= 0)) {
|
||||
currentFrame.duration = frames[frames.length - 1].duration;
|
||||
}
|
||||
|
||||
// Push the final frame
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength += currentFrame.byteLength;
|
||||
frames.nalCount += currentFrame.length;
|
||||
frames.duration += currentFrame.duration;
|
||||
|
||||
frames.push(currentFrame);
|
||||
return frames;
|
||||
};
|
||||
|
||||
// Convert an array of frames into an array of Gop with each Gop being composed
|
||||
// of the frames that make up that Gop
|
||||
// Also keep track of cummulative data about the Gop from the frames such as the
|
||||
// Gop duration, starting pts, etc.
|
||||
var groupFramesIntoGops = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
currentGop = [],
|
||||
gops = [];
|
||||
|
||||
// We must pre-set some of the values on the Gop since we
|
||||
// keep running totals of these values
|
||||
currentGop.byteLength = 0;
|
||||
currentGop.nalCount = 0;
|
||||
currentGop.duration = 0;
|
||||
currentGop.pts = frames[0].pts;
|
||||
currentGop.dts = frames[0].dts;
|
||||
|
||||
// store some metadata about all the Gops
|
||||
gops.byteLength = 0;
|
||||
gops.nalCount = 0;
|
||||
gops.duration = 0;
|
||||
gops.pts = frames[0].pts;
|
||||
gops.dts = frames[0].dts;
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
|
||||
if (currentFrame.keyFrame) {
|
||||
// Since the very first frame is expected to be an keyframe
|
||||
// only push to the gops array when currentGop is not empty
|
||||
if (currentGop.length) {
|
||||
gops.push(currentGop);
|
||||
gops.byteLength += currentGop.byteLength;
|
||||
gops.nalCount += currentGop.nalCount;
|
||||
gops.duration += currentGop.duration;
|
||||
}
|
||||
|
||||
currentGop = [currentFrame];
|
||||
currentGop.nalCount = currentFrame.length;
|
||||
currentGop.byteLength = currentFrame.byteLength;
|
||||
currentGop.pts = currentFrame.pts;
|
||||
currentGop.dts = currentFrame.dts;
|
||||
currentGop.duration = currentFrame.duration;
|
||||
} else {
|
||||
currentGop.duration += currentFrame.duration;
|
||||
currentGop.nalCount += currentFrame.length;
|
||||
currentGop.byteLength += currentFrame.byteLength;
|
||||
currentGop.push(currentFrame);
|
||||
}
|
||||
}
|
||||
|
||||
if (gops.length && currentGop.duration <= 0) {
|
||||
currentGop.duration = gops[gops.length - 1].duration;
|
||||
}
|
||||
gops.byteLength += currentGop.byteLength;
|
||||
gops.nalCount += currentGop.nalCount;
|
||||
gops.duration += currentGop.duration;
|
||||
|
||||
// push the final Gop
|
||||
gops.push(currentGop);
|
||||
return gops;
|
||||
};
|
||||
|
||||
/*
|
||||
* Search for the first keyframe in the GOPs and throw away all frames
|
||||
* until that keyframe. Then extend the duration of the pulled keyframe
|
||||
* and pull the PTS and DTS of the keyframe so that it covers the time
|
||||
* range of the frames that were disposed.
|
||||
*
|
||||
* @param {Array} gops video GOPs
|
||||
* @returns {Array} modified video GOPs
|
||||
*/
|
||||
var extendFirstKeyFrame = function(gops) {
|
||||
var currentGop;
|
||||
|
||||
if (!gops[0][0].keyFrame && gops.length > 1) {
|
||||
// Remove the first GOP
|
||||
currentGop = gops.shift();
|
||||
|
||||
gops.byteLength -= currentGop.byteLength;
|
||||
gops.nalCount -= currentGop.nalCount;
|
||||
|
||||
// Extend the first frame of what is now the
|
||||
// first gop to cover the time period of the
|
||||
// frames we just removed
|
||||
gops[0][0].dts = currentGop.dts;
|
||||
gops[0][0].pts = currentGop.pts;
|
||||
gops[0][0].duration += currentGop.duration;
|
||||
}
|
||||
|
||||
return gops;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default sample object
|
||||
* see ISO/IEC 14496-12:2012, section 8.6.4.3
|
||||
*/
|
||||
var createDefaultSample = function() {
|
||||
return {
|
||||
size: 0,
|
||||
flags: {
|
||||
isLeading: 0,
|
||||
dependsOn: 1,
|
||||
isDependedOn: 0,
|
||||
hasRedundancy: 0,
|
||||
degradationPriority: 0,
|
||||
isNonSyncSample: 1
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Collates information from a video frame into an object for eventual
|
||||
* entry into an MP4 sample table.
|
||||
*
|
||||
* @param {Object} frame the video frame
|
||||
* @param {Number} dataOffset the byte offset to position the sample
|
||||
* @return {Object} object containing sample table info for a frame
|
||||
*/
|
||||
var sampleForFrame = function(frame, dataOffset) {
|
||||
var sample = createDefaultSample();
|
||||
|
||||
sample.dataOffset = dataOffset;
|
||||
sample.compositionTimeOffset = frame.pts - frame.dts;
|
||||
sample.duration = frame.duration;
|
||||
sample.size = 4 * frame.length; // Space for nal unit size
|
||||
sample.size += frame.byteLength;
|
||||
|
||||
if (frame.keyFrame) {
|
||||
sample.flags.dependsOn = 2;
|
||||
sample.flags.isNonSyncSample = 0;
|
||||
}
|
||||
|
||||
return sample;
|
||||
};
|
||||
|
||||
// generate the track's sample table from an array of gops
|
||||
var generateSampleTable = function(gops, baseDataOffset) {
|
||||
var
|
||||
h, i,
|
||||
sample,
|
||||
currentGop,
|
||||
currentFrame,
|
||||
dataOffset = baseDataOffset || 0,
|
||||
samples = [];
|
||||
|
||||
for (h = 0; h < gops.length; h++) {
|
||||
currentGop = gops[h];
|
||||
|
||||
for (i = 0; i < currentGop.length; i++) {
|
||||
currentFrame = currentGop[i];
|
||||
|
||||
sample = sampleForFrame(currentFrame, dataOffset);
|
||||
|
||||
dataOffset += sample.size;
|
||||
|
||||
samples.push(sample);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from an array of gops
|
||||
var concatenateNalData = function(gops) {
|
||||
var
|
||||
h, i, j,
|
||||
currentGop,
|
||||
currentFrame,
|
||||
currentNal,
|
||||
dataOffset = 0,
|
||||
nalsByteLength = gops.byteLength,
|
||||
numberOfNals = gops.nalCount,
|
||||
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
||||
data = new Uint8Array(totalByteLength),
|
||||
view = new DataView(data.buffer);
|
||||
|
||||
// For each Gop..
|
||||
for (h = 0; h < gops.length; h++) {
|
||||
currentGop = gops[h];
|
||||
|
||||
// For each Frame..
|
||||
for (i = 0; i < currentGop.length; i++) {
|
||||
currentFrame = currentGop[i];
|
||||
|
||||
// For each NAL..
|
||||
for (j = 0; j < currentFrame.length; j++) {
|
||||
currentNal = currentFrame[j];
|
||||
|
||||
view.setUint32(dataOffset, currentNal.data.byteLength);
|
||||
dataOffset += 4;
|
||||
data.set(currentNal.data, dataOffset);
|
||||
dataOffset += currentNal.data.byteLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// generate the track's sample table from a frame
|
||||
var generateSampleTableForFrame = function(frame, baseDataOffset) {
|
||||
var
|
||||
sample,
|
||||
dataOffset = baseDataOffset || 0,
|
||||
samples = [];
|
||||
|
||||
sample = sampleForFrame(frame, dataOffset);
|
||||
samples.push(sample);
|
||||
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from a frame
|
||||
var concatenateNalDataForFrame = function(frame) {
|
||||
var
|
||||
i,
|
||||
currentNal,
|
||||
dataOffset = 0,
|
||||
nalsByteLength = frame.byteLength,
|
||||
numberOfNals = frame.length,
|
||||
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
||||
data = new Uint8Array(totalByteLength),
|
||||
view = new DataView(data.buffer);
|
||||
|
||||
// For each NAL..
|
||||
for (i = 0; i < frame.length; i++) {
|
||||
currentNal = frame[i];
|
||||
|
||||
view.setUint32(dataOffset, currentNal.data.byteLength);
|
||||
dataOffset += 4;
|
||||
data.set(currentNal.data, dataOffset);
|
||||
dataOffset += currentNal.data.byteLength;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
groupNalsIntoFrames: groupNalsIntoFrames,
|
||||
groupFramesIntoGops: groupFramesIntoGops,
|
||||
extendFirstKeyFrame: extendFirstKeyFrame,
|
||||
generateSampleTable: generateSampleTable,
|
||||
concatenateNalData: concatenateNalData,
|
||||
generateSampleTableForFrame: generateSampleTableForFrame,
|
||||
concatenateNalDataForFrame: concatenateNalDataForFrame
|
||||
};
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
// Convert an array of nal units into an array of frames with each frame being
|
||||
// composed of the nal units that make up that frame
|
||||
// Also keep track of cummulative data about the frame from the nal units such
|
||||
// as the frame duration, starting pts, etc.
|
||||
var groupNalsIntoFrames = function(nalUnits) {
|
||||
var
|
||||
i,
|
||||
currentNal,
|
||||
currentFrame = [],
|
||||
frames = [];
|
||||
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength = 0;
|
||||
frames.nalCount = 0;
|
||||
frames.duration = 0;
|
||||
|
||||
currentFrame.byteLength = 0;
|
||||
|
||||
for (i = 0; i < nalUnits.length; i++) {
|
||||
currentNal = nalUnits[i];
|
||||
|
||||
// Split on 'aud'-type nal units
|
||||
if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
|
||||
// Since the very first nal unit is expected to be an AUD
|
||||
// only push to the frames array when currentFrame is not empty
|
||||
if (currentFrame.length) {
|
||||
currentFrame.duration = currentNal.dts - currentFrame.dts;
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength += currentFrame.byteLength;
|
||||
frames.nalCount += currentFrame.length;
|
||||
frames.duration += currentFrame.duration;
|
||||
frames.push(currentFrame);
|
||||
}
|
||||
currentFrame = [currentNal];
|
||||
currentFrame.byteLength = currentNal.data.byteLength;
|
||||
currentFrame.pts = currentNal.pts;
|
||||
currentFrame.dts = currentNal.dts;
|
||||
} else {
|
||||
// Specifically flag key frames for ease of use later
|
||||
if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
|
||||
currentFrame.keyFrame = true;
|
||||
}
|
||||
currentFrame.duration = currentNal.dts - currentFrame.dts;
|
||||
currentFrame.byteLength += currentNal.data.byteLength;
|
||||
currentFrame.push(currentNal);
|
||||
}
|
||||
}
|
||||
|
||||
// For the last frame, use the duration of the previous frame if we
|
||||
// have nothing better to go on
|
||||
if (frames.length &&
|
||||
(!currentFrame.duration ||
|
||||
currentFrame.duration <= 0)) {
|
||||
currentFrame.duration = frames[frames.length - 1].duration;
|
||||
}
|
||||
|
||||
// Push the final frame
|
||||
// TODO added for LHLS, make sure this is OK
|
||||
frames.byteLength += currentFrame.byteLength;
|
||||
frames.nalCount += currentFrame.length;
|
||||
frames.duration += currentFrame.duration;
|
||||
|
||||
frames.push(currentFrame);
|
||||
return frames;
|
||||
};
|
||||
|
||||
// Convert an array of frames into an array of Gop with each Gop being composed
|
||||
// of the frames that make up that Gop
|
||||
// Also keep track of cummulative data about the Gop from the frames such as the
|
||||
// Gop duration, starting pts, etc.
|
||||
var groupFramesIntoGops = function(frames) {
|
||||
var
|
||||
i,
|
||||
currentFrame,
|
||||
currentGop = [],
|
||||
gops = [];
|
||||
|
||||
// We must pre-set some of the values on the Gop since we
|
||||
// keep running totals of these values
|
||||
currentGop.byteLength = 0;
|
||||
currentGop.nalCount = 0;
|
||||
currentGop.duration = 0;
|
||||
currentGop.pts = frames[0].pts;
|
||||
currentGop.dts = frames[0].dts;
|
||||
|
||||
// store some metadata about all the Gops
|
||||
gops.byteLength = 0;
|
||||
gops.nalCount = 0;
|
||||
gops.duration = 0;
|
||||
gops.pts = frames[0].pts;
|
||||
gops.dts = frames[0].dts;
|
||||
|
||||
for (i = 0; i < frames.length; i++) {
|
||||
currentFrame = frames[i];
|
||||
|
||||
if (currentFrame.keyFrame) {
|
||||
// Since the very first frame is expected to be an keyframe
|
||||
// only push to the gops array when currentGop is not empty
|
||||
if (currentGop.length) {
|
||||
gops.push(currentGop);
|
||||
gops.byteLength += currentGop.byteLength;
|
||||
gops.nalCount += currentGop.nalCount;
|
||||
gops.duration += currentGop.duration;
|
||||
}
|
||||
|
||||
currentGop = [currentFrame];
|
||||
currentGop.nalCount = currentFrame.length;
|
||||
currentGop.byteLength = currentFrame.byteLength;
|
||||
currentGop.pts = currentFrame.pts;
|
||||
currentGop.dts = currentFrame.dts;
|
||||
currentGop.duration = currentFrame.duration;
|
||||
} else {
|
||||
currentGop.duration += currentFrame.duration;
|
||||
currentGop.nalCount += currentFrame.length;
|
||||
currentGop.byteLength += currentFrame.byteLength;
|
||||
currentGop.push(currentFrame);
|
||||
}
|
||||
}
|
||||
|
||||
if (gops.length && currentGop.duration <= 0) {
|
||||
currentGop.duration = gops[gops.length - 1].duration;
|
||||
}
|
||||
gops.byteLength += currentGop.byteLength;
|
||||
gops.nalCount += currentGop.nalCount;
|
||||
gops.duration += currentGop.duration;
|
||||
|
||||
// push the final Gop
|
||||
gops.push(currentGop);
|
||||
return gops;
|
||||
};
|
||||
|
||||
/*
|
||||
* Search for the first keyframe in the GOPs and throw away all frames
|
||||
* until that keyframe. Then extend the duration of the pulled keyframe
|
||||
* and pull the PTS and DTS of the keyframe so that it covers the time
|
||||
* range of the frames that were disposed.
|
||||
*
|
||||
* @param {Array} gops video GOPs
|
||||
* @returns {Array} modified video GOPs
|
||||
*/
|
||||
var extendFirstKeyFrame = function(gops) {
|
||||
var currentGop;
|
||||
|
||||
if (!gops[0][0].keyFrame && gops.length > 1) {
|
||||
// Remove the first GOP
|
||||
currentGop = gops.shift();
|
||||
|
||||
gops.byteLength -= currentGop.byteLength;
|
||||
gops.nalCount -= currentGop.nalCount;
|
||||
|
||||
// Extend the first frame of what is now the
|
||||
// first gop to cover the time period of the
|
||||
// frames we just removed
|
||||
gops[0][0].dts = currentGop.dts;
|
||||
gops[0][0].pts = currentGop.pts;
|
||||
gops[0][0].duration += currentGop.duration;
|
||||
}
|
||||
|
||||
return gops;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default sample object
|
||||
* see ISO/IEC 14496-12:2012, section 8.6.4.3
|
||||
*/
|
||||
var createDefaultSample = function() {
|
||||
return {
|
||||
size: 0,
|
||||
flags: {
|
||||
isLeading: 0,
|
||||
dependsOn: 1,
|
||||
isDependedOn: 0,
|
||||
hasRedundancy: 0,
|
||||
degradationPriority: 0,
|
||||
isNonSyncSample: 1
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Collates information from a video frame into an object for eventual
|
||||
* entry into an MP4 sample table.
|
||||
*
|
||||
* @param {Object} frame the video frame
|
||||
* @param {Number} dataOffset the byte offset to position the sample
|
||||
* @return {Object} object containing sample table info for a frame
|
||||
*/
|
||||
var sampleForFrame = function(frame, dataOffset) {
|
||||
var sample = createDefaultSample();
|
||||
|
||||
sample.dataOffset = dataOffset;
|
||||
sample.compositionTimeOffset = frame.pts - frame.dts;
|
||||
sample.duration = frame.duration;
|
||||
sample.size = 4 * frame.length; // Space for nal unit size
|
||||
sample.size += frame.byteLength;
|
||||
|
||||
if (frame.keyFrame) {
|
||||
sample.flags.dependsOn = 2;
|
||||
sample.flags.isNonSyncSample = 0;
|
||||
}
|
||||
|
||||
return sample;
|
||||
};
|
||||
|
||||
// generate the track's sample table from an array of gops
|
||||
var generateSampleTable = function(gops, baseDataOffset) {
|
||||
var
|
||||
h, i,
|
||||
sample,
|
||||
currentGop,
|
||||
currentFrame,
|
||||
dataOffset = baseDataOffset || 0,
|
||||
samples = [];
|
||||
|
||||
for (h = 0; h < gops.length; h++) {
|
||||
currentGop = gops[h];
|
||||
|
||||
for (i = 0; i < currentGop.length; i++) {
|
||||
currentFrame = currentGop[i];
|
||||
|
||||
sample = sampleForFrame(currentFrame, dataOffset);
|
||||
|
||||
dataOffset += sample.size;
|
||||
|
||||
samples.push(sample);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from an array of gops
|
||||
var concatenateNalData = function(gops) {
|
||||
var
|
||||
h, i, j,
|
||||
currentGop,
|
||||
currentFrame,
|
||||
currentNal,
|
||||
dataOffset = 0,
|
||||
nalsByteLength = gops.byteLength,
|
||||
numberOfNals = gops.nalCount,
|
||||
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
||||
data = new Uint8Array(totalByteLength),
|
||||
view = new DataView(data.buffer);
|
||||
|
||||
// For each Gop..
|
||||
for (h = 0; h < gops.length; h++) {
|
||||
currentGop = gops[h];
|
||||
|
||||
// For each Frame..
|
||||
for (i = 0; i < currentGop.length; i++) {
|
||||
currentFrame = currentGop[i];
|
||||
|
||||
// For each NAL..
|
||||
for (j = 0; j < currentFrame.length; j++) {
|
||||
currentNal = currentFrame[j];
|
||||
|
||||
view.setUint32(dataOffset, currentNal.data.byteLength);
|
||||
dataOffset += 4;
|
||||
data.set(currentNal.data, dataOffset);
|
||||
dataOffset += currentNal.data.byteLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// generate the track's sample table from a frame
|
||||
var generateSampleTableForFrame = function(frame, baseDataOffset) {
|
||||
var
|
||||
sample,
|
||||
dataOffset = baseDataOffset || 0,
|
||||
samples = [];
|
||||
|
||||
sample = sampleForFrame(frame, dataOffset);
|
||||
samples.push(sample);
|
||||
|
||||
return samples;
|
||||
};
|
||||
|
||||
// generate the track's raw mdat data from a frame
|
||||
var concatenateNalDataForFrame = function(frame) {
|
||||
var
|
||||
i,
|
||||
currentNal,
|
||||
dataOffset = 0,
|
||||
nalsByteLength = frame.byteLength,
|
||||
numberOfNals = frame.length,
|
||||
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
||||
data = new Uint8Array(totalByteLength),
|
||||
view = new DataView(data.buffer);
|
||||
|
||||
// For each NAL..
|
||||
for (i = 0; i < frame.length; i++) {
|
||||
currentNal = frame[i];
|
||||
|
||||
view.setUint32(dataOffset, currentNal.data.byteLength);
|
||||
dataOffset += 4;
|
||||
data.set(currentNal.data, dataOffset);
|
||||
dataOffset += currentNal.data.byteLength;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
groupNalsIntoFrames: groupNalsIntoFrames,
|
||||
groupFramesIntoGops: groupFramesIntoGops,
|
||||
extendFirstKeyFrame: extendFirstKeyFrame,
|
||||
generateSampleTable: generateSampleTable,
|
||||
concatenateNalData: concatenateNalData,
|
||||
generateSampleTableForFrame: generateSampleTableForFrame,
|
||||
concatenateNalDataForFrame: concatenateNalDataForFrame
|
||||
};
|
||||
|
|
28
node_modules/mux.js/lib/mp4/index.js
generated
vendored
28
node_modules/mux.js/lib/mp4/index.js
generated
vendored
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
module.exports = {
|
||||
generator: require('./mp4-generator'),
|
||||
probe: require('./probe'),
|
||||
Transmuxer: require('./transmuxer').Transmuxer,
|
||||
AudioSegmentStream: require('./transmuxer').AudioSegmentStream,
|
||||
VideoSegmentStream: require('./transmuxer').VideoSegmentStream,
|
||||
CaptionParser: require('./caption-parser')
|
||||
};
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
module.exports = {
|
||||
generator: require('./mp4-generator'),
|
||||
probe: require('./probe'),
|
||||
Transmuxer: require('./transmuxer').Transmuxer,
|
||||
AudioSegmentStream: require('./transmuxer').AudioSegmentStream,
|
||||
VideoSegmentStream: require('./transmuxer').VideoSegmentStream,
|
||||
CaptionParser: require('./caption-parser')
|
||||
};
|
||||
|
|
1598
node_modules/mux.js/lib/mp4/mp4-generator.js
generated
vendored
1598
node_modules/mux.js/lib/mp4/mp4-generator.js
generated
vendored
File diff suppressed because it is too large
Load diff
22
node_modules/mux.js/lib/mp4/parse-type.js
generated
vendored
22
node_modules/mux.js/lib/mp4/parse-type.js
generated
vendored
|
@ -1,11 +1,11 @@
|
|||
var parseType = function(buffer) {
|
||||
var result = '';
|
||||
result += String.fromCharCode(buffer[0]);
|
||||
result += String.fromCharCode(buffer[1]);
|
||||
result += String.fromCharCode(buffer[2]);
|
||||
result += String.fromCharCode(buffer[3]);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
module.exports = parseType;
|
||||
var parseType = function(buffer) {
|
||||
var result = '';
|
||||
result += String.fromCharCode(buffer[0]);
|
||||
result += String.fromCharCode(buffer[1]);
|
||||
result += String.fromCharCode(buffer[2]);
|
||||
result += String.fromCharCode(buffer[3]);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
module.exports = parseType;
|
||||
|
|
732
node_modules/mux.js/lib/mp4/probe.js
generated
vendored
732
node_modules/mux.js/lib/mp4/probe.js
generated
vendored
|
@ -1,366 +1,366 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*
|
||||
* Utilities to detect basic properties and metadata about MP4s.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var toUnsigned = require('../utils/bin').toUnsigned;
|
||||
var toHexString = require('../utils/bin').toHexString;
|
||||
var findBox = require('../mp4/find-box.js');
|
||||
var parseType = require('../mp4/parse-type.js');
|
||||
var parseTfhd = require('../tools/parse-tfhd.js');
|
||||
var parseTrun = require('../tools/parse-trun.js');
|
||||
var parseTfdt = require('../tools/parse-tfdt.js');
|
||||
var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks,
|
||||
getTimescaleFromMediaHeader;
|
||||
|
||||
/**
|
||||
* Parses an MP4 initialization segment and extracts the timescale
|
||||
* values for any declared tracks. Timescale values indicate the
|
||||
* number of clock ticks per second to assume for time-based values
|
||||
* elsewhere in the MP4.
|
||||
*
|
||||
* To determine the start time of an MP4, you need two pieces of
|
||||
* information: the timescale unit and the earliest base media decode
|
||||
* time. Multiple timescales can be specified within an MP4 but the
|
||||
* base media decode time is always expressed in the timescale from
|
||||
* the media header box for the track:
|
||||
* ```
|
||||
* moov > trak > mdia > mdhd.timescale
|
||||
* ```
|
||||
* @param init {Uint8Array} the bytes of the init segment
|
||||
* @return {object} a hash of track ids to timescale values or null if
|
||||
* the init segment is malformed.
|
||||
*/
|
||||
timescale = function(init) {
|
||||
var
|
||||
result = {},
|
||||
traks = findBox(init, ['moov', 'trak']);
|
||||
|
||||
// mdhd timescale
|
||||
return traks.reduce(function(result, trak) {
|
||||
var tkhd, version, index, id, mdhd;
|
||||
|
||||
tkhd = findBox(trak, ['tkhd'])[0];
|
||||
if (!tkhd) {
|
||||
return null;
|
||||
}
|
||||
version = tkhd[0];
|
||||
index = version === 0 ? 12 : 20;
|
||||
id = toUnsigned(tkhd[index] << 24 |
|
||||
tkhd[index + 1] << 16 |
|
||||
tkhd[index + 2] << 8 |
|
||||
tkhd[index + 3]);
|
||||
|
||||
mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
||||
if (!mdhd) {
|
||||
return null;
|
||||
}
|
||||
version = mdhd[0];
|
||||
index = version === 0 ? 12 : 20;
|
||||
result[id] = toUnsigned(mdhd[index] << 24 |
|
||||
mdhd[index + 1] << 16 |
|
||||
mdhd[index + 2] << 8 |
|
||||
mdhd[index + 3]);
|
||||
return result;
|
||||
}, result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the base media decode start time, in seconds, for an MP4
|
||||
* fragment. If multiple fragments are specified, the earliest time is
|
||||
* returned.
|
||||
*
|
||||
* The base media decode time can be parsed from track fragment
|
||||
* metadata:
|
||||
* ```
|
||||
* moof > traf > tfdt.baseMediaDecodeTime
|
||||
* ```
|
||||
* It requires the timescale value from the mdhd to interpret.
|
||||
*
|
||||
* @param timescale {object} a hash of track ids to timescale values.
|
||||
* @return {number} the earliest base media decode start time for the
|
||||
* fragment, in seconds
|
||||
*/
|
||||
startTime = function(timescale, fragment) {
|
||||
var trafs, baseTimes, result;
|
||||
|
||||
// we need info from two childrend of each track fragment box
|
||||
trafs = findBox(fragment, ['moof', 'traf']);
|
||||
|
||||
// determine the start times for each track
|
||||
baseTimes = [].concat.apply([], trafs.map(function(traf) {
|
||||
return findBox(traf, ['tfhd']).map(function(tfhd) {
|
||||
var id, scale, baseTime;
|
||||
|
||||
// get the track id from the tfhd
|
||||
id = toUnsigned(tfhd[4] << 24 |
|
||||
tfhd[5] << 16 |
|
||||
tfhd[6] << 8 |
|
||||
tfhd[7]);
|
||||
// assume a 90kHz clock if no timescale was specified
|
||||
scale = timescale[id] || 90e3;
|
||||
|
||||
// get the base media decode time from the tfdt
|
||||
baseTime = findBox(traf, ['tfdt']).map(function(tfdt) {
|
||||
var version, result;
|
||||
|
||||
version = tfdt[0];
|
||||
result = toUnsigned(tfdt[4] << 24 |
|
||||
tfdt[5] << 16 |
|
||||
tfdt[6] << 8 |
|
||||
tfdt[7]);
|
||||
if (version === 1) {
|
||||
result *= Math.pow(2, 32);
|
||||
result += toUnsigned(tfdt[8] << 24 |
|
||||
tfdt[9] << 16 |
|
||||
tfdt[10] << 8 |
|
||||
tfdt[11]);
|
||||
}
|
||||
return result;
|
||||
})[0];
|
||||
baseTime = typeof baseTime === 'number' && !isNaN(baseTime) ? baseTime : Infinity;
|
||||
|
||||
// convert base time to seconds
|
||||
return baseTime / scale;
|
||||
});
|
||||
}));
|
||||
|
||||
// return the minimum
|
||||
result = Math.min.apply(null, baseTimes);
|
||||
return isFinite(result) ? result : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the composition start, in seconds, for an MP4
|
||||
* fragment.
|
||||
*
|
||||
* The composition start time of a fragment can be calculated using the base
|
||||
* media decode time, composition time offset, and timescale, as follows:
|
||||
*
|
||||
* compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale
|
||||
*
|
||||
* All of the aforementioned information is contained within a media fragment's
|
||||
* `traf` box, except for timescale info, which comes from the initialization
|
||||
* segment, so a track id (also contained within a `traf`) is also necessary to
|
||||
* associate it with a timescale
|
||||
*
|
||||
*
|
||||
* @param timescales {object} - a hash of track ids to timescale values.
|
||||
* @param fragment {Unit8Array} - the bytes of a media segment
|
||||
* @return {number} the composition start time for the fragment, in seconds
|
||||
**/
|
||||
compositionStartTime = function(timescales, fragment) {
|
||||
var trafBoxes = findBox(fragment, ['moof', 'traf']);
|
||||
var baseMediaDecodeTime = 0;
|
||||
var compositionTimeOffset = 0;
|
||||
var trackId;
|
||||
|
||||
if (trafBoxes && trafBoxes.length) {
|
||||
// The spec states that track run samples contained within a `traf` box are contiguous, but
|
||||
// it does not explicitly state whether the `traf` boxes themselves are contiguous.
|
||||
// We will assume that they are, so we only need the first to calculate start time.
|
||||
var tfhd = findBox(trafBoxes[0], ['tfhd'])[0];
|
||||
var trun = findBox(trafBoxes[0], ['trun'])[0];
|
||||
var tfdt = findBox(trafBoxes[0], ['tfdt'])[0];
|
||||
|
||||
if (tfhd) {
|
||||
var parsedTfhd = parseTfhd(tfhd);
|
||||
|
||||
trackId = parsedTfhd.trackId;
|
||||
}
|
||||
|
||||
if (tfdt) {
|
||||
var parsedTfdt = parseTfdt(tfdt);
|
||||
|
||||
baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime;
|
||||
}
|
||||
|
||||
if (trun) {
|
||||
var parsedTrun = parseTrun(trun);
|
||||
|
||||
if (parsedTrun.samples && parsedTrun.samples.length) {
|
||||
compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get timescale for this specific track. Assume a 90kHz clock if no timescale was
|
||||
// specified.
|
||||
var timescale = timescales[trackId] || 90e3;
|
||||
|
||||
// return the composition start time, in seconds
|
||||
return (baseMediaDecodeTime + compositionTimeOffset) / timescale;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the trackIds of the video tracks in this source.
|
||||
* Found by parsing the Handler Reference and Track Header Boxes:
|
||||
* moov > trak > mdia > hdlr
|
||||
* moov > trak > tkhd
|
||||
*
|
||||
* @param {Uint8Array} init - The bytes of the init segment for this source
|
||||
* @return {Number[]} A list of trackIds
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.4.3
|
||||
**/
|
||||
getVideoTrackIds = function(init) {
|
||||
var traks = findBox(init, ['moov', 'trak']);
|
||||
var videoTrackIds = [];
|
||||
|
||||
traks.forEach(function(trak) {
|
||||
var hdlrs = findBox(trak, ['mdia', 'hdlr']);
|
||||
var tkhds = findBox(trak, ['tkhd']);
|
||||
|
||||
hdlrs.forEach(function(hdlr, index) {
|
||||
var handlerType = parseType(hdlr.subarray(8, 12));
|
||||
var tkhd = tkhds[index];
|
||||
var view;
|
||||
var version;
|
||||
var trackId;
|
||||
|
||||
if (handlerType === 'vide') {
|
||||
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
||||
version = view.getUint8(0);
|
||||
trackId = (version === 0) ? view.getUint32(12) : view.getUint32(20);
|
||||
|
||||
videoTrackIds.push(trackId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return videoTrackIds;
|
||||
};
|
||||
|
||||
getTimescaleFromMediaHeader = function(mdhd) {
|
||||
// mdhd is a FullBox, meaning it will have its own version as the first byte
|
||||
var version = mdhd[0];
|
||||
var index = version === 0 ? 12 : 20;
|
||||
|
||||
return toUnsigned(
|
||||
mdhd[index] << 24 |
|
||||
mdhd[index + 1] << 16 |
|
||||
mdhd[index + 2] << 8 |
|
||||
mdhd[index + 3]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the video, audio, and hint tracks from a non fragmented
|
||||
* mp4 segment
|
||||
*/
|
||||
getTracks = function(init) {
|
||||
var traks = findBox(init, ['moov', 'trak']);
|
||||
var tracks = [];
|
||||
|
||||
traks.forEach(function(trak) {
|
||||
var track = {};
|
||||
var tkhd = findBox(trak, ['tkhd'])[0];
|
||||
var view, tkhdVersion;
|
||||
|
||||
// id
|
||||
if (tkhd) {
|
||||
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
||||
tkhdVersion = view.getUint8(0);
|
||||
|
||||
track.id = (tkhdVersion === 0) ? view.getUint32(12) : view.getUint32(20);
|
||||
}
|
||||
|
||||
var hdlr = findBox(trak, ['mdia', 'hdlr'])[0];
|
||||
|
||||
// type
|
||||
if (hdlr) {
|
||||
var type = parseType(hdlr.subarray(8, 12));
|
||||
|
||||
if (type === 'vide') {
|
||||
track.type = 'video';
|
||||
} else if (type === 'soun') {
|
||||
track.type = 'audio';
|
||||
} else {
|
||||
track.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// codec
|
||||
var stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
|
||||
|
||||
if (stsd) {
|
||||
var sampleDescriptions = stsd.subarray(8);
|
||||
// gives the codec type string
|
||||
track.codec = parseType(sampleDescriptions.subarray(4, 8));
|
||||
|
||||
var codecBox = findBox(sampleDescriptions, [track.codec])[0];
|
||||
var codecConfig, codecConfigType;
|
||||
|
||||
if (codecBox) {
|
||||
// https://tools.ietf.org/html/rfc6381#section-3.3
|
||||
if ((/^[a-z]vc[1-9]$/i).test(track.codec)) {
|
||||
// we don't need anything but the "config" parameter of the
|
||||
// avc1 codecBox
|
||||
codecConfig = codecBox.subarray(78);
|
||||
codecConfigType = parseType(codecConfig.subarray(4, 8));
|
||||
|
||||
if (codecConfigType === 'avcC' && codecConfig.length > 11) {
|
||||
track.codec += '.';
|
||||
|
||||
// left padded with zeroes for single digit hex
|
||||
// profile idc
|
||||
track.codec += toHexString(codecConfig[9]);
|
||||
// the byte containing the constraint_set flags
|
||||
track.codec += toHexString(codecConfig[10]);
|
||||
// level idc
|
||||
track.codec += toHexString(codecConfig[11]);
|
||||
} else {
|
||||
// TODO: show a warning that we couldn't parse the codec
|
||||
// and are using the default
|
||||
track.codec = 'avc1.4d400d';
|
||||
}
|
||||
} else if ((/^mp4[a,v]$/i).test(track.codec)) {
|
||||
// we do not need anything but the streamDescriptor of the mp4a codecBox
|
||||
codecConfig = codecBox.subarray(28);
|
||||
codecConfigType = parseType(codecConfig.subarray(4, 8));
|
||||
|
||||
if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) {
|
||||
track.codec += '.' + toHexString(codecConfig[19]);
|
||||
// this value is only a single digit
|
||||
track.codec += '.' + toHexString((codecConfig[20] >>> 2) & 0x3f).replace(/^0/, '');
|
||||
} else {
|
||||
// TODO: show a warning that we couldn't parse the codec
|
||||
// and are using the default
|
||||
track.codec = 'mp4a.40.2';
|
||||
}
|
||||
} else {
|
||||
// flac, opus, etc
|
||||
track.codec = track.codec.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
||||
|
||||
if (mdhd) {
|
||||
track.timescale = getTimescaleFromMediaHeader(mdhd);
|
||||
}
|
||||
|
||||
tracks.push(track);
|
||||
});
|
||||
|
||||
return tracks;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// export mp4 inspector's findBox and parseType for backwards compatibility
|
||||
findBox: findBox,
|
||||
parseType: parseType,
|
||||
timescale: timescale,
|
||||
startTime: startTime,
|
||||
compositionStartTime: compositionStartTime,
|
||||
videoTrackIds: getVideoTrackIds,
|
||||
tracks: getTracks,
|
||||
getTimescaleFromMediaHeader: getTimescaleFromMediaHeader
|
||||
};
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*
|
||||
* Utilities to detect basic properties and metadata about MP4s.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var toUnsigned = require('../utils/bin').toUnsigned;
|
||||
var toHexString = require('../utils/bin').toHexString;
|
||||
var findBox = require('../mp4/find-box.js');
|
||||
var parseType = require('../mp4/parse-type.js');
|
||||
var parseTfhd = require('../tools/parse-tfhd.js');
|
||||
var parseTrun = require('../tools/parse-trun.js');
|
||||
var parseTfdt = require('../tools/parse-tfdt.js');
|
||||
var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks,
|
||||
getTimescaleFromMediaHeader;
|
||||
|
||||
/**
|
||||
* Parses an MP4 initialization segment and extracts the timescale
|
||||
* values for any declared tracks. Timescale values indicate the
|
||||
* number of clock ticks per second to assume for time-based values
|
||||
* elsewhere in the MP4.
|
||||
*
|
||||
* To determine the start time of an MP4, you need two pieces of
|
||||
* information: the timescale unit and the earliest base media decode
|
||||
* time. Multiple timescales can be specified within an MP4 but the
|
||||
* base media decode time is always expressed in the timescale from
|
||||
* the media header box for the track:
|
||||
* ```
|
||||
* moov > trak > mdia > mdhd.timescale
|
||||
* ```
|
||||
* @param init {Uint8Array} the bytes of the init segment
|
||||
* @return {object} a hash of track ids to timescale values or null if
|
||||
* the init segment is malformed.
|
||||
*/
|
||||
timescale = function(init) {
|
||||
var
|
||||
result = {},
|
||||
traks = findBox(init, ['moov', 'trak']);
|
||||
|
||||
// mdhd timescale
|
||||
return traks.reduce(function(result, trak) {
|
||||
var tkhd, version, index, id, mdhd;
|
||||
|
||||
tkhd = findBox(trak, ['tkhd'])[0];
|
||||
if (!tkhd) {
|
||||
return null;
|
||||
}
|
||||
version = tkhd[0];
|
||||
index = version === 0 ? 12 : 20;
|
||||
id = toUnsigned(tkhd[index] << 24 |
|
||||
tkhd[index + 1] << 16 |
|
||||
tkhd[index + 2] << 8 |
|
||||
tkhd[index + 3]);
|
||||
|
||||
mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
||||
if (!mdhd) {
|
||||
return null;
|
||||
}
|
||||
version = mdhd[0];
|
||||
index = version === 0 ? 12 : 20;
|
||||
result[id] = toUnsigned(mdhd[index] << 24 |
|
||||
mdhd[index + 1] << 16 |
|
||||
mdhd[index + 2] << 8 |
|
||||
mdhd[index + 3]);
|
||||
return result;
|
||||
}, result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the base media decode start time, in seconds, for an MP4
|
||||
* fragment. If multiple fragments are specified, the earliest time is
|
||||
* returned.
|
||||
*
|
||||
* The base media decode time can be parsed from track fragment
|
||||
* metadata:
|
||||
* ```
|
||||
* moof > traf > tfdt.baseMediaDecodeTime
|
||||
* ```
|
||||
* It requires the timescale value from the mdhd to interpret.
|
||||
*
|
||||
* @param timescale {object} a hash of track ids to timescale values.
|
||||
* @return {number} the earliest base media decode start time for the
|
||||
* fragment, in seconds
|
||||
*/
|
||||
startTime = function(timescale, fragment) {
|
||||
var trafs, baseTimes, result;
|
||||
|
||||
// we need info from two childrend of each track fragment box
|
||||
trafs = findBox(fragment, ['moof', 'traf']);
|
||||
|
||||
// determine the start times for each track
|
||||
baseTimes = [].concat.apply([], trafs.map(function(traf) {
|
||||
return findBox(traf, ['tfhd']).map(function(tfhd) {
|
||||
var id, scale, baseTime;
|
||||
|
||||
// get the track id from the tfhd
|
||||
id = toUnsigned(tfhd[4] << 24 |
|
||||
tfhd[5] << 16 |
|
||||
tfhd[6] << 8 |
|
||||
tfhd[7]);
|
||||
// assume a 90kHz clock if no timescale was specified
|
||||
scale = timescale[id] || 90e3;
|
||||
|
||||
// get the base media decode time from the tfdt
|
||||
baseTime = findBox(traf, ['tfdt']).map(function(tfdt) {
|
||||
var version, result;
|
||||
|
||||
version = tfdt[0];
|
||||
result = toUnsigned(tfdt[4] << 24 |
|
||||
tfdt[5] << 16 |
|
||||
tfdt[6] << 8 |
|
||||
tfdt[7]);
|
||||
if (version === 1) {
|
||||
result *= Math.pow(2, 32);
|
||||
result += toUnsigned(tfdt[8] << 24 |
|
||||
tfdt[9] << 16 |
|
||||
tfdt[10] << 8 |
|
||||
tfdt[11]);
|
||||
}
|
||||
return result;
|
||||
})[0];
|
||||
baseTime = typeof baseTime === 'number' && !isNaN(baseTime) ? baseTime : Infinity;
|
||||
|
||||
// convert base time to seconds
|
||||
return baseTime / scale;
|
||||
});
|
||||
}));
|
||||
|
||||
// return the minimum
|
||||
result = Math.min.apply(null, baseTimes);
|
||||
return isFinite(result) ? result : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the composition start, in seconds, for an MP4
|
||||
* fragment.
|
||||
*
|
||||
* The composition start time of a fragment can be calculated using the base
|
||||
* media decode time, composition time offset, and timescale, as follows:
|
||||
*
|
||||
* compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale
|
||||
*
|
||||
* All of the aforementioned information is contained within a media fragment's
|
||||
* `traf` box, except for timescale info, which comes from the initialization
|
||||
* segment, so a track id (also contained within a `traf`) is also necessary to
|
||||
* associate it with a timescale
|
||||
*
|
||||
*
|
||||
* @param timescales {object} - a hash of track ids to timescale values.
|
||||
* @param fragment {Unit8Array} - the bytes of a media segment
|
||||
* @return {number} the composition start time for the fragment, in seconds
|
||||
**/
|
||||
compositionStartTime = function(timescales, fragment) {
|
||||
var trafBoxes = findBox(fragment, ['moof', 'traf']);
|
||||
var baseMediaDecodeTime = 0;
|
||||
var compositionTimeOffset = 0;
|
||||
var trackId;
|
||||
|
||||
if (trafBoxes && trafBoxes.length) {
|
||||
// The spec states that track run samples contained within a `traf` box are contiguous, but
|
||||
// it does not explicitly state whether the `traf` boxes themselves are contiguous.
|
||||
// We will assume that they are, so we only need the first to calculate start time.
|
||||
var tfhd = findBox(trafBoxes[0], ['tfhd'])[0];
|
||||
var trun = findBox(trafBoxes[0], ['trun'])[0];
|
||||
var tfdt = findBox(trafBoxes[0], ['tfdt'])[0];
|
||||
|
||||
if (tfhd) {
|
||||
var parsedTfhd = parseTfhd(tfhd);
|
||||
|
||||
trackId = parsedTfhd.trackId;
|
||||
}
|
||||
|
||||
if (tfdt) {
|
||||
var parsedTfdt = parseTfdt(tfdt);
|
||||
|
||||
baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime;
|
||||
}
|
||||
|
||||
if (trun) {
|
||||
var parsedTrun = parseTrun(trun);
|
||||
|
||||
if (parsedTrun.samples && parsedTrun.samples.length) {
|
||||
compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get timescale for this specific track. Assume a 90kHz clock if no timescale was
|
||||
// specified.
|
||||
var timescale = timescales[trackId] || 90e3;
|
||||
|
||||
// return the composition start time, in seconds
|
||||
return (baseMediaDecodeTime + compositionTimeOffset) / timescale;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the trackIds of the video tracks in this source.
|
||||
* Found by parsing the Handler Reference and Track Header Boxes:
|
||||
* moov > trak > mdia > hdlr
|
||||
* moov > trak > tkhd
|
||||
*
|
||||
* @param {Uint8Array} init - The bytes of the init segment for this source
|
||||
* @return {Number[]} A list of trackIds
|
||||
*
|
||||
* @see ISO-BMFF-12/2015, Section 8.4.3
|
||||
**/
|
||||
getVideoTrackIds = function(init) {
|
||||
var traks = findBox(init, ['moov', 'trak']);
|
||||
var videoTrackIds = [];
|
||||
|
||||
traks.forEach(function(trak) {
|
||||
var hdlrs = findBox(trak, ['mdia', 'hdlr']);
|
||||
var tkhds = findBox(trak, ['tkhd']);
|
||||
|
||||
hdlrs.forEach(function(hdlr, index) {
|
||||
var handlerType = parseType(hdlr.subarray(8, 12));
|
||||
var tkhd = tkhds[index];
|
||||
var view;
|
||||
var version;
|
||||
var trackId;
|
||||
|
||||
if (handlerType === 'vide') {
|
||||
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
||||
version = view.getUint8(0);
|
||||
trackId = (version === 0) ? view.getUint32(12) : view.getUint32(20);
|
||||
|
||||
videoTrackIds.push(trackId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return videoTrackIds;
|
||||
};
|
||||
|
||||
getTimescaleFromMediaHeader = function(mdhd) {
|
||||
// mdhd is a FullBox, meaning it will have its own version as the first byte
|
||||
var version = mdhd[0];
|
||||
var index = version === 0 ? 12 : 20;
|
||||
|
||||
return toUnsigned(
|
||||
mdhd[index] << 24 |
|
||||
mdhd[index + 1] << 16 |
|
||||
mdhd[index + 2] << 8 |
|
||||
mdhd[index + 3]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the video, audio, and hint tracks from a non fragmented
|
||||
* mp4 segment
|
||||
*/
|
||||
getTracks = function(init) {
|
||||
var traks = findBox(init, ['moov', 'trak']);
|
||||
var tracks = [];
|
||||
|
||||
traks.forEach(function(trak) {
|
||||
var track = {};
|
||||
var tkhd = findBox(trak, ['tkhd'])[0];
|
||||
var view, tkhdVersion;
|
||||
|
||||
// id
|
||||
if (tkhd) {
|
||||
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
||||
tkhdVersion = view.getUint8(0);
|
||||
|
||||
track.id = (tkhdVersion === 0) ? view.getUint32(12) : view.getUint32(20);
|
||||
}
|
||||
|
||||
var hdlr = findBox(trak, ['mdia', 'hdlr'])[0];
|
||||
|
||||
// type
|
||||
if (hdlr) {
|
||||
var type = parseType(hdlr.subarray(8, 12));
|
||||
|
||||
if (type === 'vide') {
|
||||
track.type = 'video';
|
||||
} else if (type === 'soun') {
|
||||
track.type = 'audio';
|
||||
} else {
|
||||
track.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// codec
|
||||
var stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
|
||||
|
||||
if (stsd) {
|
||||
var sampleDescriptions = stsd.subarray(8);
|
||||
// gives the codec type string
|
||||
track.codec = parseType(sampleDescriptions.subarray(4, 8));
|
||||
|
||||
var codecBox = findBox(sampleDescriptions, [track.codec])[0];
|
||||
var codecConfig, codecConfigType;
|
||||
|
||||
if (codecBox) {
|
||||
// https://tools.ietf.org/html/rfc6381#section-3.3
|
||||
if ((/^[a-z]vc[1-9]$/i).test(track.codec)) {
|
||||
// we don't need anything but the "config" parameter of the
|
||||
// avc1 codecBox
|
||||
codecConfig = codecBox.subarray(78);
|
||||
codecConfigType = parseType(codecConfig.subarray(4, 8));
|
||||
|
||||
if (codecConfigType === 'avcC' && codecConfig.length > 11) {
|
||||
track.codec += '.';
|
||||
|
||||
// left padded with zeroes for single digit hex
|
||||
// profile idc
|
||||
track.codec += toHexString(codecConfig[9]);
|
||||
// the byte containing the constraint_set flags
|
||||
track.codec += toHexString(codecConfig[10]);
|
||||
// level idc
|
||||
track.codec += toHexString(codecConfig[11]);
|
||||
} else {
|
||||
// TODO: show a warning that we couldn't parse the codec
|
||||
// and are using the default
|
||||
track.codec = 'avc1.4d400d';
|
||||
}
|
||||
} else if ((/^mp4[a,v]$/i).test(track.codec)) {
|
||||
// we do not need anything but the streamDescriptor of the mp4a codecBox
|
||||
codecConfig = codecBox.subarray(28);
|
||||
codecConfigType = parseType(codecConfig.subarray(4, 8));
|
||||
|
||||
if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) {
|
||||
track.codec += '.' + toHexString(codecConfig[19]);
|
||||
// this value is only a single digit
|
||||
track.codec += '.' + toHexString((codecConfig[20] >>> 2) & 0x3f).replace(/^0/, '');
|
||||
} else {
|
||||
// TODO: show a warning that we couldn't parse the codec
|
||||
// and are using the default
|
||||
track.codec = 'mp4a.40.2';
|
||||
}
|
||||
} else {
|
||||
// flac, opus, etc
|
||||
track.codec = track.codec.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
||||
|
||||
if (mdhd) {
|
||||
track.timescale = getTimescaleFromMediaHeader(mdhd);
|
||||
}
|
||||
|
||||
tracks.push(track);
|
||||
});
|
||||
|
||||
return tracks;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// export mp4 inspector's findBox and parseType for backwards compatibility
|
||||
findBox: findBox,
|
||||
parseType: parseType,
|
||||
timescale: timescale,
|
||||
startTime: startTime,
|
||||
compositionStartTime: compositionStartTime,
|
||||
videoTrackIds: getVideoTrackIds,
|
||||
tracks: getTracks,
|
||||
getTimescaleFromMediaHeader: getTimescaleFromMediaHeader
|
||||
};
|
||||
|
|
214
node_modules/mux.js/lib/mp4/track-decode-info.js
generated
vendored
214
node_modules/mux.js/lib/mp4/track-decode-info.js
generated
vendored
|
@ -1,107 +1,107 @@
|
|||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
var ONE_SECOND_IN_TS = require('../utils/clock').ONE_SECOND_IN_TS;
|
||||
|
||||
/**
|
||||
* Store information about the start and end of the track and the
|
||||
* duration for each frame/sample we process in order to calculate
|
||||
* the baseMediaDecodeTime
|
||||
*/
|
||||
var collectDtsInfo = function(track, data) {
|
||||
if (typeof data.pts === 'number') {
|
||||
if (track.timelineStartInfo.pts === undefined) {
|
||||
track.timelineStartInfo.pts = data.pts;
|
||||
}
|
||||
|
||||
if (track.minSegmentPts === undefined) {
|
||||
track.minSegmentPts = data.pts;
|
||||
} else {
|
||||
track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
|
||||
}
|
||||
|
||||
if (track.maxSegmentPts === undefined) {
|
||||
track.maxSegmentPts = data.pts;
|
||||
} else {
|
||||
track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.dts === 'number') {
|
||||
if (track.timelineStartInfo.dts === undefined) {
|
||||
track.timelineStartInfo.dts = data.dts;
|
||||
}
|
||||
|
||||
if (track.minSegmentDts === undefined) {
|
||||
track.minSegmentDts = data.dts;
|
||||
} else {
|
||||
track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
|
||||
}
|
||||
|
||||
if (track.maxSegmentDts === undefined) {
|
||||
track.maxSegmentDts = data.dts;
|
||||
} else {
|
||||
track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear values used to calculate the baseMediaDecodeTime between
|
||||
* tracks
|
||||
*/
|
||||
var clearDtsInfo = function(track) {
|
||||
delete track.minSegmentDts;
|
||||
delete track.maxSegmentDts;
|
||||
delete track.minSegmentPts;
|
||||
delete track.maxSegmentPts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the track's baseMediaDecodeTime based on the earliest
|
||||
* DTS the transmuxer has ever seen and the minimum DTS for the
|
||||
* current track
|
||||
* @param track {object} track metadata configuration
|
||||
* @param keepOriginalTimestamps {boolean} If true, keep the timestamps
|
||||
* in the source; false to adjust the first segment to start at 0.
|
||||
*/
|
||||
var calculateTrackBaseMediaDecodeTime = function(track, keepOriginalTimestamps) {
|
||||
var
|
||||
baseMediaDecodeTime,
|
||||
scale,
|
||||
minSegmentDts = track.minSegmentDts;
|
||||
|
||||
// Optionally adjust the time so the first segment starts at zero.
|
||||
if (!keepOriginalTimestamps) {
|
||||
minSegmentDts -= track.timelineStartInfo.dts;
|
||||
}
|
||||
|
||||
// track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
|
||||
// we want the start of the first segment to be placed
|
||||
baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime;
|
||||
|
||||
// Add to that the distance this segment is from the very first
|
||||
baseMediaDecodeTime += minSegmentDts;
|
||||
|
||||
// baseMediaDecodeTime must not become negative
|
||||
baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
|
||||
|
||||
if (track.type === 'audio') {
|
||||
// Audio has a different clock equal to the sampling_rate so we need to
|
||||
// scale the PTS values into the clock rate of the track
|
||||
scale = track.samplerate / ONE_SECOND_IN_TS;
|
||||
baseMediaDecodeTime *= scale;
|
||||
baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
|
||||
}
|
||||
|
||||
return baseMediaDecodeTime;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
clearDtsInfo: clearDtsInfo,
|
||||
calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
|
||||
collectDtsInfo: collectDtsInfo
|
||||
};
|
||||
/**
|
||||
* mux.js
|
||||
*
|
||||
* Copyright (c) Brightcove
|
||||
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
||||
*/
|
||||
var ONE_SECOND_IN_TS = require('../utils/clock').ONE_SECOND_IN_TS;
|
||||
|
||||
/**
|
||||
* Store information about the start and end of the track and the
|
||||
* duration for each frame/sample we process in order to calculate
|
||||
* the baseMediaDecodeTime
|
||||
*/
|
||||
var collectDtsInfo = function(track, data) {
|
||||
if (typeof data.pts === 'number') {
|
||||
if (track.timelineStartInfo.pts === undefined) {
|
||||
track.timelineStartInfo.pts = data.pts;
|
||||
}
|
||||
|
||||
if (track.minSegmentPts === undefined) {
|
||||
track.minSegmentPts = data.pts;
|
||||
} else {
|
||||
track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
|
||||
}
|
||||
|
||||
if (track.maxSegmentPts === undefined) {
|
||||
track.maxSegmentPts = data.pts;
|
||||
} else {
|
||||
track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.dts === 'number') {
|
||||
if (track.timelineStartInfo.dts === undefined) {
|
||||
track.timelineStartInfo.dts = data.dts;
|
||||
}
|
||||
|
||||
if (track.minSegmentDts === undefined) {
|
||||
track.minSegmentDts = data.dts;
|
||||
} else {
|
||||
track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
|
||||
}
|
||||
|
||||
if (track.maxSegmentDts === undefined) {
|
||||
track.maxSegmentDts = data.dts;
|
||||
} else {
|
||||
track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear values used to calculate the baseMediaDecodeTime between
|
||||
* tracks
|
||||
*/
|
||||
var clearDtsInfo = function(track) {
|
||||
delete track.minSegmentDts;
|
||||
delete track.maxSegmentDts;
|
||||
delete track.minSegmentPts;
|
||||
delete track.maxSegmentPts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the track's baseMediaDecodeTime based on the earliest
|
||||
* DTS the transmuxer has ever seen and the minimum DTS for the
|
||||
* current track
|
||||
* @param track {object} track metadata configuration
|
||||
* @param keepOriginalTimestamps {boolean} If true, keep the timestamps
|
||||
* in the source; false to adjust the first segment to start at 0.
|
||||
*/
|
||||
var calculateTrackBaseMediaDecodeTime = function(track, keepOriginalTimestamps) {
|
||||
var
|
||||
baseMediaDecodeTime,
|
||||
scale,
|
||||
minSegmentDts = track.minSegmentDts;
|
||||
|
||||
// Optionally adjust the time so the first segment starts at zero.
|
||||
if (!keepOriginalTimestamps) {
|
||||
minSegmentDts -= track.timelineStartInfo.dts;
|
||||
}
|
||||
|
||||
// track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
|
||||
// we want the start of the first segment to be placed
|
||||
baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime;
|
||||
|
||||
// Add to that the distance this segment is from the very first
|
||||
baseMediaDecodeTime += minSegmentDts;
|
||||
|
||||
// baseMediaDecodeTime must not become negative
|
||||
baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
|
||||
|
||||
if (track.type === 'audio') {
|
||||
// Audio has a different clock equal to the sampling_rate so we need to
|
||||
// scale the PTS values into the clock rate of the track
|
||||
scale = track.samplerate / ONE_SECOND_IN_TS;
|
||||
baseMediaDecodeTime *= scale;
|
||||
baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
|
||||
}
|
||||
|
||||
return baseMediaDecodeTime;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
clearDtsInfo: clearDtsInfo,
|
||||
calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
|
||||
collectDtsInfo: collectDtsInfo
|
||||
};
|
||||
|
|
2476
node_modules/mux.js/lib/mp4/transmuxer.js
generated
vendored
2476
node_modules/mux.js/lib/mp4/transmuxer.js
generated
vendored
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue