Last active
November 6, 2016 00:28
-
-
Save zhehaowang/4e55452df6d4240dea7edc674a5a82a3 to your computer and use it in GitHub Desktop.
In browser, buffer and play video served by ndnfs; NDN hackathon 2016
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
</head> | |
<body> | |
<video id="video1"> | |
</video> | |
<script type="text/javascript" src="../../build/ndn.js"></script> | |
<script type="text/javascript"> | |
var face = new Face(new MicroForwarderTransport(), | |
new MicroForwarderTransport.ConnectionInfo()); | |
</script> | |
<script type="text/javascript"> | |
// documents for "proper mp4 fragmentation": | |
// https://hacks.mozilla.org/2015/07/streaming-media-on-demand-with-media-source-extensions/ | |
// keywords: media-source-extensions | |
// this page in chrome might help with debugging codec / mp4 fragmentation: chrome://media-internals/ | |
// for the streaming part, this doc helped: https://developers.google.com/web/updates/2016/03/mse-sourcebuffer | |
// initial code comes from https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer | |
var video = document.getElementById('video1'); | |
var assetURL = '/ndn/hackathon/demo/NDN-VFAQ-0001-HD-fragmented.mp4/%FDX%1E%15K'; | |
//var assetURL = 'NDN-VFAQ-0001-HD-fragmented.mp4'; | |
// Need to be specific for Blink regarding codecs | |
// ./mp4info frag_bunny.mp4 | grep Codec | |
var mimeCodec = 'video/mp4; codecs="avc1.64001F, mp4a.40.2"'; | |
if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) { | |
var mediaSource = new MediaSource(); | |
//console.log(mediaSource.readyState); // closed | |
video.src = URL.createObjectURL(mediaSource); | |
mediaSource.addEventListener('sourceopen', sourceOpen); | |
} else { | |
console.error('Unsupported MIME type or codec: ', mimeCodec); | |
} | |
function sourceOpen (_) { | |
//console.log(this.readyState); // open | |
var mediaSource = this; | |
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); | |
var segmentStreamer = new SegmentStreamer(face, function (buf, done) { | |
if (done) { | |
mediaSource.endOfStream(); | |
} else { | |
sourceBuffer.appendBuffer(buf); | |
if (video.paused) { | |
// start playing after first chunk is appended | |
video.play(); | |
} | |
} | |
}); | |
// Fetch the first segment to initiate segment fetching. | |
face.expressInterest | |
(new Name(assetURL).appendSegment(0), | |
segmentStreamer.onData.bind(segmentStreamer), | |
function(interest) { | |
console.log("Timeout fetching initial interest: " + interest.getName().toUri()); | |
}); | |
}; | |
var SegmentStreamer = function SegmentStreamer(face, callback) | |
{ | |
this.face_ = face; | |
this.callback_ = callback; | |
this.finalSegmentNumber_ = null; | |
this.segmentStore_ = new SegmentStore(); | |
} | |
SegmentStreamer.prototype.onData = function(interest, data) | |
{ | |
console.log("Debug: Received data " + data.getName().toUri()); | |
if (!data.getName().get(-1).isSegment()) { | |
console.log("Skipping non-segment"); | |
return; | |
} | |
var segmentNumber = data.getName().get(-1).toSegment(); | |
this.segmentStore_.storeContent(segmentNumber, data); | |
if (data.getMetaInfo().getFinalBlockId().getValue().size() > 0) | |
this.finalSegmentNumber_ = | |
data.getMetaInfo().getFinalBlockId().toSegment(); | |
// The content was already put in the store. Retrieve as much as possible. | |
var entry; | |
while ((entry = this.segmentStore_.maybeRetrieveNextEntry()) !== null) { | |
segmentNumber = entry.key; | |
var done = (this.finalSegmentNumber_ !== null && | |
segmentNumber === this.finalSegmentNumber_); | |
/* | |
this.callback_(entry.value.getContent().buf(), done); | |
*/ | |
if (done) | |
return; | |
} | |
// Request new segments. | |
var toRequest = this.segmentStore_.requestSegmentNumbers(8); | |
for (var i = 0; i < toRequest.length; ++i) { | |
if (this.finalSegmentNumber_ !== null && | |
toRequest[i] > this.finalSegmentNumber_) | |
continue; | |
this.face_.expressInterest | |
(data.getName().getPrefix(-1).appendSegment(toRequest[i]), | |
this.onData.bind(this), this.onTimeout.bind(this)); | |
} | |
} | |
SegmentStreamer.prototype.onTimeout = function(interest) | |
{ | |
console.log("Interest timed out: " + interest.getName().toUri()); | |
this.callback_(undefined, true); | |
// TODO: Re-express? | |
} | |
/* | |
* A SegmentStore stores segments until they are retrieved in order starting | |
* with segment 0. | |
*/ | |
var SegmentStore = function SegmentStore() | |
{ | |
// Each entry is an object where the key is the segment number and value is | |
// null if the segment number is requested or the data if received. | |
this.store = new SortedArray(); | |
this.maxRetrievedSegmentNumber = -1; | |
}; | |
/** | |
* Store the Data packet with the given segmentNumber. | |
* @param {number} segmentNumber The segment number of the packet. | |
* @param {Data} data The Data packet. | |
*/ | |
SegmentStore.prototype.storeContent = function(segmentNumber, data) | |
{ | |
// We don't expect to try to store a segment that has already been retrieved, | |
// but check anyway. | |
if (segmentNumber > this.maxRetrievedSegmentNumber) | |
this.store.set(segmentNumber, data); | |
}; | |
/* | |
* If the min segment number is this.maxRetrievedSegmentNumber + 1 and its value | |
* is not null, then delete from the store, return the entry with key and value, | |
* and update maxRetrievedSegmentNumber. Otherwise return null. | |
* @return {object} An object where "key" is the segment number and "value" is | |
* the Data object. However, if there is no next entry then return null. | |
*/ | |
SegmentStore.prototype.maybeRetrieveNextEntry = function() | |
{ | |
if (this.store.entries.length > 0 && this.store.entries[0].value != null && | |
this.store.entries[0].key == this.maxRetrievedSegmentNumber + 1) { | |
var entry = this.store.entries[0]; | |
this.store.removeAt(0); | |
++this.maxRetrievedSegmentNumber; | |
return entry; | |
} | |
else | |
return null; | |
}; | |
/* | |
* Return an array of the next segment numbers that need to be requested so that | |
* the total requested segments is totalRequestedSegments. If a segment store | |
* entry value is null, it is already requested and is not returned. If a | |
* segment number is returned, create a entry in the segment store with a null | |
* value. | |
* @param {number} totalRequestedSegments The total number of requested segments. | |
* @return {Array<number>} An array of the next segment number to request. The | |
* array may be empty. | |
*/ | |
SegmentStore.prototype.requestSegmentNumbers = function(totalRequestedSegments) | |
{ | |
// First, count how many are already requested. | |
var nRequestedSegments = 0; | |
for (var i = 0; i < this.store.entries.length; ++i) { | |
if (this.store.entries[i].value == null) { | |
++nRequestedSegments; | |
if (nRequestedSegments >= totalRequestedSegments) | |
// Already maxed out on requests. | |
return []; | |
} | |
} | |
var toRequest = []; | |
var nextSegmentNumber = this.maxRetrievedSegmentNumber + 1; | |
for (var i = 0; i < this.store.entries.length; ++i) { | |
var entry = this.store.entries[i]; | |
// Fill in the gap before the segment number in the entry. | |
while (nextSegmentNumber < entry.key) { | |
toRequest.push(nextSegmentNumber); | |
++nextSegmentNumber; | |
++nRequestedSegments; | |
if (nRequestedSegments >= totalRequestedSegments) | |
break; | |
} | |
if (nRequestedSegments >= totalRequestedSegments) | |
break; | |
nextSegmentNumber = entry.key + 1; | |
} | |
// We already filled in the gaps for the segments in the store. Continue after the last. | |
while (nRequestedSegments < totalRequestedSegments) { | |
toRequest.push(nextSegmentNumber); | |
++nextSegmentNumber; | |
++nRequestedSegments; | |
} | |
// Mark the new segment numbers as requested. | |
for (var i = 0; i < toRequest.length; ++i) | |
this.store.set(toRequest[i], null); | |
return toRequest; | |
}; | |
/* | |
* A SortedArray is an array of objects with key and value, where the key is an | |
* integer. | |
*/ | |
var SortedArray = function SortedArray() | |
{ | |
this.entries = []; | |
}; | |
/** | |
* Sort the entries by the integer "key". | |
*/ | |
SortedArray.prototype.sortEntries = function() | |
{ | |
this.entries.sort(function(a, b) { return a.key - b.key; }); | |
}; | |
/** | |
* Return the index number in this.entries of the object with a matching "key". | |
* @param {number} key The value of the object's "key". | |
* @returns {number} The index number, or -1 if not found. | |
*/ | |
SortedArray.prototype.indexOfKey = function(key) | |
{ | |
for (var i = 0; i < this.entries.length; ++i) { | |
if (this.entries[i].key == key) | |
return i; | |
} | |
return -1; | |
}; | |
/** | |
* Find or create an entry with the given "key" and set its "value". | |
* @param {integer} key The "key" of the entry object. | |
* @param {object} value The "value" of the entry object. | |
*/ | |
SortedArray.prototype.set = function(key, value) | |
{ | |
var i = this.indexOfKey(key); | |
if (i >= 0) { | |
this.entries[i].value = value; | |
return; | |
} | |
this.entries.push({ key: key, value: value}); | |
this.sortEntries(); | |
}; | |
/** | |
* Remove the entryin this.entries at the given index. | |
* @param {number} index The index of the entry to remove. | |
*/ | |
SortedArray.prototype.removeAt = function(index) | |
{ | |
this.entries.splice(index, 1); | |
}; | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment