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