diff --git a/static/script-tests/tests/devices/mediaplayer/html5commontests.js b/static/script-tests/tests/devices/mediaplayer/html5commontests.js index a15c797e..90b9367c 100644 --- a/static/script-tests/tests/devices/mediaplayer/html5commontests.js +++ b/static/script-tests/tests/devices/mediaplayer/html5commontests.js @@ -33,6 +33,7 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye var clock; var stubCreateElementResults = undefined; var mediaEventListeners = undefined; + var sourceEventListeners = undefined; var stubCreateElement = function (sandbox, application) { var device = application.getDevice(); @@ -61,11 +62,6 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye mediaEventListeners.canplay(); }, emitPlaybackError: function(mediaPlayer, errorCode) { - - // MEDIA_ERR_NETWORK == 2 - errorCode = errorCode !== undefined ? errorCode : 2; - // This code, or higher, is needed for the error event. A value of 1 should result in an abort event. - // See http://www.w3.org/TR/2011/WD-html5-20110405/video.html stubCreateElementResults.video.error = { code: errorCode }; stubCreateElementResults.audio.error = { code: errorCode }; @@ -122,6 +118,16 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye }; mixins.setUp = function() { + function mediaAddEventListener(event, callback) { + if (mediaEventListeners[event]) { throw "Listener already registered on media mock for event: " + event; } + mediaEventListeners[event] = callback; + } + + function sourceAddEventListener(event, callback) { + if (sourceEventListeners[event]) { throw "Listener already registered on media source mock for event: " + event; } + sourceEventListeners[event] = callback; + } + this.sandbox = sinon.sandbox.create(); // We will use a div to provide fake elements for video and audio elements. This is to get around browser @@ -131,8 +137,14 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye stubCreateElementResults = { video: document.createElement("div"), audio: document.createElement("div"), + source: document.createElement("source") }; mediaEventListeners = {}; + sourceEventListeners = {}; + + stubCreateElementResults.source.addEventListener = sourceAddEventListener; + stubCreateElementResults.source.removeEventListener = this.sandbox.stub(); + var mediaElements = [stubCreateElementResults.video, stubCreateElementResults.audio]; for (var i = 0; i < mediaElements.length; i++) { var media = mediaElements[i]; @@ -145,10 +157,7 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye media.seekable.start = this.sandbox.stub(); media.seekable.end = this.sandbox.stub(); - media.addEventListener = function (event, callback) { - if (mediaEventListeners[event]) { throw "Listener already registered on media mock for event: " + event; } - mediaEventListeners[event] = callback; - }; + media.addEventListener = mediaAddEventListener; media.removeEventListener = this.sandbox.stub(); } @@ -266,6 +275,10 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye stubCreateElementResults.video.currentTime = 0; }; + var emitSourceElementError = function() { + sourceEventListeners.error(); + }; + var setMetadata = function (mediaPlayer, currentTime, range) { var mediaElements = [stubCreateElementResults.video, stubCreateElementResults.audio]; for (var i = 0; i < mediaElements.length; i++) { @@ -277,7 +290,6 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye } }; - //--------------------- // HTML5 specific tests //--------------------- @@ -325,6 +337,17 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye }); }; + mixins.testSourceElementIsRemovedFromMediaElementOnReset = function(queue) { + expectAsserts(1); + var self = this; + runMediaPlayerTest(this, queue, function (MediaPlayer) { + self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'testURL', 'video/mp4'); + self._mediaPlayer.reset(); + + assertNull(stubCreateElementResults.video.firstChild); + }); + }; + mixins.testCreatedAudioElementIsPutInRootWidget = function(queue) { expectAsserts(1); var self = this; @@ -348,13 +371,28 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye }); }; - mixins.testSourceURLSetOnSetSource = function(queue) { - expectAsserts(1); + mixins.testSourceURLSetAsChildElementOnSetSource = function(queue) { + expectAsserts(5); var self = this; runMediaPlayerTest(this, queue, function (MediaPlayer) { + assertEquals(0, stubCreateElementResults.video.children.length); self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'http://testurl/', 'video/mp4'); + assertEquals(1, stubCreateElementResults.video.children.length); + var childElement = stubCreateElementResults.video.firstChild; + assertEquals('source', childElement.nodeName.toLowerCase()); + assertEquals('http://testurl/', childElement.src); + assertEquals('video/mp4', childElement.type); + }); + }; - assertEquals('http://testurl/', stubCreateElementResults.video.src); + mixins.testSetSourceUsesGenerateSourceElementExtensionPoint = function(queue) { + expectAsserts(2); + var self = this; + runMediaPlayerTest(this, queue, function (MediaPlayer) { + self.sandbox.spy(self._mediaPlayer, "_generateSourceElement"); + assert(self._mediaPlayer._generateSourceElement.notCalled); + self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'http://testurl/', 'video/mp4'); + assert(self._mediaPlayer._generateSourceElement.calledWith('http://testurl/', 'video/mp4')); }); }; @@ -475,11 +513,27 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye assertFunction(mediaEventListeners.error); - stubCreateElementResults.video.error = { code: 2 }; // MEDIA_ERR_NETWORK - http://www.w3.org/TR/2011/WD-html5-20110405/video.html#dom-media-error + deviceMockingHooks.emitPlaybackError(self._mediaPlayer, 3); // MEDIA_ERR_DECODE - http://www.w3.org/TR/2011/WD-html5-20110405/video.html#dom-media-error + + assert(errorStub.calledWith("Media element emitted error with code: 3")); + }); + }; + + mixins.testErrorEventFromSourceElementCausesErrorLog = function(queue) { + expectAsserts(3); + var self = this; + runMediaPlayerTest(this, queue, function (MediaPlayer) { + + var errorStub = self.sandbox.stub(); + self.sandbox.stub(self._device, "getLogger").returns({error: errorStub}); + + self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'http://testurl/', 'video/mp4'); + assertFunction(sourceEventListeners.error); - deviceMockingHooks.emitPlaybackError(self._mediaPlayer); + emitSourceElementError(); - assert(errorStub.calledWith("Media element emitted error with code: 2")); + assert(errorStub.calledWith("Media source element emitted an error")); + assertEvent(self, MediaPlayer.EVENT.ERROR); }); }; @@ -847,6 +901,19 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye }); }; + mixins.testResetRemovesEventListenerFromTheSourceElement = function(queue) { + expectAsserts(2); + var self = this; + runMediaPlayerTest(this, queue, function (MediaPlayer) { + assert(stubCreateElementResults.source.removeEventListener.withArgs("error").notCalled); + + self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'http://testurl/', 'video/mp4'); + self._mediaPlayer.reset(); + + assert(stubCreateElementResults.source.removeEventListener.withArgs("error").called); + }); + }; + mixins.testPlayFromCurrentTimeWhenPlayingGoesToBufferingThenToPlaying = function(queue) { var currentAndTargetTime = 50; doTestPlayFromNearCurrentTimeWhenPlayingGoesToBufferingThenToPlaying(this, queue, currentAndTargetTime, currentAndTargetTime); @@ -993,17 +1060,21 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye }; mixins.testResetUnloadsMediaElementSourceAsPerGuidelines = function(queue) { - expectAsserts(2); + // Guidelines in HTML5 video spec, section 4.8.10.15: + // http://www.w3.org/TR/2011/WD-html5-20110405/video.html#best-practices-for-authors-using-media-elements + expectAsserts(3); var self = this; runMediaPlayerTest(this, queue, function (MediaPlayer) { self._mediaPlayer.setSource(MediaPlayer.TYPE.VIDEO, 'http://testurl/', 'video/mp4'); stubCreateElementResults.video.load.reset(); self.sandbox.stub(stubCreateElementResults.video, 'removeAttribute'); + self.sandbox.spy(self._device, 'removeElement'); self._mediaPlayer.reset(); assert(stubCreateElementResults.video.removeAttribute.withArgs('src').calledOnce); assert(stubCreateElementResults.video.load.calledOnce); + assert(self._device.removeElement.withArgs(stubCreateElementResults.source).calledBefore(stubCreateElementResults.video.load)); }); }; @@ -1704,4 +1775,4 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye // Mixin the common tests shared by all MediaPlayer implementations (last, so it can detect conflicts) window.commonTests.mediaPlayer.all.mixinTests(testCase, mediaPlayerDeviceModifierRequireName, config, deviceMockingHooks); -} \ No newline at end of file +} diff --git a/static/script/devices/media/html5.js b/static/script/devices/media/html5.js index f02dbad0..32991cd0 100644 --- a/static/script/devices/media/html5.js +++ b/static/script/devices/media/html5.js @@ -332,6 +332,8 @@ require.def( }, webkitMemoryLeakFix : function() { // http://stackoverflow.com/questions/5170398/ios-safari-memory-leak-when-loading-unloading-html5-video + // Resetting source is also advised by HTML5 video spec, section 4.8.10.15: + // http://www.w3.org/TR/2011/WD-html5-20110405/video.html#best-practices-for-authors-using-media-elements this._mediaElement.removeAttribute("src"); this._mediaElement.load(); } diff --git a/static/script/devices/mediaplayer/html5.js b/static/script/devices/mediaplayer/html5.js index e7cd7779..193819de 100644 --- a/static/script/devices/mediaplayer/html5.js +++ b/static/script/devices/mediaplayer/html5.js @@ -78,6 +78,7 @@ require.def( this._wrapOnDeviceBuffering = function(event) { self._onDeviceBuffering(); }; this._wrapOnStatus = function(event) { self._onStatus(); }; this._wrapOnMetadata = function(event) { self._onMetadata(); }; + this._wrapOnSourceError = function(event) { self._onSourceError(); }; this._mediaElement.addEventListener("canplay", this._wrapOnFinishedBuffering, false); this._mediaElement.addEventListener("seeked", this._wrapOnFinishedBuffering, false); this._mediaElement.addEventListener("playing", this._wrapOnFinishedBuffering, false); @@ -90,8 +91,12 @@ require.def( var appElement = RuntimeContext.getCurrentApplication().getRootWidget().outputElement; device.prependChildElement(appElement, this._mediaElement); + this._sourceElement = this._generateSourceElement(url, mimeType); + this._sourceElement.addEventListener("error", this._wrapOnSourceError, false); + this._mediaElement.preload = "auto"; - this._mediaElement.src = url; + device.appendChildElement(this._mediaElement, this._sourceElement); + this._mediaElement.load(); this._toStopped(); @@ -350,6 +355,10 @@ require.def( this._reportError("Media element emitted error with code: " + this._mediaElement.error.code); }, + _onSourceError: function() { + this._reportError("Media source element emitted an error"); + }, + /** * @protected */ @@ -450,21 +459,37 @@ require.def( this._mediaElement.removeEventListener("waiting", this._wrapOnDeviceBuffering, false); this._mediaElement.removeEventListener("timeupdate", this._wrapOnStatus, false); this._mediaElement.removeEventListener("loadedmetadata", this._wrapOnMetadata, false); + this._sourceElement.removeEventListener("error", this._wrapOnSourceError, false); + + var device = RuntimeContext.getDevice(); + device.removeElement(this._sourceElement); this._unloadMediaSrc(); - var device = RuntimeContext.getDevice(); device.removeElement(this._mediaElement); - delete this._mediaElement; + delete this._sourceElement; } }, _unloadMediaSrc: function() { + // Reset source as advised by HTML5 video spec, section 4.8.10.15: + // http://www.w3.org/TR/2011/WD-html5-20110405/video.html#best-practices-for-authors-using-media-elements this._mediaElement.removeAttribute('src'); this._mediaElement.load(); }, + /** + * @protected + */ + _generateSourceElement: function(url, mimeType) { + var device = RuntimeContext.getDevice(); + var sourceElement = device._createElement('source'); + sourceElement.src = url; + sourceElement.type = mimeType; + return sourceElement; + }, + _reportError: function(errorMessage) { RuntimeContext.getDevice().getLogger().error(errorMessage); this._emitEvent(MediaPlayer.EVENT.ERROR);