From ce3e7d82f6758578af7f05d2b7bcb4a10e317fea Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Tue, 12 Aug 2014 16:21:55 -0700 Subject: [PATCH 1/3] Added base sourcehandler code Refactoring of HTML5 source handler pattern Updated source handlers to share selection logic Created a mixin for source handler functions and update Flash to have source handlers --- build/source-loader.js | 1 + src/js/media/flash.js | 95 +++++++++++++++++----------- src/js/media/flash.rtmp.js | 88 ++++++++++++++++++++++++++ src/js/media/html5.js | 126 ++++++++++++++++++++++++++++++------- src/js/media/media.js | 94 ++++++++++++++++++++++++++- src/js/player.js | 9 ++- 6 files changed, 351 insertions(+), 62 deletions(-) create mode 100644 src/js/media/flash.rtmp.js diff --git a/build/source-loader.js b/build/source-loader.js index 8a451cf772..40626c5835 100644 --- a/build/source-loader.js +++ b/build/source-loader.js @@ -44,6 +44,7 @@ var sourceFiles = [ "src/js/media/media.js", "src/js/media/html5.js", "src/js/media/flash.js", + "src/js/media/flash.rtmp.js", "src/js/media/loader.js", "src/js/tracks.js", "src/js/json.js", diff --git a/src/js/media/flash.js b/src/js/media/flash.js index 038ae39721..ff0c8404d9 100644 --- a/src/js/media/flash.js +++ b/src/js/media/flash.js @@ -65,14 +65,9 @@ vjs.Flash = vjs.MediaTechController.extend({ // If source was supplied pass as a flash var. if (source) { - if (source.type && vjs.Flash.isStreamingType(source.type)) { - var parts = vjs.Flash.streamToParts(source.src); - flashVars['rtmpConnection'] = encodeURIComponent(parts.connection); - flashVars['rtmpStream'] = encodeURIComponent(parts.stream); - } - else { - flashVars['src'] = encodeURIComponent(vjs.getAbsoluteURL(source.src)); - } + this.ready(function(){ + this.setSource(source); + }); } // Add placeholder to player div @@ -124,15 +119,14 @@ vjs.Flash.prototype.src = function(src){ return this['currentSrc'](); } - if (vjs.Flash.isStreamingSrc(src)) { - src = vjs.Flash.streamToParts(src); - this.setRtmpConnection(src.connection); - this.setRtmpStream(src.stream); - } else { - // Make sure source URL is absolute. - src = vjs.getAbsoluteURL(src); - this.el_.vjs_src(src); - } + // Setting src through `src` not `setSrc` will be deprecated + return this.setSrc(src); +}; + +vjs.Flash.prototype.setSrc = function(src){ + // Make sure source URL is absolute. + src = vjs.getAbsoluteURL(src); + this.el_.vjs_src(src); // Currently the SWF doesn't autoplay if you load a source later. // e.g. Load player w/ no source, wait 2s, set src. @@ -158,17 +152,11 @@ vjs.Flash.prototype['currentTime'] = function(time){ }; vjs.Flash.prototype['currentSrc'] = function(){ - var src = this.el_.vjs_getProperty('currentSrc'); - // no src, check and see if RTMP - if (src == null) { - var connection = this['rtmpConnection'](), - stream = this['rtmpStream'](); - - if (connection && stream) { - src = vjs.Flash.streamFromParts(connection, stream); - } + if (this.currentSource_) { + return this.currentSource_.src; + } else { + return this.el_.vjs_getProperty('currentSrc'); } - return src; }; vjs.Flash.prototype.load = function(){ @@ -229,19 +217,59 @@ vjs.Flash.isSupported = function(){ // return swfobject.hasFlashPlayerVersion('10'); }; -vjs.Flash.canPlaySource = function(srcObj){ +// Add Source Handler pattern functions to this tech +vjs.MediaTechController.withSourceHandlers(vjs.Flash); + +/** + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.nativeSourceHandler = {}; + +/** + * Check Flash can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Flash.nativeSourceHandler.canHandleSource = function(source){ var type; - if (!srcObj.type) { + if (!source.type) { return ''; } - type = srcObj.type.replace(/;.*/,'').toLowerCase(); - if (type in vjs.Flash.formats || type in vjs.Flash.streamingFormats) { + // Strip code information from the type because we don't get that specific + type = source.type.replace(/;.*/,'').toLowerCase(); + + if (type in vjs.Flash.formats) { return 'maybe'; } + + return ''; +}; + +/** + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.nativeSourceHandler.handleSource = function(source, tech){ + tech.setSrc(source.src); }; +/** + * Clean up the source handler when disposing the player or switching sources.. + * (no cleanup is needed when supporting the format natively) + */ +vjs.Flash.nativeSourceHandler.dispose = function(){}; + +// Register the native source handler +vjs.Flash.registerSourceHandler(vjs.Flash.nativeSourceHandler); + vjs.Flash.formats = { 'video/flv': 'FLV', 'video/x-flv': 'FLV', @@ -249,11 +277,6 @@ vjs.Flash.formats = { 'video/m4v': 'MP4' }; -vjs.Flash.streamingFormats = { - 'rtmp/mp4': 'MP4', - 'rtmp/flv': 'FLV' -}; - vjs.Flash['onReady'] = function(currSwf){ var el, player; diff --git a/src/js/media/flash.rtmp.js b/src/js/media/flash.rtmp.js new file mode 100644 index 0000000000..f42063752c --- /dev/null +++ b/src/js/media/flash.rtmp.js @@ -0,0 +1,88 @@ +vjs.Flash.streamingFormats = { + 'rtmp/mp4': 'MP4', + 'rtmp/flv': 'FLV' +}; + +vjs.Flash.streamFromParts = function(connection, stream) { + return connection + '&' + stream; +}; + +vjs.Flash.streamToParts = function(src) { + var parts = { + connection: '', + stream: '' + }; + + if (! src) { + return parts; + } + + // Look for the normal URL separator we expect, '&'. + // If found, we split the URL into two pieces around the + // first '&'. + var connEnd = src.indexOf('&'); + var streamBegin; + if (connEnd !== -1) { + streamBegin = connEnd + 1; + } + else { + // If there's not a '&', we use the last '/' as the delimiter. + connEnd = streamBegin = src.lastIndexOf('/') + 1; + if (connEnd === 0) { + // really, there's not a '/'? + connEnd = streamBegin = src.length; + } + } + parts.connection = src.substring(0, connEnd); + parts.stream = src.substring(streamBegin, src.length); + + return parts; +}; + +vjs.Flash.isStreamingType = function(srcType) { + return srcType in vjs.Flash.streamingFormats; +}; + +// RTMP has four variations, any string starting +// with one of these protocols should be valid +vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i; + +vjs.Flash.isStreamingSrc = function(src) { + return vjs.Flash.RTMP_RE.test(src); +}; + +/** + * A source handler for RTMP urls + * @type {Object} + */ +vjs.Flash.rtmpSourceHandler = {}; + +/** + * Check Flash can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Flash.rtmpSourceHandler.canHandleSource = function(source){ + if (vjs.Flash.isStreamingType(source.type) || vjs.Flash.isStreamingSrc(source.src)) { + return 'maybe'; + } + + return ''; +}; + +/** + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.rtmpSourceHandler.handleSource = function(source, tech){ + var srcParts = vjs.Flash.streamToParts(source.src); + + tech.setRtmpConnection(srcParts.connection); + tech.setRtmpStream(srcParts.stream); +}; + +// Register the native source handler +vjs.Flash.registerSourceHandler(vjs.Flash.rtmpSourceHandler); diff --git a/src/js/media/html5.js b/src/js/media/html5.js index b5821d59a7..0d8f60ca20 100644 --- a/src/js/media/html5.js +++ b/src/js/media/html5.js @@ -12,22 +12,8 @@ vjs.Html5 = vjs.MediaTechController.extend({ /** @constructor */ init: function(player, options, ready){ - // volume cannot be changed from 1 on iOS - this['featuresVolumeControl'] = vjs.Html5.canControlVolume(); - - // just in case; or is it excessively... - this['featuresPlaybackRate'] = vjs.Html5.canControlPlaybackRate(); - - // In iOS, if you move a video element in the DOM, it breaks video playback. - this['movingMediaElementInDOM'] = !vjs.IS_IOS; - - // HTML video is able to automatically resize when going to fullscreen - this['featuresFullscreenResize'] = true; - - // HTML video supports progress events - this['featuresProgressEvents'] = true; - vjs.MediaTechController.call(this, player, options, ready); + this.setupTriggers(); var source = options['source']; @@ -241,16 +227,25 @@ vjs.Html5.prototype.enterFullScreen = function(){ video.webkitEnterFullScreen(); } }; + vjs.Html5.prototype.exitFullScreen = function(){ this.el_.webkitExitFullScreen(); }; + + vjs.Html5.prototype.src = function(src) { if (src === undefined) { return this.el_.src; } else { - this.el_.src = src; + // Setting src through `src` instead of `setSrc` will be deprecated + this.setSrc(src); } }; + +vjs.Html5.prototype.setSrc = function(src) { + this.el_.src = src; +}; + vjs.Html5.prototype.load = function(){ this.el_.load(); }; vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; }; @@ -279,10 +274,13 @@ vjs.Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val vjs.Html5.prototype.networkState = function(){ return this.el_.networkState; }; -/* HTML5 Support Testing ---------------------------------------------------- */ +/** + * Check if HTML5 video is supported by this browser/device + * @return {Boolean} + */ vjs.Html5.isSupported = function(){ - // ie9 with no Media Player is a LIAR! (#984) + // IE9 with no Media Player is a LIAR! (#984) try { vjs.TEST_VID['volume'] = 0.5; } catch (e) { @@ -292,31 +290,111 @@ vjs.Html5.isSupported = function(){ return !!vjs.TEST_VID.canPlayType; }; -vjs.Html5.canPlaySource = function(srcObj){ +// Add Source Handler pattern functions to this tech +vjs.MediaTechController.withSourceHandlers(vjs.Html5); + +/** + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * @param {Object} source The source object + * @param {vjs.Html5} tech The instance of the HTML5 tech + */ +vjs.Html5.nativeSourceHandler = {}; + +/** + * Check if the video element can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Html5.nativeSourceHandler.canHandleSource = function(source){ + var can = ''; + // IE9 on Windows 7 without MediaPlayer throws an error here // https://github.com/videojs/video.js/issues/519 try { - return !!vjs.TEST_VID.canPlayType(srcObj.type); + can = !!vjs.TEST_VID.canPlayType(source.type); } catch(e) { - return ''; + can = ''; } - // TODO: Check Type - // If no Type, check ext - // Check Media Type + // TODO: If no type, check the extension + + return can; }; +/** + * Pass the source to the video element + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Html5} tech The instance of the Html5 tech + */ +vjs.Html5.nativeSourceHandler.handleSource = function(source, tech){ + tech.setSrc(source.src); +}; + +/** + * Clean up the source handler when disposing the player or switching sources.. + * (no cleanup is needed when supporting the format natively) + */ +vjs.Html5.nativeSourceHandler.dispose = function(){}; + +// Register the native source handler +vjs.Html5.registerSourceHandler(vjs.Html5.nativeSourceHandler); + +/** + * Check if the volume can be changed in this browser/device. + * Volume cannot be changed in a lot of mobile devices. + * Specifically, it can't be changed from 1 on iOS. + * @return {Boolean} + */ vjs.Html5.canControlVolume = function(){ var volume = vjs.TEST_VID.volume; vjs.TEST_VID.volume = (volume / 2) + 0.1; return volume !== vjs.TEST_VID.volume; }; +/** + * Check if playbackRate is supported in this browser/device. + * @return {[type]} [description] + */ vjs.Html5.canControlPlaybackRate = function(){ var playbackRate = vjs.TEST_VID.playbackRate; vjs.TEST_VID.playbackRate = (playbackRate / 2) + 0.1; return playbackRate !== vjs.TEST_VID.playbackRate; }; +/** + * Set the tech's volume control support status + * @type {Boolean} + */ +vjs.Html5.prototype['featuresVolumeControl'] = vjs.Html5.canControlVolume(); + +/** + * Set the tech's playbackRate support status + * @type {Boolean} + */ +vjs.Html5.prototype['featuresPlaybackRate'] = vjs.Html5.canControlPlaybackRate(); + +/** + * Set the tech's status on moving the video element. + * In iOS, if you move a video element in the DOM, it breaks video playback. + * @type {Boolean} + */ +vjs.Html5.prototype['movingMediaElementInDOM'] = !vjs.IS_IOS; + +/** + * Set the the tech's fullscreen resize support status. + * HTML video is able to automatically resize when going to fullscreen. + * (No longer appears to be used. Can probably be removed.) + */ +vjs.Html5.prototype['featuresFullscreenResize'] = true; + +/** + * Set the tech's progress event support status + * (this disables the manual progress events of the MediaTechController) + */ +vjs.Html5.prototype['featuresProgressEvents'] = true; + // HTML5 Feature detection and Device Fixes --------------------------------- // (function() { var canPlayType, diff --git a/src/js/media/media.js b/src/js/media/media.js index 40aad56152..685f88564b 100644 --- a/src/js/media/media.js +++ b/src/js/media/media.js @@ -271,4 +271,96 @@ vjs.MediaTechController.prototype['featuresPlaybackRate'] = false; vjs.MediaTechController.prototype['featuresProgressEvents'] = false; vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; -vjs.media = {}; +/** + * A functional mixin for techs that want to use the Source Handler pattern. + * + * ##### EXAMPLE: + * + * videojs.MediaTechController.withSourceHandlers.call(MyTech); + * + */ +vjs.MediaTechController.withSourceHandlers = function(tech){ + /** + * All registered source handlers. + * Source handlers are scripts for handling specific formats. + * The source handler pattern is used for adaptive formats (HLS, DASH) that + * manually load video data and feed it into a Source Buffer (Media Source Extensions) + * @type {Array} + */ + tech.sourceHandlers = []; + + /** + * Register a source handler + * @param {Function} handler The source handler + * @param {Boolean} first Register it before any existing handlers + */ + tech.registerSourceHandler = function(handler, index){ + if (index === undefined) { + // add to the end of the list + index = tech.sourceHandlers.length; + } + + tech.sourceHandlers.splice(index, 0, handler); + }; + + /** + * Return the first source handler that supports the source + * TODO: Answer question: should 'probably' be prioritized over 'maybe' + * @param {Object} source The source object + * @returns {Object} The first source handler that supports the source + * @returns {null} Null if no source handler is found + */ + tech.selectSourceHandler = function(source){ + var handlers = tech.sourceHandlers; + + // console.log('here', handlers[0].canHandleSource); + + for (var i = 0; i < handlers.length; i++) { + can = handlers[i].canHandleSource(source); + + if (can) { + return handlers[i]; + } + } + + return null; + }; + + tech.prototype.setSource = function(source){ + var sh = tech.selectSourceHandler(source); + + this.currentSource_ = source; + + if (sh) { + // Clean up any existing source handler + if (this.sourceHandler && this.sourceHandler.dispose) { + this.sourceHandler.dispose(); + } + + this.sourceHandler = sh; + sh.handleSource(source, this); + } else { + // If no source handler was found, attempt the existing one. It could be + // that it's a source object with no type. + // There should be no way to get here without loading an initial handler. + // The tech wouldn't be loaded if a handler wasn't found in canPlaySource. + this.sourceHandler.handleSource(source, this); + } + }; + + /** + * Check if the HTML5 tech can support the given source + * @param {Object} srcObj The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + tech.canPlaySource = function(srcObj){ + var sh = tech.selectSourceHandler(srcObj); + + if (sh) { + return sh.canHandleSource(srcObj); + } + + return ''; + }; +}; + diff --git a/src/js/player.js b/src/js/player.js index 6028650d67..6661b0adaa 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1173,7 +1173,14 @@ vjs.Player.prototype.src = function(source){ // wait until the tech is ready to set the source this.ready(function(){ - this.techCall('src', source.src); + + // The setSource tech method was added with source handlers + // so older techs won't support it + if (this.tech['setSource']) { + this.techCall('setSource', source); + } else { + this.techCall('src', source.src); + } if (this.options_['preload'] == 'auto') { this.load(); From 1e6b16d30933f466f230f90bbf473012579af6b7 Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Tue, 7 Oct 2014 10:45:02 -0700 Subject: [PATCH 2/3] Moved RTMP functions to flash.rtmp.js --- build/source-loader.js | 1 + src/js/lib.js | 80 ---------------------- src/js/media/flash.js | 48 ------------- src/js/media/html5.js | 32 +++++---- src/js/media/media.js | 29 ++++---- src/js/player.js | 1 - src/js/tracks.js | 8 ++- src/js/xhr.js | 151 +++++++++++++++++++++++++++++++++++++++++ test/karma.conf.js | 1 + 9 files changed, 192 insertions(+), 159 deletions(-) create mode 100644 src/js/xhr.js diff --git a/build/source-loader.js b/build/source-loader.js index 40626c5835..492ff11806 100644 --- a/build/source-loader.js +++ b/build/source-loader.js @@ -19,6 +19,7 @@ var sourceFiles = [ "src/js/core-object.js", "src/js/events.js", "src/js/lib.js", + "src/js/xhr.js", "src/js/util.js", "src/js/component.js", "src/js/button.js", diff --git a/src/js/lib.js b/src/js/lib.js index ae90855779..90ceb55fae 100644 --- a/src/js/lib.js +++ b/src/js/lib.js @@ -614,86 +614,6 @@ vjs.createTimeRange = function(start, end){ }; }; -/** - * Simple http request for retrieving external files (e.g. text tracks) - * @param {String} url URL of resource - * @param {Function} onSuccess Success callback - * @param {Function=} onError Error callback - * @param {Boolean=} withCredentials Flag which allow credentials - * @private - */ -vjs.get = function(url, onSuccess, onError, withCredentials){ - var fileUrl, request, urlInfo, winLoc, crossOrigin; - - onError = onError || function(){}; - - if (typeof XMLHttpRequest === 'undefined') { - // Shim XMLHttpRequest for older IEs - window.XMLHttpRequest = function () { - try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} - try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} - try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} - throw new Error('This browser does not support XMLHttpRequest.'); - }; - } - - request = new XMLHttpRequest(); - - urlInfo = vjs.parseUrl(url); - winLoc = window.location; - // check if url is for another domain/origin - // ie8 doesn't know location.origin, so we won't rely on it here - crossOrigin = (urlInfo.protocol + urlInfo.host) !== (winLoc.protocol + winLoc.host); - - // Use XDomainRequest for IE if XMLHTTPRequest2 isn't available - // 'withCredentials' is only available in XMLHTTPRequest2 - // Also XDomainRequest has a lot of gotchas, so only use if cross domain - if(crossOrigin && window.XDomainRequest && !('withCredentials' in request)) { - request = new window.XDomainRequest(); - request.onload = function() { - onSuccess(request.responseText); - }; - request.onerror = onError; - // these blank handlers need to be set to fix ie9 http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ - request.onprogress = function() {}; - request.ontimeout = onError; - - // XMLHTTPRequest - } else { - fileUrl = (urlInfo.protocol == 'file:' || winLoc.protocol == 'file:'); - - request.onreadystatechange = function() { - if (request.readyState === 4) { - if (request.status === 200 || fileUrl && request.status === 0) { - onSuccess(request.responseText); - } else { - onError(request.responseText); - } - } - }; - } - - // open the connection - try { - // Third arg is async, or ignored by XDomainRequest - request.open('GET', url, true); - // withCredentials only supported by XMLHttpRequest2 - if(withCredentials) { - request.withCredentials = true; - } - } catch(e) { - onError(e); - return; - } - - // send the request - try { - request.send(); - } catch(e) { - onError(e); - } -}; - /** * Add to local storage (may removable) * @private diff --git a/src/js/media/flash.js b/src/js/media/flash.js index ff0c8404d9..a3868726e5 100644 --- a/src/js/media/flash.js +++ b/src/js/media/flash.js @@ -421,51 +421,3 @@ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ return objTag + attrsString + '>' + paramsString + ''; }; - -vjs.Flash.streamFromParts = function(connection, stream) { - return connection + '&' + stream; -}; - -vjs.Flash.streamToParts = function(src) { - var parts = { - connection: '', - stream: '' - }; - - if (! src) { - return parts; - } - - // Look for the normal URL separator we expect, '&'. - // If found, we split the URL into two pieces around the - // first '&'. - var connEnd = src.indexOf('&'); - var streamBegin; - if (connEnd !== -1) { - streamBegin = connEnd + 1; - } - else { - // If there's not a '&', we use the last '/' as the delimiter. - connEnd = streamBegin = src.lastIndexOf('/') + 1; - if (connEnd === 0) { - // really, there's not a '/'? - connEnd = streamBegin = src.length; - } - } - parts.connection = src.substring(0, connEnd); - parts.stream = src.substring(streamBegin, src.length); - - return parts; -}; - -vjs.Flash.isStreamingType = function(srcType) { - return srcType in vjs.Flash.streamingFormats; -}; - -// RTMP has four variations, any string starting -// with one of these protocols should be valid -vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i; - -vjs.Flash.isStreamingSrc = function(src) { - return vjs.Flash.RTMP_RE.test(src); -}; diff --git a/src/js/media/html5.js b/src/js/media/html5.js index 0d8f60ca20..f37c65337c 100644 --- a/src/js/media/html5.js +++ b/src/js/media/html5.js @@ -22,8 +22,8 @@ vjs.Html5 = vjs.MediaTechController.extend({ // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source // anyway so the error gets fired. - if (source && ((this.el_.currentSrc !== source.src) || (player.tag && player.tag.initNetworkState_ === 3))) { - this.el_.src = source.src; + if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) { + this.setSource(source); } // Determine if native controls should be used @@ -307,18 +307,26 @@ vjs.Html5.nativeSourceHandler = {}; * @return {String} 'probably', 'maybe', or '' (empty string) */ vjs.Html5.nativeSourceHandler.canHandleSource = function(source){ - var can = ''; - - // IE9 on Windows 7 without MediaPlayer throws an error here - // https://github.com/videojs/video.js/issues/519 - try { - can = !!vjs.TEST_VID.canPlayType(source.type); - } catch(e) { - can = ''; + var ext; + + function canPlayType(type){ + // IE9 on Windows 7 without MediaPlayer throws an error here + // https://github.com/videojs/video.js/issues/519 + try { + return !!vjs.TEST_VID.canPlayType(type); + } catch(e) { + return ''; + } } - // TODO: If no type, check the extension - return can; + // If a type was provided we should rely on that + if (source.type) { + return canPlayType(source.type); + } else { + // If no type, fall back to checking 'video/[EXTENSION]' + ext = source.src.match(/\.([^\/\?]+)(\?[^\/]+)?$/i)[1]; + return canPlayType('video/'+ext); + } }; /** diff --git a/src/js/media/media.js b/src/js/media/media.js index 685f88564b..0aba93b728 100644 --- a/src/js/media/media.js +++ b/src/js/media/media.js @@ -313,8 +313,6 @@ vjs.MediaTechController.withSourceHandlers = function(tech){ tech.selectSourceHandler = function(source){ var handlers = tech.sourceHandlers; - // console.log('here', handlers[0].canHandleSource); - for (var i = 0; i < handlers.length; i++) { can = handlers[i].canHandleSource(source); @@ -326,26 +324,23 @@ vjs.MediaTechController.withSourceHandlers = function(tech){ return null; }; + /** + * Create a function for setting the source using a source object + * and source handlers. + * Should never be called unless a source handler was found. + * @param {Object} source A source object with src and type keys + */ tech.prototype.setSource = function(source){ var sh = tech.selectSourceHandler(source); - this.currentSource_ = source; + // Clean up any existing source handler + if (this.sourceHandler && this.sourceHandler.dispose) { + this.sourceHandler.dispose(); + } - if (sh) { - // Clean up any existing source handler - if (this.sourceHandler && this.sourceHandler.dispose) { - this.sourceHandler.dispose(); - } + this.currentSource_ = source; - this.sourceHandler = sh; - sh.handleSource(source, this); - } else { - // If no source handler was found, attempt the existing one. It could be - // that it's a source object with no type. - // There should be no way to get here without loading an initial handler. - // The tech wouldn't be loaded if a handler wasn't found in canPlaySource. - this.sourceHandler.handleSource(source, this); - } + this.sourceHandler = sh.handleSource(source, this); }; /** diff --git a/src/js/player.js b/src/js/player.js index 6661b0adaa..3396311384 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1084,7 +1084,6 @@ vjs.Player.prototype.exitFullWindow = function(){ }; vjs.Player.prototype.selectSource = function(sources){ - // Loop through each playback technology in the options order for (var i=0,j=this.options_['techOrder'];i Date: Mon, 1 Dec 2014 15:45:52 -0800 Subject: [PATCH 3/3] Added tests for the source handler tech interface --- src/js/media/media.js | 73 ++++++++++++++++++++--------------- test/karma.conf.js | 1 + test/unit/flash.js | 4 ++ test/unit/media.html5.js | 4 ++ test/unit/media.js | 83 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 31 deletions(-) diff --git a/src/js/media/media.js b/src/js/media/media.js index 0aba93b728..647bf6ceef 100644 --- a/src/js/media/media.js +++ b/src/js/media/media.js @@ -279,28 +279,28 @@ vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; * videojs.MediaTechController.withSourceHandlers.call(MyTech); * */ -vjs.MediaTechController.withSourceHandlers = function(tech){ +vjs.MediaTechController.withSourceHandlers = function(Tech){ /** - * All registered source handlers. + * Register a source handler * Source handlers are scripts for handling specific formats. * The source handler pattern is used for adaptive formats (HLS, DASH) that * manually load video data and feed it into a Source Buffer (Media Source Extensions) - * @type {Array} - */ - tech.sourceHandlers = []; - - /** - * Register a source handler * @param {Function} handler The source handler * @param {Boolean} first Register it before any existing handlers */ - tech.registerSourceHandler = function(handler, index){ + Tech.registerSourceHandler = function(handler, index){ + var handlers = Tech.sourceHandlers; + + if (!handlers) { + handlers = Tech.sourceHandlers = []; + } + if (index === undefined) { // add to the end of the list - index = tech.sourceHandlers.length; + index = handlers.length; } - tech.sourceHandlers.splice(index, 0, handler); + handlers.splice(index, 0, handler); }; /** @@ -310,8 +310,8 @@ vjs.MediaTechController.withSourceHandlers = function(tech){ * @returns {Object} The first source handler that supports the source * @returns {null} Null if no source handler is found */ - tech.selectSourceHandler = function(source){ - var handlers = tech.sourceHandlers; + Tech.selectSourceHandler = function(source){ + var handlers = Tech.sourceHandlers || []; for (var i = 0; i < handlers.length; i++) { can = handlers[i].canHandleSource(source); @@ -324,38 +324,49 @@ vjs.MediaTechController.withSourceHandlers = function(tech){ return null; }; + /** + * Check if the tech can support the given source + * @param {Object} srcObj The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + Tech.canPlaySource = function(srcObj){ + var sh = Tech.selectSourceHandler(srcObj); + + if (sh) { + return sh.canHandleSource(srcObj); + } + + return ''; + }; + /** * Create a function for setting the source using a source object * and source handlers. * Should never be called unless a source handler was found. * @param {Object} source A source object with src and type keys + * @return {vjs.MediaTechController} self */ - tech.prototype.setSource = function(source){ - var sh = tech.selectSourceHandler(source); + Tech.prototype.setSource = function(source){ + var sh = Tech.selectSourceHandler(source); - // Clean up any existing source handler - if (this.sourceHandler && this.sourceHandler.dispose) { - this.sourceHandler.dispose(); - } + // Dispose any existing source handler + this.disposeSourceHandler(); + this.off('dispose', this.disposeSourceHandler); this.currentSource_ = source; + this.sourceHandler_ = sh.handleSource(source, this); + this.on('dispose', this.disposeSourceHandler); - this.sourceHandler = sh.handleSource(source, this); + return this; }; /** - * Check if the HTML5 tech can support the given source - * @param {Object} srcObj The source object - * @return {String} 'probably', 'maybe', or '' (empty string) + * Clean up any existing source handler */ - tech.canPlaySource = function(srcObj){ - var sh = tech.selectSourceHandler(srcObj); - - if (sh) { - return sh.canHandleSource(srcObj); + Tech.prototype.disposeSourceHandler = function(){ + if (this.sourceHandler_ && this.sourceHandler_.dispose) { + this.sourceHandler_.dispose(); } - - return ''; }; -}; +}; diff --git a/test/karma.conf.js b/test/karma.conf.js index 303c21ee49..caacec16e7 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -90,6 +90,7 @@ module.exports = function(config) { "../src/js/media/media.js", "../src/js/media/html5.js", "../src/js/media/flash.js", + "../src/js/media/flash.rtmp.js", "../src/js/media/loader.js", "../src/js/tracks.js", "../src/js/json.js", diff --git a/test/unit/flash.js b/test/unit/flash.js index 3471414587..24f5ea3e5d 100644 --- a/test/unit/flash.js +++ b/test/unit/flash.js @@ -143,3 +143,7 @@ test('ready triggering before and after disposing the tech', function() { vjs.Flash['checkReady'].restore(); }); + +test('should have the source handler interface', function() { + ok(vjs.Flash.registerSourceHandler, 'has the registerSourceHandler function'); +}); diff --git a/test/unit/media.html5.js b/test/unit/media.html5.js index 81598298cc..a2e1758221 100644 --- a/test/unit/media.html5.js +++ b/test/unit/media.html5.js @@ -130,3 +130,7 @@ test('error events may not set the errors property', function() { tech.trigger('error'); ok(true, 'no error was thrown'); }); + +test('should have the source handler interface', function() { + ok(vjs.Html5.registerSourceHandler, 'has the registerSourceHandler function'); +}); diff --git a/test/unit/media.js b/test/unit/media.js index 3a80b1a808..41722270e6 100644 --- a/test/unit/media.js +++ b/test/unit/media.js @@ -134,3 +134,86 @@ test('dispose() should stop time tracking', function() { } ok(true, 'no exception was thrown'); }); + +test('should add the source hanlder interface to a tech', function(){ + var mockPlayer = { + off: noop, + trigger: noop + }; + var sourceA = { src: 'foo.mp4', type: 'video/mp4' }; + var sourceB = { src: 'no-support', type: 'no-support' }; + + // Define a new tech class + var Tech = videojs.MediaTechController.extend(); + + // Extend Tech with source handlers + vjs.MediaTechController.withSourceHandlers(Tech); + + // Check for the expected class methods + ok(Tech.registerSourceHandler, 'added a registerSourceHandler function to the Tech'); + ok(Tech.selectSourceHandler, 'added a selectSourceHandler function to the Tech'); + + // Create an instance of Tech + var tech = new Tech(mockPlayer); + + // Check for the expected instance methods + ok(tech.setSource, 'added a setSource function to the tech instance'); + + // Create an internal state class for the source handler + // The internal class would be used by a source hanlder to maintain state + // and provde a dispose method for the handler. + // This is optional for source handlers + var disposeCalled = false; + var handlerInternalState = function(){}; + handlerInternalState.prototype.dispose = function(){ + disposeCalled = true; + }; + + // Create source handlers + var handlerOne = { + canHandleSource: function(source){ + if (source.type !=='no-support') { + return 'probably'; + } + return ''; + }, + handleSource: function(s, t){ + strictEqual(tech, t, 'the tech instance was passed to the source handler'); + strictEqual(sourceA, s, 'the tech instance was passed to the source handler'); + return new handlerInternalState(); + } + }; + + var handlerTwo = { + canHandleSource: function(source){ + return ''; // no support + }, + handleSource: function(source, tech){ + ok(false, 'handlerTwo supports nothing and should never be called'); + } + }; + + // Test registering source handlers + Tech.registerSourceHandler(handlerOne); + strictEqual(Tech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array'); + Tech.registerSourceHandler(handlerTwo, 0); + strictEqual(Tech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)'); + + // Test handler selection + strictEqual(Tech.selectSourceHandler(sourceA), handlerOne, 'handlerOne was selected to handle the valid source'); + strictEqual(Tech.selectSourceHandler(sourceB), null, 'no handler was selected to handle the invalid source'); + + // Test canPlaySource return values + strictEqual(Tech.canPlaySource(sourceA), 'probably', 'the Tech returned probably for the valid source'); + strictEqual(Tech.canPlaySource(sourceB), '', 'the Tech returned an empty string for the invalid source'); + + // Pass a source through the source handler process of a tech instance + tech.setSource(sourceA); + strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored'); + ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored'); + + // Check that the handler dipose method works + ok(!disposeCalled, 'dispose has not been called for the handler yet'); + tech.dispose(); + ok(disposeCalled, 'the handler dispose method was called when the tech was disposed'); +});