Skip to content

Instantly share code, notes, and snippets.

@zhehaowang
Last active November 6, 2016 00:28
Show Gist options
  • Save zhehaowang/4e55452df6d4240dea7edc674a5a82a3 to your computer and use it in GitHub Desktop.
Save zhehaowang/4e55452df6d4240dea7edc674a5a82a3 to your computer and use it in GitHub Desktop.
In browser, buffer and play video served by ndnfs; NDN hackathon 2016
<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