videostream-mp4box
Version:
JavaScript version of GPAC's MP4Box tool
615 lines (568 loc) • 19.3 kB
JavaScript
var DataStream = require('./../src/DataStream');
var MP4Box = require('./../src/mp4box');
var TextMP4 = require('./../src/text-mp4');
var VTTin4Parser = TextMP4.VTTin4Parser;
var XMLSubtitlein4Parser = TextMP4.XMLSubtitlein4Parser;
var Textin4Parser = TextMP4.Textin4Parser;
/* Setting the level of logs (error, warning, info, debug) */
Log.setLogLevel(Log.i);
/* The main object processing the mp4 files */
var mp4box;
/* Metadata extracted from the mp4 file */
var movieInfo;
/* object responsible for file downloading */
var downloader = new Downloader();
/* the HTML5 video element */
var video;
var autoplay = false;
var startButton, loadButton, initButton, initAllButton, playButton;
var urlInput, chunkTimeoutInput, chunkSizeInput;
var infoDiv, dlTimeoutDiv;
var chunkTimeoutLabel, chunkSizeLabel, segmentSizeLabel;
var urlSelector;
var saveChecked;
window.onload = function () {
video = document.getElementById('v');
playButton = document.getElementById("playButton");
startButton = document.getElementById("startButton");
loadButton = document.getElementById("loadButton");
initButton = document.getElementById("initButton");
initAllButton = document.getElementById("initAllButton");
urlInput = document.getElementById('url');
chunkTimeoutInput = document.getElementById('chunk_speed_range');
chunkSizeInput = document.getElementById("segment_size_range");
infoDiv = document.getElementById('infoDiv');
dlTimeoutDiv = document.getElementById('dlTimeout');
chunkTimeoutLabel = document.querySelector('#chunk_speed_range_out');
chunkSizeLabel = document.querySelector('#chunk_size_range_out');
segmentSizeLabel = document.querySelector('#segment_size_range_out');
playbackRateLabel = document.querySelector('#playback_rate_range_out');
chunkDownloadBitRate = document.querySelector('#chunk_dl_rate');
urlSelector = document.getElementById('urlSelector');
urlSelector.selectedIndex = -1;
saveChecked = document.getElementById("saveChecked");
video.addEventListener("seeking", onSeeking);
reset();
}
/* GUI-related callback functions */
function setUrl(url) {
urlInput.value = url;
if (urlInput.value !== "") {
loadButton.disabled = false;
playButton.disabled = false;
} else {
loadButton.disabled = true;
playButton.disabled = false;
}
}
function toggleDownloadMode(event) {
var checkedBox = event.target;
if (checkedBox.checked) {
dlTimeoutDiv.style.display = "none";
downloader.setRealTime(true);
} else {
dlTimeoutDiv.style.display = "inline";
downloader.setRealTime(false);
}
}
function setDownloadTimeout(value) {
var b;
chunkTimeoutLabel.value = value;
chunkTimeoutInput.value = value;
downloader.setInterval(parseInt(value));
b = Math.floor(parseInt(chunkSizeLabel.value)*8/parseInt(value));
chunkDownloadBitRate.innerHTML = b;
}
function setDownloadChunkSize(value) {
chunkSizeLabel.value = value;
downloader.setChunkSize(parseInt(value));
chunkDownloadBitRate.innerHTML = Math.floor(parseInt(chunkSizeLabel.value)*8/parseInt(chunkTimeoutInput.value));
}
function setSegmentSize(value) {
segmentSizeLabel.value = value;
}
function setPlaybackRate(value) {
playbackRateLabel.value = value;
video.playbackRate = parseInt(value);
}
/* Functions to generate the tables displaying file information */
function resetDisplay() {
infoDiv.innerHTML = '';
}
function getBasicTrackHeader() {
var html = '';
html += "<th>Track ID</th>";
html += "<th>Track References</th>";
html += "<th>Alternate Group</th>";
html += "<th>Creation Date</th>";
html += "<th>Modified Date</th>";
html += "<th>Timescale</th>";
html += "<th>Media Duration</th>";
html += "<th>Number of Samples</th>";
html += "<th>Bitrate (kbps)</th>";
html += "<th>Codec</th>";
html += "<th>Language</th>";
html += "<th>Track Width</th>";
html += "<th>Track Height</th>";
html += "<th>Track Layer</th>";
return html;
}
function getBasicTrackInfo(track) {
var html = '';
html += "<td>"+track.id+"</td>";
html += "<td>";
if (track.references.length === 0) {
html += "none";
} else {
for (var i = 0; i < track.references.length; i++) {
if (i > 0) html += "<br>";
html += "Reference of type "+track.references[i]+" to tracks "+track.references[i].track_ids;
}
}
html += "</td>";
html += "<td>"+track.alternate_group+"</td>";
html += "<td>"+track.created+"</td>";
html += "<td>"+track.modified+"</td>";
html += "<td>"+track.timescale+"</td>";
html += "<td>"+track.duration+" ("+Log.getDurationString(track.duration,track.timescale)+") </td>";
html += "<td>"+track.nb_samples+"</td>";
html += "<td>"+Math.floor(track.bitrate/1024)+"</td>";
html += "<td>"+track.codec+"</td>";
html += "<td>"+track.language+"</td>";
html += "<td>"+track.track_width+"</td>";
html += "<td>"+track.track_height+"</td>";
html += "<td>"+track.layer+"</td>";
return html;
}
function getVideoTrackHeader() {
var html = '';
html += "<th>Width</th>";
html += "<th>Height</th>";
return html;
}
function getVideoTrackInfo(track) {
var html = '';
html += "<td>"+track.video.width+"</td>";
html += "<td>"+track.video.height+"</td>";
return html;
}
function getAudioTrackHeader() {
var html = '';
html += "<th>Sample Rate</th>";
html += "<th>Channel Count</th>";
html += "<th>Volume</th>";
return html;
}
function getAudioTrackInfo(track) {
var html = '';
html += "<td>"+track.audio.sample_rate+"</td>";
html += "<td>"+track.audio.channel_count+"</td>";
html += "<td>"+track.volume+"</td>";
return html;
}
function getTrackListInfo(tracks, type) {
var html = '';
if (tracks.length>0) {
html += type+" track(s) info";
html += "<table>";
html += "<tr>";
html += getBasicTrackHeader();
switch (type) {
case "Video":
html += getVideoTrackHeader();
break;
case "Audio":
html += getAudioTrackHeader();
break;
case "Subtitle":
break;
case "Metadata":
break;
case "Hint":
break;
default:
break;
}
html += "<th>Source Buffer Status</th>";
html += "</tr>";
for (var i = 0; i < tracks.length; i++) {
html += "<tr>";
html += getBasicTrackInfo(tracks[i]);
switch (type) {
case "Video":
html += getVideoTrackInfo(tracks[i]);
break;
case "Audio":
html += getAudioTrackInfo(tracks[i]);
break;
case "Subtitle":
break;
case "Metadata":
break;
case "Hint":
break;
default:
break;
}
var mime = 'video/mp4; codecs=\"'+tracks[i].codec+'\"';
if (MediaSource.isTypeSupported(mime)) {
html += "<td id=\"buffer"+tracks[i].id+"\">"+"<input id=\"addTrack"+tracks[i].id+"\" type=\"checkbox\">"+"</td>";
} else {
html += "<td>Not supported by your browser, exposing track content using HTML TextTrack <input id=\"addTrack"+tracks[i].id+"\" type=\"checkbox\"></td>";
}
html += "</tr>";
}
html += "</table>";
}
return html;
}
function displayMovieInfo(info) {
var html = "Movie Info";
var fileLength = downloader.getFileLength();
html += "<div>";
html += "<table>";
html += "<tr><th>File Size</th><td>"+fileLength+" bytes</td></tr>";
html += "<tr><th>Brands</th><td>"+info.brands+"</td></tr>";
html += "<tr><th>Creation Date</th><td>"+info.created+"</td></tr>";
html += "<tr><th>Modified Date</th><td>"+info.modified+"</td></tr>";
html += "<tr><th>Timescale</th><td>"+info.timescale+"</td></tr>";
html += "<tr><th>Duration</th><td>"+info.duration+" ("+Log.getDurationString(info.duration,info.timescale)+")</td></tr>";
html += "<tr><th>Bitrate</th><td>"+Math.floor((fileLength*8*info.timescale)/(info.duration*1000))+" kbps</td></tr>";
html += "<tr><th>Progressive</th><td>"+info.isProgressive+"</td></tr>";
html += "<tr><th>Fragmented</th><td>"+info.isFragmented+"</td></tr>";
html += "<tr><th>MPEG-4 IOD</th><td>"+info.hasIOD+"</td></tr>";
if (info.isFragmented) {
html += "<tr><th>Fragmented duration</th><td>"+info.fragment_duration+" ("+Log.getDurationString(info.fragment_duration,info.timescale)+")</td></tr>";
}
html += "</table>";
html += getTrackListInfo(info.videoTracks, "Video");
html += getTrackListInfo(info.audioTracks, "Audio");
html += getTrackListInfo(info.subtitleTracks, "Subtitle");
html += getTrackListInfo(info.metadataTracks, "Metadata");
html += getTrackListInfo(info.otherTracks, "Other");
html += "</div>";
infoDiv.innerHTML = html;
}
/* main functions, MSE-related */
function resetMediaSource() {
var mediaSource;
mediaSource = new MediaSource();
mediaSource.video = video;
video.ms = mediaSource;
mediaSource.addEventListener("sourceopen", onSourceOpen);
mediaSource.addEventListener("sourceclose", onSourceClose);
video.src = window.URL.createObjectURL(mediaSource);
/* TODO: remove Text tracks */
}
function onSourceClose(e) {
var ms = e.target;
Log.e("MSE", "Source closed, video error: "+ (ms.video.error ? ms.video.error.code : "(none)"));
Log.d("MSE", ms);
}
function onSourceOpen(e) {
var ms = e.target;
Log.i("MSE", "Source opened");
Log.d("MSE", ms);
urlSelector.disabled = false;
}
function updateBufferedString(sb, string) {
var rangeString;
if (sb.ms.readyState === "open") {
rangeString = Log.printRanges(sb.buffered);
Log.i("MSE - SourceBuffer #"+sb.id, string+", updating: "+sb.updating+", currentTime: "+Log.getDurationString(video.currentTime, 1)+", buffered: "+rangeString+", pending: "+sb.pendingAppends.length);
if (sb.bufferTd === undefined) {
sb.bufferTd = document.getElementById("buffer"+sb.id);
}
sb.bufferTd.textContent = rangeString;
}
}
function onInitAppended(e) {
var sb = e.target;
if (sb.ms.readyState === "open") {
updateBufferedString(sb, "Init segment append ended");
sb.sampleNum = 0;
sb.removeEventListener('updateend', onInitAppended);
sb.addEventListener('updateend', onUpdateEnd.bind(sb, true));
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/
onUpdateEnd.call(sb, false);
if (autoplay) {
start();
}
}
}
function onUpdateEnd(isNotInit) {
if (isNotInit === true) {
updateBufferedString(this, "Update ended");
}
if (this.sampleNum) {
mp4box.releaseUsedSamples(this.id, this.sampleNum);
delete this.sampleNum;
}
if (this.ms.readyState === "open" && this.updating === false && this.pendingAppends.length > 0) {
var obj = this.pendingAppends.shift();
Log.i("MSE - SourceBuffer #"+this.id, "Appending new buffer, pending: "+this.pendingAppends.length);
this.sampleNum = obj.sampleNum;
this.appendBuffer(obj.buffer);
}
}
function addBuffer(video, track_id, codec) {
var sb;
var ms = video.ms;
var mime = 'video/mp4; codecs=\"'+codec+'\"';
if (MediaSource.isTypeSupported(mime)) {
Log.i("MSE - SourceBuffer #"+track_id,"Creation with type '"+mime+"'");
sb = ms.addSourceBuffer(mime);
sb.ms = ms;
sb.id = track_id;
mp4box.setSegmentOptions(track_id, sb, { nbSamples: parseInt(segmentSizeLabel.value) } );
sb.pendingAppends = [];
} else {
Log.w("MSE", "MIME type '"+mime+"' not supported for creation of a SourceBuffer for track id "+track_id);
var textrack = video.addTextTrack("subtitles", "Text track for track "+track_id);
mp4box.setExtractionOptions(track_id, textrack, { nbSamples: 1 });
}
}
function removeBuffer(video, track_id) {
var sb;
var ms = video.ms;
Log.i("MSE - SourceBuffer #"+track_id,"Removing buffer");
mp4box.unsetSegmentOptions(track_id);
for (var i = 0; i < ms.sourceBuffers.length; i++) {
sb = ms.sourceBuffers[i];
if (sb.id == track_id) {
ms.removeSourceBuffer(sb);
break;
}
}
if (ms.sourceBuffers.length === 0) {
return true;
} else {
return false;
}
}
function addSourceBufferListener(info) {
for (var i = 0; i < info.tracks.length; i++) {
var track = info.tracks[i];
var checkBox = document.getElementById("addTrack"+track.id);
if (!checkBox) continue;
checkBox.addEventListener("change", (function (track_id, codec) {
return function (e) {
var check = e.target;
if (check.checked) {
addBuffer(video, track_id, codec);
initButton.disabled = false;
} else {
initButton.disabled = removeBuffer(video, track_id);
}
};
})(track.id, track.codec));
}
}
function initializeAllSourceBuffers() {
if (movieInfo) {
var info = movieInfo;
for (var i = 0; i < info.tracks.length; i++) {
var track = info.tracks[i];
addBuffer(video, track.id, track.codec);
}
initAllButton.disabled = true;
initButton.disabled = true;
startButton.disabled = false;
initializeSourceBuffers();
}
}
function initializeSourceBuffers() {
var initSegs = mp4box.initializeSegmentation();
for (var i = 0; i < initSegs.length; i++) {
var sb = initSegs[i].user;
sb.addEventListener("updateend", onInitAppended);
Log.i("MSE - SourceBuffer #"+sb.id,"Appending initialization data");
sb.appendBuffer(initSegs[i].buffer);
saveBuffer(initSegs[i].buffer, 'track-'+initSegs[i].id+'-init.mp4');
sb.segmentIndex = 0;
}
initAllButton.disabled = true;
initButton.disabled = true;
startButton.disabled = false;
}
/* main player functions */
function reset() {
stop();
downloader.reset();
startButton.disabled = true;
resetMediaSource();
resetDisplay();
}
function load() {
var ms = video.ms;
if (ms.readyState !== "open") {
return;
}
mp4box = new MP4Box();
mp4box.onMoovStart = function () {
Log.i("Application", "Starting to parse movie information");
}
mp4box.onReady = function (info) {
Log.i("Application", "Movie information received");
movieInfo = info;
stop();
if (info.isFragmented) {
ms.duration = info.fragment_duration/info.timescale;
} else {
ms.duration = info.duration/info.timescale;
}
displayMovieInfo(info);
addSourceBufferListener(info);
if (autoplay) {
initializeAllSourceBuffers();
} else {
initAllButton.disabled = false;
}
}
mp4box.onSegment = function (id, user, buffer, sampleNum) {
var sb = user;
saveBuffer(buffer, 'track-'+id+'-segment-'+sb.segmentIndex+'.m4s');
sb.segmentIndex++;
sb.pendingAppends.push({ id: id, buffer: buffer, sampleNum: sampleNum });
Log.i("Application","Received new segment for track "+id+" up to sample #"+sampleNum+", segments pending append: "+sb.pendingAppends.length);
onUpdateEnd.call(sb, true);
}
mp4box.onSamples = function (id, user, samples) {
var texttrack = user;
Log.i("TextTrack #"+id,"Received "+samples.length+" new sample(s)");
for (var j = 0; j < samples.length; j++) {
var sample = samples[j];
if (sample.description.type === "wvtt") {
var vtt4Parser = new VTTin4Parser();
var cues = vtt4Parser.parseSample(sample.data);
for (var i = 0; i < cues.length; i++) {
var cueIn4 = cues[i];
var cue = new VTTCue(sample.dts/sample.timescale, (sample.dts+sample.duration)/sample.timescale, cueIn4.payl.text);
texttrack.addCue(cue);
}
} else if (sample.description.type === "metx" || sample.description.type === "stpp") {
var xmlSub4Parser = new XMLSubtitlein4Parser();
var xmlSubSample = xmlSub4Parser.parseSample(sample);
console.log("Parsed XML sample at time "+Log.getDurationString(sample.dts,sample.timescale)+" :", xmlSubSample.document);
} else if (sample.description.type === "mett" || sample.description.type === "sbtt" || sample.description.type === "stxt") {
var textSampleParser = new Textin4Parser();
if (sample.description.txtC && j===0) {
console.log("Parser Configuration: ", sample.description.txtC.config);
}
var textSample = textSampleParser.parseSample(sample);
console.log("Parsed text sample at time "+Log.getDurationString(sample.dts,sample.timescale)+" :");
console.log(textSample);
}
}
}
loadButton.disabled = true;
startButton.disabled = true;
stopButton.disabled = false;
downloader.setCallback(
function (response, end, error) {
if (response) {
var nextStart = mp4box.appendBuffer(response);
downloader.setChunkStart(nextStart);
}
if (end) {
mp4box.flush();
}
if (error) {
reset();
}
}
);
downloader.setInterval(parseInt(chunkTimeoutLabel.value));
downloader.setChunkSize(parseInt(chunkSizeLabel.value));
downloader.setUrl(urlInput.value);
loadButton.disabled = true;
downloader.start();
}
function start() {
startButton.disabled = true;
stopButton.disabled = false;
downloader.setChunkStart(mp4box.seek(0, true).offset);
downloader.setChunkSize(parseInt(chunkSizeLabel.value));
downloader.setInterval(parseInt(chunkTimeoutLabel.value));
downloader.resume();
}
function stop() {
if (!downloader.isStopped()) {
stopButton.disabled = true;
startButton.disabled = false;
downloader.stop();
}
}
function play() {
playButton.disabled = true;
autoplay = true;
load();
}
function onSeeking(e) {
var i, start, end;
var seek_info;
if (video.lastSeekTime !== video.currentTime) {
for (i = 0; i < video.buffered.length; i++) {
start = video.buffered.start(i);
end = video.buffered.end(i);
if (video.currentTime >= start && video.currentTime <= end) {
return;
}
}
/* Chrome fires twice the seeking event with the same value */
Log.i("Application", "Seeking called to video time "+Log.getDurationString(video.currentTime));
seek_info = mp4box.seek(video.currentTime, true);
downloader.stop();
downloader.setChunkStart(seek_info.offset);
downloader.resume();
startButton.disabled = true;
stopButton.disabled = false;
video.lastSeekTime = video.currentTime;
}
}
function computeWaitingTimeFromBuffer(v) {
var ms = v.ms;
var sb;
var startRange, endRange;
var currentTime = v.currentTime;
var playbackRate = v.playbackRate;
var maxStartRange = 0;
var minEndRange = Infinity;
var ratio;
var wait;
var duration;
/* computing the intersection of the buffered values of all active sourcebuffers around the current time,
may already be done by the browser when calling video.buffered (to be checked: TODO) */
for (var i = 0; i < ms.activeSourceBuffers.length; i++) {
sb = ms.activeSourceBuffers.item(i);
for (var j = 0; j < sb.buffered.length; j++) {
startRange = sb.buffered.start(j);
endRange = sb.buffered.end(j);
if (currentTime >= startRange && currentTime <= endRange) {
if (startRange >= maxStartRange) maxStartRange = startRange;
if (endRange <= minEndRange) minEndRange = endRange;
break;
}
}
}
duration = minEndRange - maxStartRange;
ratio = (currentTime - maxStartRange)/duration;
Log.i("Demo", "Playback position ("+Log.getDurationString(currentTime)+") in current buffer ["+Log.getDurationString(maxStartRange)+","+Log.getDurationString(minEndRange)+"]: "+Math.floor(ratio*100)+"%");
if (ratio >= 3/(playbackRate+3)) {
Log.i("Demo", "Downloading immediately new data!");
/* when the currentTime of the video is at more than 3/4 of the buffered range (for a playback rate of 1),
immediately fetch a new buffer */
return 1; /* return 1 ms (instead of 0) to be able to compute a non-infinite bitrate value */
} else {
/* if not, wait for half (at playback rate of 1) of the remaining time in the buffer */
wait = 1000*(minEndRange - currentTime)/(2*playbackRate);
Log.i("Demo", "Waiting for "+Log.getDurationString(wait,1000)+" s for the next download");
return wait;
}
}
function saveBuffer(buffer, name) {
if (saveChecked.checked) {
var d = new DataStream(buffer);
d.save(name);
}
}