Skip to content

Commit

Permalink
Merge pull request #231 from fmtvp/TVPJSFRMWK-2310
Browse files Browse the repository at this point in the history
HTML5 media player now uses a 'source' element to set source, rather than src attribute on media element. This is for improved device compatibility.
  • Loading branch information
dhurrell committed Apr 13, 2015
2 parents 6c5f89b + 9615399 commit 7f458f2
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 21 deletions.
107 changes: 89 additions & 18 deletions static/script-tests/tests/devices/mediaplayer/html5commontests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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
Expand All @@ -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];
Expand All @@ -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();
}

Expand Down Expand Up @@ -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++) {
Expand All @@ -277,7 +290,6 @@ window.commonTests.mediaPlayer.html5.mixinTests = function (testCase, mediaPlaye
}
};


//---------------------
// HTML5 specific tests
//---------------------
Expand Down Expand Up @@ -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;
Expand All @@ -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'));
});
};

Expand Down Expand Up @@ -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);
});
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
});
};

Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions static/script/devices/media/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
31 changes: 28 additions & 3 deletions static/script/devices/mediaplayer/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 7f458f2

Please sign in to comment.