Skip to content

Instantly share code, notes, and snippets.

@anuragteapot
Last active March 29, 2020 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anuragteapot/288c51d8edd7c0743a73b7fa4d59fcf2 to your computer and use it in GitHub Desktop.
Save anuragteapot/288c51d8edd7c0743a73b7fa4d59fcf2 to your computer and use it in GitHub Desktop.
Media Source Extension
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this;
var args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
class MediaSourceApi {
/**
*
* Constructor
*
* @method constructor
*
* @param {HTMLVideoElement} videoElement
* @param {string} src
* @param {Boolean} autoPlay
*
* @returns {VoidFunction}
*
* @public
*
*/
constructor(videoElement, src, autoPlay) {
/**
* @private { HTMLVideoElement }
*/
this.video = videoElement;
/**
* @private { Boolean }
*/
this.autoPlay = autoPlay;
/**
* @private { String }
*/
this.assetURL = src;
/**
* @private { Object }
*/
this.queue = {};
/**
* @private { Boolean }
*/
this.video.muted = false;
/**
* @private { String }
*/
this.mimeCodec = 'video/mp4; codecs="avc1.64001e, mp4a.40.2"';
/**
* @private { MediaSource }
*/
this.mediaSource = null;
/**
* @private { number }
*/
this.segmentNumbers;
/**
* @private { number }
*/
this.segmentLength = 0;
/**
* @private { number }
*
* */
this.segmentDuration = 0;
/**
* @private { number }
*/
this.segmentCurrent = 0;
/**
* @private { number }
*/
this.segmentFetched = 0;
/**
* @private { Array }
*/
this.requestedSegments = [];
/**
* @private { Double }
*/
this.bufferedStartTime = 0.0;
/**
* @private { Double }
*/
this.bufferedEndTime = 0.0;
/**
* @private { number}
*/
this.segmentBufferDiff = 3;
/**
* @private { SourceBuffer }
*/
this.sourceBuffer = null;
}
/**
*
* This function initialize the player.
*
* @method init
*
* @param {number} time
*
* @returns {this} this
*
* @public
*
*/
init() {
if (
"MediaSource" in window &&
MediaSource.isTypeSupported(this.mimeCodec)
) {
this.mediaSource = new MediaSource();
this.video.src = URL.createObjectURL(this.mediaSource);
this.addEvents("MEDIA_SOURCE");
} else {
console.warn("Unsupported MIME type or codec: ", this.mimeCodec);
console.warn("Playing with default HTML5 Video Player");
this.video.src = this.assetURL;
}
return this;
}
/**
*
* This function flush data.
*
* @method flushData
*
* @returns {VoidFunction}
*
* @public
*
*/
flushData() {
this.mediaSource = null;
this.segmentNumbers;
this.segmentLength = 0;
this.segmentDuration = 0;
this.segmentCurrent = 0;
this.segmentFetched = 0;
this.requestedSegments = [];
this.bufferedStartTime = 0.0;
this.bufferedEndTime = 0.0;
this.segmentBufferDiff = 3;
this.sourceBuffer = null;
}
/**
*
* This function attaches events.
*
* @method addEvents
*
* @returns {VoidFunction}
*
* @public
*
*/
addEvents(type) {
if (!type) {
return new Error("Types is required");
}
if (type == "VIDEO") {
this.video.addEventListener(
"seeking",
debounce(this.handleSeek, 500),
false
);
this.video.addEventListener("seeked", this.videoOnSeeked, false);
this.video.addEventListener("loadeddata", this.videOnLoadedData, false);
this.video.addEventListener("timeupdate", this.checkBuffer, false);
this.video.addEventListener("canplay", this.onCanPlay, false);
} else if (type == "MEDIA_SOURCE") {
this.mediaSource.addEventListener(
"sourceopen",
this.mediaSourceOpen,
false
);
this.mediaSource.addEventListener(
"sourceended",
this.mediaSourceEnded,
false
);
this.mediaSource.addEventListener(
"sourceclose",
this.mediaSourceClose,
false
);
} else if (type == "SOURCE_BUFFER") {
this.sourceBuffer.addEventListener(
"update",
this.sourceBufferOnUpdate,
false
);
this.sourceBuffer.addEventListener(
"updatestart",
this.sourceBufferUpdateStart,
false
);
this.sourceBuffer.addEventListener(
"updateend",
this.sourceBufferOnUpdateEnd
);
this.sourceBuffer.addEventListener(
"abort",
this.sourceBufferAbort,
false
);
this.sourceBuffer.addEventListener(
"error",
this.sourceBufferError,
false
);
}
}
/**
*
* This function remove events.
*
* @method removeEvents
*
* @returns {VoidFunction}
*
* @public
*
*/
removeEvents(type) {
if (!type) {
return new Error("Types is required");
}
if (type == "VIDEO") {
this.video.removeEventListener("seeking", debounce(this.handleSeek, 500));
this.video.removeEventListener("seeked", this.videoOnSeeked);
this.video.removeEventListener("loadeddata", this.videOnLoadedData);
this.video.removeEventListener("timeupdate", this.checkBuffer);
this.video.removeEventListener("canplay", this.onCanPlay);
} else if (type == "MEDIA_SOURCE") {
this.mediaSource.removeEventListener("sourceopen", this.mediaSourceOpen);
this.mediaSource.removeEventListener(
"sourceended",
this.mediaSourceEnded
);
this.mediaSource.removeEventListener(
"sourceclose",
this.mediaSourceClose
);
} else if (type == "SOURCE_BUFFER") {
this.sourceBuffer.removeEventListener(
"update",
this.sourceBufferOnUpdate
);
this.sourceBuffer.removeEventListener(
"updatestart",
this.sourceBufferUpdateStart
);
this.sourceBuffer.removeEventListener(
"updateend",
this.sourceBufferOnUpdateEnd
);
this.sourceBuffer.removeEventListener("abort", this.sourceBufferAbort);
this.sourceBuffer.removeEventListener("error", this.sourceBufferError);
}
}
/**
*
* Use to change video src..
*
* @method change
*
* @param {String} src
*
* @returns {VoidFunction}
*
* @async
*
* @public
*
*/
async change(src) {
this.video.pause();
await this.removeSourceBuffer(0, Infinity);
this.sourceBuffer.appendWindowStart = 0;
this.sourceBuffer.appendWindowEnd = Infinity;
this.removeEvents("VIDEO");
this.removeEvents("SOURCE_BUFFER");
this.removeEvents("MEDIA_SOURCE");
if (this.mediaSource.readyState == "open") {
this.mediaSource.endOfStream();
}
this.assetURL = src;
this.flushData();
this.init();
}
/**
*
* Use to destroy video.
*
* @method destroy
*
* @param {String} src
*
* @returns {VoidFunction}
*
* @async
*
* @public
*
*/
async destroy() {
this.video.pause();
await this.removeSourceBuffer(0, Infinity);
this.sourceBuffer.appendWindowStart = 0;
this.sourceBuffer.appendWindowEnd = Infinity;
this.removeEvents("VIDEO");
this.removeEvents("SOURCE_BUFFER");
this.removeEvents("MEDIA_SOURCE");
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
if (this.mediaSource.readyState == "open") {
this.mediaSource.endOfStream();
}
this.video.src = null;
this.video.removeAttribute("src");
}
/**
*
* Use to end video.
*
* @method end
*
* @returns {VoidFunction}
*
* @async
*
* @public
*
*/
async end() {
await this.removeSourceBuffer(0, Infinity);
this.sourceBuffer.appendWindowStart = 0;
this.sourceBuffer.appendWindowEnd = Infinity;
if (this.mediaSource.readyState == "open") {
this.mediaSource.endOfStream();
}
}
/**
*
* This function gives start and end length of media to be fetched.
*
* @method mediaSourceClose
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
mediaSourceClose(event) {
console.log(event.type);
}
/**
*
* This function gives start and end length of media to be fetched.
*
* @method mediaSourceEnded
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
mediaSourceEnded(event) {
console.log(event.type);
}
/**
*
* This function gives start and end length of media to be fetched.
*
* @method mediaSourceOpen
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
mediaSourceOpen = async event => {
if (this.mediaSource.sourceBuffers.length > 0) return;
this.addEvents("VIDEO");
this.sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeCodec);
this.addEvents("SOURCE_BUFFER");
const fileLength = await this.getFileLength(this.assetURL);
this.setFileLength(fileLength);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method sourceBufferOnUpdateEnd
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
sourceBufferOnUpdateEnd = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method sourceBufferOnUpdate
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
sourceBufferOnUpdate = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method sourceBufferAbort
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
sourceBufferAbort = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method sourceBufferError
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
sourceBufferError = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method sourceBufferUpdateStart
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
sourceBufferUpdateStart = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method videoOnSeeked
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
videoOnSeeked = event => {
// console.log(event.type);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method videOnLoadedData
*
* @param {Event} event
*
* @returns {VoidFunction}
*
* @public
*
*/
videOnLoadedData = event => {
// console.log(event.type);
};
/**
*
* This function play the video.
*
* @method onCanPlay
*
* @returns {VoidFunction}
*
* @public
*
*/
onCanPlay = () => {
this.segmentDuration = this.video.duration / this.segmentNumbers;
if (this.autoPlay) {
const promise = this.video.play();
if (promise !== undefined) {
promise
.then(_ => {
console.log(_);
})
.catch(error => {
console.log(error);
});
}
}
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method setFileLength
*
* @param {number} fileLength
*
* @returns {VoidFunction}
*
* @public
*
*/
setFileLength = async fileLength => {
this.segmentNumbers = parseInt((fileLength / 1024 / 1024).toFixed(0));
for (let i = 0; i < this.segmentNumbers; ++i) {
this.requestedSegments[i] = false;
}
this.segmentLength = Math.round(fileLength / this.segmentNumbers);
const segmentInfo = this.getSegmentInfoByNumber(0);
try {
const responseData = await this.fetchRange(
this.assetURL,
segmentInfo.start,
segmentInfo.end
);
await this.appendSegment(responseData);
this.requestedSegments[0] = true;
} catch (err) {
console.log(err);
}
};
/**
*
* This function remove source buffer.
*
* @method removeSourceBuffer
*
* @param {number} start
* @param {number} end
*
* @returns {Promise}
*
* @public
*
*/
removeSourceBuffer(start, end) {
return new Promise((resolve, reject) => {
this.sourceBuffer.remove(start, end);
this.sourceBuffer.addEventListener("updateend", resolve);
this.sourceBuffer.addEventListener("error", reject);
});
}
/**
*
* This function get the length of file.
*
* @method getFileLength
*
* @param {string} url
*
* @returns {Promise}
*
* @public
*
*/
getFileLength = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("head", url);
xhr.onload = () => {
resolve(xhr.getResponseHeader("content-length"));
};
xhr.onerror = err => {
reject(err);
};
xhr.send();
});
};
/**
*
* This function makes https request to get data.
*
* @method fetchRange
*
* @param {string} url
* @param {number} start
* @param {number} end
*
* @returns {Promise}
*
* @public
*
*/
fetchRange = (url, start, end) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("get", url);
xhr.responseType = "arraybuffer";
xhr.setRequestHeader("Range", "bytes=" + start + "-" + end);
xhr.onload = async () => {
resolve(new Uint8Array(xhr.response));
};
xhr.onerror = err => {
reject(err);
};
xhr.send();
});
};
/**
*
* This function append buffer to source buffer.
*
* @method appendSegment
*
* @param {ArrayBuffer} chunk
*
* @returns {Promise}
*
* @public
*
*/
appendSegment = chunk => {
return new Promise((resolve, reject) => {
const file = new Blob([chunk], { type: "video/mp4" });
const reader = new FileReader();
// const duration = this.mediaSource.duration;
// if (this.segmentCurrent != 0 && duration != NaN) {
// console.log("Duration: " + duration);
// this.sourceBuffer.timestampOffset = duration;
// }
reader.onload = event => {
const segmentChunk = new Uint8Array(event.target.result);
if (segmentChunk === null) {
mediaSource.endOfStream("network");
return reject(new Error("Network Error"));
}
this.sourceBuffer.appendBuffer(new Uint8Array(event.target.result));
this.sourceBuffer.addEventListener("updateend", resolve);
this.sourceBuffer.addEventListener("error", reject);
};
reader.readAsArrayBuffer(file);
});
};
/**
*
* This function check for buffer and perform various operaions.
*
* @method checkBuffer
*
* @returns {VoidFunction}
*
* @public
*
* @async
*
*/
checkBuffer = async () => {
if (this.sourceBuffer.updating) {
return;
}
if (this.mediaSource.readyState !== "open") {
return;
}
if (this.sourceBuffer.mode == "PARSING_MEDIA_SEGMENT") {
return;
}
if (this.video.buffered.length >= 1) {
this.bufferedEndTime = this.video.buffered.end(
this.video.buffered.length - 1
);
this.bufferedStartTime = this.video.buffered.start(
this.video.buffered.length - 1
);
}
const segmentInfo = this.getSegmentInfoByTime(this.bufferedStartTime);
if (this.requestedSegments[segmentInfo.segment] == true) {
for (let i = 0; i < segmentInfo.segment; i++) {
this.requestedSegments[i] = false;
}
}
const nextSegment = this.getNextSegment();
if (nextSegment == this.segmentNumbers && this.haveAllSegments()) {
if (this.mediaSource.readyState == "open") {
this.mediaSource.endOfStream();
}
this.video.removeEventListener("timeupdate", this.checkBuffer);
} else if (this.shouldFetchNextSegment(nextSegment)) {
this.requestedSegments[nextSegment] = true;
this.segmentCurrent = nextSegment;
const segmentInfo = this.getSegmentInfoByNumber(nextSegment);
console.log(segmentInfo);
try {
const responseData = await this.fetchRange(
this.assetURL,
segmentInfo.start,
segmentInfo.end
);
await this.appendSegment(responseData);
} catch (err) {
this.requestedSegments[nextSegment] = false;
console.log(err);
}
}
};
/**
*
* This function which handle seek.
*
* @method handleSeek
*
* @returns {VoidFunction}
*
* @async
*
* @public
*
*/
handleSeek = async () => {
if (this.sourceBuffer.updating) {
return;
}
if (this.mediaSource.readyState !== "open") {
return;
}
if (this.sourceBuffer.mode == "PARSING_MEDIA_SEGMENT") {
return;
}
if (
this.bufferedEndTime > this.video.currentTime &&
this.bufferedStartTime < this.video.currentTime
) {
return;
}
const nextSegment = this.getNextSegment();
if (nextSegment === this.segmentNumbers && this.haveAllSegments()) {
if (this.mediaSource.readyState == "open") {
this.mediaSource.endOfStream();
}
this.video.removeEventListener("timeupdate", this.checkBuffer);
} else {
console.log("Handle out seek");
console.log(this);
if (this.shouldFetchNextSegment(nextSegment)) {
console.log("yes");
}
// this.sourceBuffer.appendWindowStart = Math.floor(this.video.currentTime);
// this.sourceBuffer.appendWindowEnd = Infinity;
// this.segmentCurrent = nextSegment;
// this.video.removeEventListener("timeupdate", this.checkBuffer);
// for (let segment = 0; segment <= nextSegment; segment++) {
// if (this.shouldFetchNextSegment(segment)) {
// this.requestedSegments[segment] = true;
// const segmentInfo = this.getSegmentInfoByNumber(segment);
// try {
// const responseData = await this.fetchRange(
// this.assetURL,
// segmentInfo.start,
// segmentInfo.end
// );
// await this.appendSegment(responseData);
// } catch (err) {
// console.log(err);
// }
// }
// }
// this.video.addEventListener("timeupdate", this.checkBuffer);
}
};
fetch = async time => {
const segmentInfo = this.getSegmentInfoByTime(parseInt(time));
this.video.pause();
// console.log(this.sourceBuffer.buffered);
// console.log(this.video.buffered);
// await this.removeSourceBuffer(0, segmentInfo.timeStart);
// console.log(segmentInfo);
this.sourceBuffer.appendWindowStart = segmentInfo.timeStart;
// this.sourceBuffer.timestampOffset = segmentInfo.timeStart;
try {
const responseData = await this.fetchRange(
this.assetURL,
segmentInfo.start,
segmentInfo.end
);
await this.appendSegment(responseData);
this.video.currentTime = time;
} catch (err) {
console.log(err);
}
this.video.play();
};
/**
*
* This function to get next segment need to be fetched.
*
* @method getNextSegment
*
* @returns {number}
*
* @public
*
*/
getNextSegment = () => {
const s1 =
((Math.ceil(this.video.currentTime) / Math.floor(this.segmentDuration)) |
0) +
1;
const currentSegmentInfo = this.getSegmentInfoByTime(
this.video.currentTime
);
const s2 =
this.segmentCurrent + 1 <= this.segmentNumbers &&
this.segmentCurrent - currentSegmentInfo.segment <= this.segmentBufferDiff
? this.segmentCurrent + 1
: this.segmentCurrent;
return Math.max(s1, s2);
};
/**
*
* This function check every segment is fetched or not/
*
* @method haveAllSegments
*
* @returns {Boolean}
*
* @public
*
*/
haveAllSegments = () => {
return this.requestedSegments.every(val => {
return !!val;
});
};
/**
*
* This to check whether should fetch next segment or not ?
*
* @method shouldFetchNextSegment
*
* @param {number} nextSegment
*
* @returns {Boolean}
*
* @public
*
*/
shouldFetchNextSegment = nextSegment => {
if (
this.segmentCurrent <= 2 &&
this.requestedSegments[nextSegment] == false
) {
return true;
}
return (
this.video.currentTime >=
Math.floor(this.segmentDuration) * nextSegment * 0.1 &&
this.requestedSegments[nextSegment] == false
);
};
/**
*
* This function gives start and end length of media to be fetched.
*
* @method getSegmentInfoByTime
*
* @param {number} time
*
* @returns {{ segment: number, timeStart: number, timeEnd:number, start:number, end :number}}
*
* @public
*
*/
getSegmentInfoByTime(time) {
const segmentNumber = Math.floor(time / this.segmentDuration);
return this.getSegmentInfoByNumber(segmentNumber);
}
/**
*
* This function gives start and end length of media to be fetched.
*
* @method getSegmentInfoByNumber
*
* @param {number} time
*
* @returns {{ segment: number, timeStart: number, timeEnd:number, start:number, end :number}}
*
* @public
*
*/
getSegmentInfoByNumber(segmentNumber) {
return {
segment: segmentNumber,
timeStart:
Math.floor(Math.floor(segmentNumber) * this.segmentDuration) +
(segmentNumber == 0 ? 0 : 1),
timeEnd: Math.floor(Math.floor(segmentNumber + 1) * this.segmentDuration),
start:
Math.floor(segmentNumber) * this.segmentLength +
(segmentNumber == 0 ? 0 : 1),
end: Math.floor(segmentNumber + 1) * this.segmentLength
};
}
}
if (typeof window != undefined) {
window.FlexVideoPlayer = MediaSourceApi;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment