Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const TIMEUPDATE_TIMEOUT_LENGTH = 10000; |
michael@0 | 6 | const ENDED_TIMEOUT_LENGTH = 10000; |
michael@0 | 7 | |
michael@0 | 8 | /* Time we wait for the canplaythrough event to fire |
michael@0 | 9 | * Note: this needs to be at least 30s because the |
michael@0 | 10 | * B2G emulator in VMs is really slow. */ |
michael@0 | 11 | const CANPLAYTHROUGH_TIMEOUT_LENGTH = 60000; |
michael@0 | 12 | |
michael@0 | 13 | /** |
michael@0 | 14 | * This class manages playback of a HTMLMediaElement with a MediaStream. |
michael@0 | 15 | * When constructed by a caller, an object instance is created with |
michael@0 | 16 | * a media element and a media stream object. |
michael@0 | 17 | * |
michael@0 | 18 | * @param {HTMLMediaElement} mediaElement the media element for playback |
michael@0 | 19 | * @param {MediaStream} mediaStream the media stream used in |
michael@0 | 20 | * the mediaElement for playback |
michael@0 | 21 | */ |
michael@0 | 22 | function MediaStreamPlayback(mediaElement, mediaStream) { |
michael@0 | 23 | this.mediaElement = mediaElement; |
michael@0 | 24 | this.mediaStream = mediaStream; |
michael@0 | 25 | } |
michael@0 | 26 | |
michael@0 | 27 | MediaStreamPlayback.prototype = { |
michael@0 | 28 | |
michael@0 | 29 | /** |
michael@0 | 30 | * Starts media with a media stream, runs it until a canplaythrough and |
michael@0 | 31 | * timeupdate event fires, and stops the media. |
michael@0 | 32 | * |
michael@0 | 33 | * @param {Boolean} isResume specifies if this media element is being resumed |
michael@0 | 34 | * from a previous run |
michael@0 | 35 | * @param {Function} onSuccess the success callback if the media playback |
michael@0 | 36 | * start and stop cycle completes successfully |
michael@0 | 37 | * @param {Function} onError the error callback if the media playback |
michael@0 | 38 | * start and stop cycle fails |
michael@0 | 39 | */ |
michael@0 | 40 | playMedia : function MSP_playMedia(isResume, onSuccess, onError) { |
michael@0 | 41 | var self = this; |
michael@0 | 42 | |
michael@0 | 43 | this.startMedia(isResume, function() { |
michael@0 | 44 | self.stopMediaElement(); |
michael@0 | 45 | onSuccess(); |
michael@0 | 46 | }, onError); |
michael@0 | 47 | }, |
michael@0 | 48 | |
michael@0 | 49 | /** |
michael@0 | 50 | * Starts the media with the associated stream. |
michael@0 | 51 | * |
michael@0 | 52 | * @param {Boolean} isResume specifies if the media element playback |
michael@0 | 53 | * is being resumed from a previous run |
michael@0 | 54 | * @param {Function} onSuccess the success function call back |
michael@0 | 55 | * if media starts correctly |
michael@0 | 56 | * @param {Function} onError the error function call back |
michael@0 | 57 | * if media fails to start |
michael@0 | 58 | */ |
michael@0 | 59 | startMedia : function MSP_startMedia(isResume, onSuccess, onError) { |
michael@0 | 60 | var self = this; |
michael@0 | 61 | var canPlayThroughFired = false; |
michael@0 | 62 | |
michael@0 | 63 | // If we're initially running this media, check that the time is zero |
michael@0 | 64 | if (!isResume) { |
michael@0 | 65 | is(this.mediaStream.currentTime, 0, |
michael@0 | 66 | "Before starting the media element, currentTime = 0"); |
michael@0 | 67 | } |
michael@0 | 68 | |
michael@0 | 69 | /** |
michael@0 | 70 | * Callback fired when the canplaythrough event is fired. We only |
michael@0 | 71 | * run the logic of this function once, as this event can fire |
michael@0 | 72 | * multiple times while a HTMLMediaStream is playing content from |
michael@0 | 73 | * a real-time MediaStream. |
michael@0 | 74 | */ |
michael@0 | 75 | var canPlayThroughCallback = function() { |
michael@0 | 76 | // Disable the canplaythrough event listener to prevent multiple calls |
michael@0 | 77 | canPlayThroughFired = true; |
michael@0 | 78 | self.mediaElement.removeEventListener('canplaythrough', |
michael@0 | 79 | canPlayThroughCallback, false); |
michael@0 | 80 | |
michael@0 | 81 | is(self.mediaElement.paused, false, |
michael@0 | 82 | "Media element should be playing"); |
michael@0 | 83 | is(self.mediaElement.duration, Number.POSITIVE_INFINITY, |
michael@0 | 84 | "Duration should be infinity"); |
michael@0 | 85 | |
michael@0 | 86 | // When the media element is playing with a real-time stream, we |
michael@0 | 87 | // constantly switch between having data to play vs. queuing up data, |
michael@0 | 88 | // so we can only check that the ready state is one of those two values |
michael@0 | 89 | ok(self.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || |
michael@0 | 90 | self.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA, |
michael@0 | 91 | "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA"); |
michael@0 | 92 | |
michael@0 | 93 | is(self.mediaElement.seekable.length, 0, |
michael@0 | 94 | "Seekable length shall be zero"); |
michael@0 | 95 | is(self.mediaElement.buffered.length, 0, |
michael@0 | 96 | "Buffered length shall be zero"); |
michael@0 | 97 | |
michael@0 | 98 | is(self.mediaElement.seeking, false, |
michael@0 | 99 | "MediaElement is not seekable with MediaStream"); |
michael@0 | 100 | ok(isNaN(self.mediaElement.startOffsetTime), |
michael@0 | 101 | "Start offset time shall not be a number"); |
michael@0 | 102 | is(self.mediaElement.loop, false, "Loop shall be false"); |
michael@0 | 103 | is(self.mediaElement.preload, "", "Preload should not exist"); |
michael@0 | 104 | is(self.mediaElement.src, "", "No src should be defined"); |
michael@0 | 105 | is(self.mediaElement.currentSrc, "", |
michael@0 | 106 | "Current src should still be an empty string"); |
michael@0 | 107 | |
michael@0 | 108 | var timeUpdateFired = false; |
michael@0 | 109 | |
michael@0 | 110 | var timeUpdateCallback = function() { |
michael@0 | 111 | if (self.mediaStream.currentTime > 0 && |
michael@0 | 112 | self.mediaElement.currentTime > 0) { |
michael@0 | 113 | timeUpdateFired = true; |
michael@0 | 114 | self.mediaElement.removeEventListener('timeupdate', |
michael@0 | 115 | timeUpdateCallback, false); |
michael@0 | 116 | onSuccess(); |
michael@0 | 117 | } |
michael@0 | 118 | }; |
michael@0 | 119 | |
michael@0 | 120 | // When timeupdate fires, we validate time has passed and move |
michael@0 | 121 | // onto the success condition |
michael@0 | 122 | self.mediaElement.addEventListener('timeupdate', timeUpdateCallback, |
michael@0 | 123 | false); |
michael@0 | 124 | |
michael@0 | 125 | // If timeupdate doesn't fire in enough time, we fail the test |
michael@0 | 126 | setTimeout(function() { |
michael@0 | 127 | if (!timeUpdateFired) { |
michael@0 | 128 | self.mediaElement.removeEventListener('timeupdate', |
michael@0 | 129 | timeUpdateCallback, false); |
michael@0 | 130 | onError("timeUpdate event never fired"); |
michael@0 | 131 | } |
michael@0 | 132 | }, TIMEUPDATE_TIMEOUT_LENGTH); |
michael@0 | 133 | }; |
michael@0 | 134 | |
michael@0 | 135 | // Adds a listener intended to be fired when playback is available |
michael@0 | 136 | // without further buffering. |
michael@0 | 137 | this.mediaElement.addEventListener('canplaythrough', canPlayThroughCallback, |
michael@0 | 138 | false); |
michael@0 | 139 | |
michael@0 | 140 | // Hooks up the media stream to the media element and starts playing it |
michael@0 | 141 | this.mediaElement.mozSrcObject = this.mediaStream; |
michael@0 | 142 | this.mediaElement.play(); |
michael@0 | 143 | |
michael@0 | 144 | // If canplaythrough doesn't fire in enough time, we fail the test |
michael@0 | 145 | setTimeout(function() { |
michael@0 | 146 | if (!canPlayThroughFired) { |
michael@0 | 147 | self.mediaElement.removeEventListener('canplaythrough', |
michael@0 | 148 | canPlayThroughCallback, false); |
michael@0 | 149 | onError("canplaythrough event never fired"); |
michael@0 | 150 | } |
michael@0 | 151 | }, CANPLAYTHROUGH_TIMEOUT_LENGTH); |
michael@0 | 152 | }, |
michael@0 | 153 | |
michael@0 | 154 | /** |
michael@0 | 155 | * Stops the media with the associated stream. |
michael@0 | 156 | * |
michael@0 | 157 | * Precondition: The media stream and element should both be actively |
michael@0 | 158 | * being played. |
michael@0 | 159 | */ |
michael@0 | 160 | stopMediaElement : function MSP_stopMediaElement() { |
michael@0 | 161 | this.mediaElement.pause(); |
michael@0 | 162 | this.mediaElement.mozSrcObject = null; |
michael@0 | 163 | } |
michael@0 | 164 | } |
michael@0 | 165 | |
michael@0 | 166 | |
michael@0 | 167 | /** |
michael@0 | 168 | * This class is basically the same as MediaStreamPlayback except |
michael@0 | 169 | * ensures that the instance provided startMedia is a MediaStream. |
michael@0 | 170 | * |
michael@0 | 171 | * @param {HTMLMediaElement} mediaElement the media element for playback |
michael@0 | 172 | * @param {LocalMediaStream} mediaStream the media stream used in |
michael@0 | 173 | * the mediaElement for playback |
michael@0 | 174 | */ |
michael@0 | 175 | function LocalMediaStreamPlayback(mediaElement, mediaStream) { |
michael@0 | 176 | ok(mediaStream instanceof LocalMediaStream, |
michael@0 | 177 | "Stream should be a LocalMediaStream"); |
michael@0 | 178 | MediaStreamPlayback.call(this, mediaElement, mediaStream); |
michael@0 | 179 | } |
michael@0 | 180 | |
michael@0 | 181 | LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, { |
michael@0 | 182 | |
michael@0 | 183 | /** |
michael@0 | 184 | * Starts media with a media stream, runs it until a canplaythrough and |
michael@0 | 185 | * timeupdate event fires, and calls stop() on the stream. |
michael@0 | 186 | * |
michael@0 | 187 | * @param {Boolean} isResume specifies if this media element is being resumed |
michael@0 | 188 | * from a previous run |
michael@0 | 189 | * @param {Function} onSuccess the success callback if the media element |
michael@0 | 190 | * successfully fires ended on a stop() call |
michael@0 | 191 | * on the stream |
michael@0 | 192 | * @param {Function} onError the error callback if the media element fails |
michael@0 | 193 | * to fire an ended callback on a stop() call |
michael@0 | 194 | * on the stream |
michael@0 | 195 | */ |
michael@0 | 196 | playMediaWithStreamStop : { |
michael@0 | 197 | value: function (isResume, onSuccess, onError) { |
michael@0 | 198 | var self = this; |
michael@0 | 199 | |
michael@0 | 200 | this.startMedia(isResume, function() { |
michael@0 | 201 | self.stopStreamInMediaPlayback(function() { |
michael@0 | 202 | self.stopMediaElement(); |
michael@0 | 203 | onSuccess(); |
michael@0 | 204 | }, onError); |
michael@0 | 205 | }, onError); |
michael@0 | 206 | } |
michael@0 | 207 | }, |
michael@0 | 208 | |
michael@0 | 209 | /** |
michael@0 | 210 | * Stops the local media stream while it's currently in playback in |
michael@0 | 211 | * a media element. |
michael@0 | 212 | * |
michael@0 | 213 | * Precondition: The media stream and element should both be actively |
michael@0 | 214 | * being played. |
michael@0 | 215 | * |
michael@0 | 216 | * @param {Function} onSuccess the success callback if the media element |
michael@0 | 217 | * fires an ended event from stop() being called |
michael@0 | 218 | * @param {Function} onError the error callback if the media element |
michael@0 | 219 | * fails to fire an ended event from stop() being |
michael@0 | 220 | * called |
michael@0 | 221 | */ |
michael@0 | 222 | stopStreamInMediaPlayback : { |
michael@0 | 223 | value: function (onSuccess, onError) { |
michael@0 | 224 | var endedFired = false; |
michael@0 | 225 | var self = this; |
michael@0 | 226 | |
michael@0 | 227 | /** |
michael@0 | 228 | * Callback fired when the ended event fires when stop() is called on the |
michael@0 | 229 | * stream. |
michael@0 | 230 | */ |
michael@0 | 231 | var endedCallback = function() { |
michael@0 | 232 | endedFired = true; |
michael@0 | 233 | self.mediaElement.removeEventListener('ended', endedCallback, false); |
michael@0 | 234 | ok(true, "ended event successfully fired"); |
michael@0 | 235 | onSuccess(); |
michael@0 | 236 | }; |
michael@0 | 237 | |
michael@0 | 238 | this.mediaElement.addEventListener('ended', endedCallback, false); |
michael@0 | 239 | this.mediaStream.stop(); |
michael@0 | 240 | |
michael@0 | 241 | // If ended doesn't fire in enough time, then we fail the test |
michael@0 | 242 | setTimeout(function() { |
michael@0 | 243 | if (!endedFired) { |
michael@0 | 244 | onError("ended event never fired"); |
michael@0 | 245 | } |
michael@0 | 246 | }, ENDED_TIMEOUT_LENGTH); |
michael@0 | 247 | } |
michael@0 | 248 | } |
michael@0 | 249 | }); |