(function(window){ var urlCount = 0, NativeMediaSource = window.MediaSource || window.WebKitMediaSource || {}, nativeUrl = window.URL || {}, EventEmitter, flvCodec = /video\/flv(;\s*codecs=["']vp6,aac["'])?$/, objectUrlPrefix = 'blob:vjs-media-source/'; EventEmitter = function(){}; EventEmitter.prototype.init = function(){ this.listeners = []; }; EventEmitter.prototype.addEventListener = function(type, listener){ if (!this.listeners[type]){ this.listeners[type] = []; } this.listeners[type].unshift(listener); }; EventEmitter.prototype.removeEventListener = function(type, listener){ var listeners = this.listeners[type], i = listeners.length; while (i--) { if (listeners[i] === listener) { return listeners.splice(i, 1); } } }; EventEmitter.prototype.trigger = function(event){ var listeners = this.listeners[event.type] || [], i = listeners.length; while (i--) { listeners[i](event); } }; // extend the media source APIs // Media Source videojs.MediaSource = function(){ var self = this; videojs.MediaSource.prototype.init.call(this); this.sourceBuffers = []; this.readyState = 'closed'; this.listeners = { sourceopen: [function(event){ // find the swf where we will push media data self.swfObj = document.getElementById(event.swfId); self.readyState = 'open'; // trigger load events if (self.swfObj) { self.swfObj.vjs_load(); } }], webkitsourceopen: [function(event){ self.trigger({ type: 'sourceopen' }); }] }; }; videojs.MediaSource.prototype = new EventEmitter(); /** * The maximum size in bytes for append operations to the video.js * SWF. Calling through to Flash blocks and can be expensive so * tuning this parameter may improve playback on slower * systems. There are two factors to consider: * - Each interaction with the SWF must be quick or you risk dropping * video frames. To maintain 60fps for the rest of the page, each append * cannot take longer than 16ms. Given the likelihood that the page will * be executing more javascript than just playback, you probably want to * aim for ~8ms. * - Bigger appends significantly increase throughput. The total number of * bytes over time delivered to the SWF must exceed the video bitrate or * playback will stall. * * The default is set so that a 4MB/s stream should playback * without stuttering. */ videojs.MediaSource.BYTES_PER_SECOND_GOAL = 4 * 1024 * 1024; videojs.MediaSource.TICKS_PER_SECOND = 60; // create a new source buffer to receive a type of media data videojs.MediaSource.prototype.addSourceBuffer = function(type){ var sourceBuffer; // if this is an FLV type, we'll push data to flash if (flvCodec.test(type)) { // Flash source buffers sourceBuffer = new videojs.SourceBuffer(this); } else if (this.nativeSource) { // native source buffers sourceBuffer = this.nativeSource.addSourceBuffer.apply(this.nativeSource, arguments); } else { throw new Error('NotSupportedError (Video.js)'); } this.sourceBuffers.push(sourceBuffer); return sourceBuffer; }; videojs.MediaSource.prototype.endOfStream = function(){ this.swfObj.vjs_endOfStream(); this.readyState = 'ended'; }; // store references to the media sources so they can be connected // to a video element (a swf object) videojs.mediaSources = {}; // provide a method for a swf object to notify JS that a media source is now open videojs.MediaSource.open = function(msObjectURL, swfId){ var mediaSource = videojs.mediaSources[msObjectURL]; if (mediaSource) { mediaSource.trigger({ type: 'sourceopen', swfId: swfId }); } else { throw new Error('Media Source not found (Video.js)'); } }; // Source Buffer videojs.SourceBuffer = function(source){ var self = this, // byte arrays queued to be appended buffer = [], // the total number of queued bytes bufferSize = 0, scheduleTick = function(func) { // Chrome doesn't invoke requestAnimationFrame callbacks // in background tabs, so use setTimeout. window.setTimeout(func, Math.ceil(1000 / videojs.MediaSource.TICKS_PER_SECOND)); }, append = function() { var chunk, i, length, payload, maxSize, binary = ''; if (!buffer.length) { // do nothing if the buffer is empty return; } if (document.hidden) { // When the document is hidden, the browser will likely // invoke callbacks less frequently than we want. Just // append a whole second's worth of data. It doesn't // matter if the video janks, since the user can't see it. maxSize = videojs.MediaSource.BYTES_PER_SECOND_GOAL; } else { maxSize = Math.ceil(videojs.MediaSource.BYTES_PER_SECOND_GOAL/ videojs.MediaSource.TICKS_PER_SECOND); } // concatenate appends up to the max append size payload = new Uint8Array(Math.min(maxSize, bufferSize)); i = payload.byteLength; while (i) { chunk = buffer[0].subarray(0, i); payload.set(chunk, payload.byteLength - i); // requeue any bytes that won't make it this round if (chunk.byteLength < buffer[0].byteLength) { buffer[0] = buffer[0].subarray(i); } else { buffer.shift(); } i -= chunk.byteLength; } bufferSize -= payload.byteLength; // schedule another append if necessary if (bufferSize !== 0) { scheduleTick(append); } else { self.trigger({ type: 'updateend' }); } // base64 encode the bytes for (i = 0, length = payload.byteLength; i < length; i++) { binary += String.fromCharCode(payload[i]); } b64str = window.btoa(binary); // bypass normal ExternalInterface calls and pass xml directly // EI can be slow by default self.source.swfObj.CallFunction('' + b64str + ''); }; videojs.SourceBuffer.prototype.init.call(this); this.source = source; // accept video data and pass to the video (swf) object this.appendBuffer = function(uint8Array){ if (buffer.length === 0) { scheduleTick(append); } this.trigger({ type: 'update' }); buffer.push(uint8Array); bufferSize += uint8Array.byteLength; }; // reset the parser and remove any data queued to be sent to the swf this.abort = function() { buffer = []; bufferSize = 0; this.source.swfObj.vjs_abort(); }; }; videojs.SourceBuffer.prototype = new EventEmitter(); // URL videojs.URL = { createObjectURL: function(object){ var url = objectUrlPrefix + urlCount; urlCount++; // setup the mapping back to object videojs.mediaSources[url] = object; return url; } }; // plugin videojs.plugin('mediaSource', function(options){ var player = this; player.on('loadstart', function(){ var url = player.currentSrc(), trigger = function(event){ mediaSource.trigger(event); }, mediaSource; if (player.techName === 'Html5' && url.indexOf(objectUrlPrefix) === 0) { // use the native media source implementation mediaSource = videojs.mediaSources[url]; if (!mediaSource.nativeUrl) { // initialize the native source mediaSource.nativeSource = new NativeMediaSource(); mediaSource.nativeSource.addEventListener('sourceopen', trigger, false); mediaSource.nativeSource.addEventListener('webkitsourceopen', trigger, false); mediaSource.nativeUrl = nativeUrl.createObjectURL(mediaSource.nativeSource); } player.src(mediaSource.nativeUrl); } }); }); })(this);