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