OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 'use strict'; | |
6 | |
7 Polymer('audio-player', { | |
8 /** | |
9 * Child Elements | |
10 */ | |
11 audioController: null, | |
12 audioElement: null, | |
13 trackList: null, | |
14 | |
15 // Attributes of the element (lower characters only). | |
16 // These values must be used only to data binding and shouldn't be assigned | |
17 // any value nowhere except in the handler. | |
18 publish: { | |
19 playing: { | |
20 value: true, | |
21 reflect: true | |
22 }, | |
23 currenttrackurl: { | |
24 value: '', | |
25 reflect: true | |
26 }, | |
27 playcount: { | |
28 value: 0, | |
29 reflect: true | |
30 } | |
31 }, | |
32 | |
33 /** | |
34 * Model object of the Audio Player. | |
35 * @type {AudioPlayerModel} | |
36 */ | |
37 model: null, | |
38 | |
39 /** | |
40 * Initializes an element. This method is called automatically when the | |
41 * element is ready. | |
42 */ | |
43 ready: function() { | |
44 this.audioController = this.$.audioController; | |
45 this.audioElement = this.$.audio; | |
46 this.trackList = this.$.trackList; | |
47 | |
48 this.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
49 | |
50 this.audioElement.volume = 0; // Temporary initial volume. | |
51 this.audioElement.addEventListener('ended', this.onAudioEnded.bind(this)); | |
52 this.audioElement.addEventListener('error', this.onAudioError.bind(this)); | |
53 | |
54 var onAudioStatusUpdatedBound = this.onAudioStatusUpdate_.bind(this); | |
55 this.audioElement.addEventListener('timeupdate', onAudioStatusUpdatedBound); | |
56 this.audioElement.addEventListener('ended', onAudioStatusUpdatedBound); | |
57 this.audioElement.addEventListener('play', onAudioStatusUpdatedBound); | |
58 this.audioElement.addEventListener('pause', onAudioStatusUpdatedBound); | |
59 this.audioElement.addEventListener('suspend', onAudioStatusUpdatedBound); | |
60 this.audioElement.addEventListener('abort', onAudioStatusUpdatedBound); | |
61 this.audioElement.addEventListener('error', onAudioStatusUpdatedBound); | |
62 this.audioElement.addEventListener('emptied', onAudioStatusUpdatedBound); | |
63 this.audioElement.addEventListener('stalled', onAudioStatusUpdatedBound); | |
64 }, | |
65 | |
66 /** | |
67 * Registers handlers for changing of external variables | |
68 */ | |
69 observe: { | |
70 'trackList.currentTrackIndex': 'onCurrentTrackIndexChanged', | |
71 'audioController.playing': 'onControllerPlayingChanged', | |
72 'audioController.time': 'onControllerTimeChanged', | |
73 'model.volume': 'onVolumeChanged', | |
74 }, | |
75 | |
76 /** | |
77 * Invoked when trackList.currentTrackIndex is changed. | |
78 * @param {number} oldValue old value. | |
79 * @param {number} newValue new value. | |
80 */ | |
81 onCurrentTrackIndexChanged: function(oldValue, newValue) { | |
82 var currentTrackUrl = ''; | |
83 | |
84 if (oldValue != newValue) { | |
85 var currentTrack = this.trackList.getCurrentTrack(); | |
86 if (currentTrack && currentTrack.url != this.audioElement.src) { | |
87 this.audioElement.src = currentTrack.url; | |
88 currentTrackUrl = this.audioElement.src; | |
89 if (this.audioController.playing) | |
90 this.audioElement.play(); | |
91 } | |
92 } | |
93 | |
94 // The attributes may be being watched, so we change it at the last. | |
95 this.currenttrackurl = currentTrackUrl; | |
96 }, | |
97 | |
98 /** | |
99 * Invoked when audioController.playing is changed. | |
100 * @param {boolean} oldValue old value. | |
101 * @param {boolean} newValue new value. | |
102 */ | |
103 onControllerPlayingChanged: function(oldValue, newValue) { | |
104 this.playing = newValue; | |
105 | |
106 if (newValue) { | |
107 if (!this.audioElement.src) { | |
108 var currentTrack = this.trackList.getCurrentTrack(); | |
109 if (currentTrack && currentTrack.url != this.audioElement.src) { | |
110 this.audioElement.src = currentTrack.url; | |
111 } | |
112 } | |
113 | |
114 if (this.audioElement.src) { | |
115 this.currenttrackurl = this.audioElement.src; | |
116 this.audioElement.play(); | |
117 return; | |
118 } | |
119 } | |
120 | |
121 // When the new status is "stopped". | |
122 this.cancelAutoAdvance_(); | |
123 this.audioElement.pause(); | |
124 this.currenttrackurl = ''; | |
125 this.lastAudioUpdateTime_ = null; | |
126 }, | |
127 | |
128 /** | |
129 * Invoked when audioController.volume is changed. | |
130 * @param {number} oldValue old value. | |
131 * @param {number} newValue new value. | |
132 */ | |
133 onVolumeChanged: function(oldValue, newValue) { | |
134 this.audioElement.volume = newValue / 100; | |
135 }, | |
136 | |
137 /** | |
138 * Invoked when the model changed. | |
139 * @param {AudioPlayerModel} oldValue Old Value. | |
140 * @param {AudioPlayerModel} newValue New Value. | |
141 */ | |
142 modelChanged: function(oldValue, newValue) { | |
143 this.trackList.model = newValue; | |
144 this.audioController.model = newValue; | |
145 | |
146 // Invoke the handler manually. | |
147 this.onVolumeChanged(0, newValue.volume); | |
148 }, | |
149 | |
150 /** | |
151 * Invoked when audioController.time is changed. | |
152 * @param {number} oldValue old time (in ms). | |
153 * @param {number} newValue new time (in ms). | |
154 */ | |
155 onControllerTimeChanged: function(oldValue, newValue) { | |
156 // Ignores updates from the audio element. | |
157 if (this.lastAudioUpdateTime_ === newValue) | |
158 return; | |
159 | |
160 if (this.audioElement.readyState !== 0) | |
161 this.audioElement.currentTime = this.audioController.time / 1000; | |
162 }, | |
163 | |
164 /** | |
165 * Invoked when the next button in the controller is clicked. | |
166 * This handler is registered in the 'on-click' attribute of the element. | |
167 */ | |
168 onControllerNextClicked: function() { | |
169 this.advance_(true /* forward */, true /* repeat */); | |
170 }, | |
171 | |
172 /** | |
173 * Invoked when the previous button in the controller is clicked. | |
174 * This handler is registered in the 'on-click' attribute of the element. | |
175 */ | |
176 onControllerPreviousClicked: function() { | |
177 this.advance_(false /* forward */, true /* repeat */); | |
178 }, | |
179 | |
180 /** | |
181 * Invoked when the playback in the audio element is ended. | |
182 * This handler is registered in this.ready(). | |
183 */ | |
184 onAudioEnded: function() { | |
185 this.playcount++; | |
186 this.advance_(true /* forward */, this.model.repeat); | |
187 }, | |
188 | |
189 /** | |
190 * Invoked when the playback in the audio element gets error. | |
191 * This handler is registered in this.ready(). | |
192 */ | |
193 onAudioError: function() { | |
194 this.scheduleAutoAdvance_(true /* forward */, this.model.repeat); | |
195 }, | |
196 | |
197 /** | |
198 * Invoked when the time of playback in the audio element is updated. | |
199 * This handler is registered in this.ready(). | |
200 * @private | |
201 */ | |
202 onAudioStatusUpdate_: function() { | |
203 this.audioController.time = | |
204 (this.lastAudioUpdateTime_ = this.audioElement.currentTime * 1000); | |
205 this.audioController.duration = this.audioElement.duration * 1000; | |
206 this.audioController.playing = !this.audioElement.paused; | |
207 }, | |
208 | |
209 /** | |
210 * Invoked when receiving a request to replay the current music from the track | |
211 * list element. | |
212 */ | |
213 onReplayCurrentTrack: function() { | |
214 // Changes the current time back to the beginning, regardless of the current | |
215 // status (playing or paused). | |
216 this.audioElement.currentTime = 0; | |
217 this.audioController.time = 0; | |
218 }, | |
219 | |
220 /** | |
221 * Goes to the previous or the next track. | |
222 * @param {boolean} forward True if next, false if previous. | |
223 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise. | |
224 * @private | |
225 */ | |
226 advance_: function(forward, repeat) { | |
227 this.cancelAutoAdvance_(); | |
228 | |
229 var nextTrackIndex = this.trackList.getNextTrackIndex(forward, true); | |
230 var isNextTrackAvailable = | |
231 (this.trackList.getNextTrackIndex(forward, repeat) !== -1); | |
232 | |
233 this.audioController.playing = isNextTrackAvailable; | |
234 | |
235 // If there is only a single file in the list, 'currentTrackInde' is not | |
236 // changed and the handler is not invoked. Instead, plays here. | |
237 // TODO(yoshiki): clean up the code around here. | |
238 if (isNextTrackAvailable && | |
239 this.trackList.currentTrackIndex == nextTrackIndex) { | |
240 this.audioElement.play(); | |
241 } | |
242 | |
243 this.trackList.currentTrackIndex = nextTrackIndex; | |
244 | |
245 Platform.performMicrotaskCheckpoint(); | |
246 }, | |
247 | |
248 /** | |
249 * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and | |
250 * cancelAutoAdvance_(). | |
251 * @type {number} | |
252 * @private | |
253 */ | |
254 autoAdvanceTimer_: null, | |
255 | |
256 /** | |
257 * Schedules automatic advance to the next track after a timeout. | |
258 * @param {boolean} forward True if next, false if previous. | |
259 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise. | |
260 * @private | |
261 */ | |
262 scheduleAutoAdvance_: function(forward, repeat) { | |
263 this.cancelAutoAdvance_(); | |
264 var currentTrackIndex = this.currentTrackIndex; | |
265 | |
266 var timerId = setTimeout( | |
267 function() { | |
268 // If the other timer is scheduled, do nothing. | |
269 if (this.autoAdvanceTimer_ !== timerId) | |
270 return; | |
271 | |
272 this.autoAdvanceTimer_ = null; | |
273 | |
274 // If the track has been changed since the advance was scheduled, do | |
275 // nothing. | |
276 if (this.currentTrackIndex !== currentTrackIndex) | |
277 return; | |
278 | |
279 // We are advancing only if the next track is not known to be invalid. | |
280 // This prevents an endless auto-advancing in the case when all tracks | |
281 // are invalid (we will only visit each track once). | |
282 this.advance_(forward, repeat, true /* only if valid */); | |
283 }.bind(this), | |
284 3000); | |
285 | |
286 this.autoAdvanceTimer_ = timerId; | |
287 }, | |
288 | |
289 /** | |
290 * Cancels the scheduled auto advance. | |
291 * @private | |
292 */ | |
293 cancelAutoAdvance_: function() { | |
294 if (this.autoAdvanceTimer_) { | |
295 clearTimeout(this.autoAdvanceTimer_); | |
296 this.autoAdvanceTimer_ = null; | |
297 } | |
298 }, | |
299 | |
300 /** | |
301 * The index of the current track. | |
302 * If the list has no tracks, the value must be -1. | |
303 * | |
304 * @type {number} | |
305 */ | |
306 get currentTrackIndex() { | |
307 return this.trackList.currentTrackIndex; | |
308 }, | |
309 set currentTrackIndex(value) { | |
310 this.trackList.currentTrackIndex = value; | |
311 }, | |
312 | |
313 /** | |
314 * The list of the tracks in the playlist. | |
315 * | |
316 * When it changed, current operation including playback is stopped and | |
317 * restarts playback with new tracks if necessary. | |
318 * | |
319 * @type {Array.<AudioPlayer.TrackInfo>} | |
320 */ | |
321 get tracks() { | |
322 return this.trackList ? this.trackList.tracks : null; | |
323 }, | |
324 set tracks(tracks) { | |
325 if (this.trackList.tracks === tracks) | |
326 return; | |
327 | |
328 this.cancelAutoAdvance_(); | |
329 | |
330 this.trackList.tracks = tracks; | |
331 var currentTrack = this.trackList.getCurrentTrack(); | |
332 if (currentTrack && currentTrack.url != this.audioElement.src) { | |
333 this.audioElement.src = currentTrack.url; | |
334 this.audioElement.play(); | |
335 } | |
336 }, | |
337 | |
338 /** | |
339 * Invoked when the audio player is being unloaded. | |
340 */ | |
341 onPageUnload: function() { | |
342 this.audioElement.src = ''; // Hack to prevent crashing. | |
343 }, | |
344 | |
345 /** | |
346 * Invoked when the 'keydown' event is fired. | |
347 * @param {Event} event The event object. | |
348 */ | |
349 onKeyDown_: function(event) { | |
350 switch (event.keyIdentifier) { | |
351 case 'Up': | |
352 if (this.audioController.volumeSliderShown && this.model.volume < 100) | |
353 this.model.volume += 1; | |
354 break; | |
355 case 'Down': | |
356 if (this.audioController.volumeSliderShown && this.model.volume > 0) | |
357 this.model.volume -= 1; | |
358 break; | |
359 case 'PageUp': | |
360 if (this.audioController.volumeSliderShown && this.model.volume < 91) | |
361 this.model.volume += 10; | |
362 break; | |
363 case 'PageDown': | |
364 if (this.audioController.volumeSliderShown && this.model.volume > 9) | |
365 this.model.volume -= 10; | |
366 break; | |
367 case 'MediaNextTrack': | |
368 this.onControllerNextClicked(); | |
369 break; | |
370 case 'MediaPlayPause': | |
371 var playing = this.audioController.playing; | |
372 this.onControllerPlayingChanged(playing, !playing); | |
373 break; | |
374 case 'MediaPreviousTrack': | |
375 this.onControllerPreviousClicked(); | |
376 break; | |
377 case 'MediaStop': | |
378 // TODO: Define "Stop" behavior. | |
379 break; | |
380 } | |
381 }, | |
382 }); | |
OLD | NEW |