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.
10 * Flag whether the audio is playing or paused. True if playing, or false
15 observer: 'playingChanged',
16 reflectToAttribute: true
20 * Current elapsed time in the current music in millisecond.
24 observer: 'timeChanged'
28 * Whether the shuffle button is ON.
32 observer: 'shuffleChanged'
36 * Whether the repeat button is ON.
40 observer: 'repeatChanged'
44 * The audio volume. 0 is silent, and 100 is maximum loud.
48 observer: 'volumeChanged'
52 * Whether the expanded button is ON.
56 observer: 'expandedChanged'
60 * Track index of the current track.
64 observer: 'currentTrackIndexChanged'
68 * Model object of the Audio Player.
69 * @type {AudioPlayerModel}
74 observer: 'modelChanged'
78 * URL of the current track. (exposed publicly for tests)
83 reflectToAttribute: true
87 * The number of played tracks. (exposed publicly for tests)
92 reflectToAttribute: true
97 * Handles change event for shuffle mode.
98 * @param {boolean} shuffle
100 shuffleChanged: function(shuffle) {
102 this.model.shuffle = shuffle;
106 * Handles change event for repeat mode.
107 * @param {boolean} repeat
109 repeatChanged: function(repeat) {
111 this.model.repeat = repeat;
115 * Handles change event for audio volume.
116 * @param {number} volume
118 volumeChanged: function(volume) {
120 this.model.volume = volume;
124 * Handles change event for expanded state of track list.
126 expandedChanged: function(expanded) {
128 this.model.expanded = expanded;
132 * Initializes an element. This method is called automatically when the
136 this.addEventListener('keydown', this.onKeyDown_.bind(this));
138 this.$.audio.volume = 0; // Temporary initial volume.
139 this.$.audio.addEventListener('ended', this.onAudioEnded.bind(this));
140 this.$.audio.addEventListener('error', this.onAudioError.bind(this));
142 var onAudioStatusUpdatedBound = this.onAudioStatusUpdate_.bind(this);
143 this.$.audio.addEventListener('timeupdate', onAudioStatusUpdatedBound);
144 this.$.audio.addEventListener('ended', onAudioStatusUpdatedBound);
145 this.$.audio.addEventListener('play', onAudioStatusUpdatedBound);
146 this.$.audio.addEventListener('pause', onAudioStatusUpdatedBound);
147 this.$.audio.addEventListener('suspend', onAudioStatusUpdatedBound);
148 this.$.audio.addEventListener('abort', onAudioStatusUpdatedBound);
149 this.$.audio.addEventListener('error', onAudioStatusUpdatedBound);
150 this.$.audio.addEventListener('emptied', onAudioStatusUpdatedBound);
151 this.$.audio.addEventListener('stalled', onAudioStatusUpdatedBound);
155 * Invoked when trackList.currentTrackIndex is changed.
156 * @param {number} newValue new value.
157 * @param {number} oldValue old value.
159 currentTrackIndexChanged: function(newValue, oldValue) {
160 var currentTrackUrl = '';
162 if (oldValue != newValue) {
163 var currentTrack = this.$.trackList.getCurrentTrack();
164 if (currentTrack && currentTrack.url != this.$.audio.src) {
165 this.$.audio.src = currentTrack.url;
166 currentTrackUrl = this.$.audio.src;
172 // The attributes may be being watched, so we change it at the last.
173 this.currenttrackurl = currentTrackUrl;
177 * Invoked when playing is changed.
178 * @param {boolean} newValue new value.
179 * @param {boolean} oldValue old value.
181 playingChanged: function(newValue, oldValue) {
183 if (!this.$.audio.src) {
184 var currentTrack = this.$.trackList.getCurrentTrack();
185 if (currentTrack && currentTrack.url != this.$.audio.src) {
186 this.$.audio.src = currentTrack.url;
190 if (this.$.audio.src) {
191 this.currenttrackurl = this.$.audio.src;
197 // When the new status is "stopped".
198 this.cancelAutoAdvance_();
199 this.$.audio.pause();
200 this.currenttrackurl = '';
201 this.lastAudioUpdateTime_ = null;
205 * Invoked when the model changed.
206 * @param {AudioPlayerModel} newModel New model.
207 * @param {AudioPlayerModel} oldModel Old model.
209 modelChanged: function(newModel, oldModel) {
211 if (newModel !== oldModel && newModel) {
212 this.shuffle = newModel.shuffle;
213 this.repeat = newModel.repeat;
214 this.volume = newModel.volume;
215 this.expanded = newModel.expanded;
220 * Invoked when time is changed.
221 * @param {number} newValue new time (in ms).
222 * @param {number} oldValue old time (in ms).
224 timeChanged: function(newValue, oldValue) {
225 // Ignores updates from the audio element.
226 if (this.lastAudioUpdateTime_ === newValue)
229 if (this.$.audio.readyState !== 0)
230 this.$.audio.currentTime = this.time / 1000;
234 * Invoked when the next button in the controller is clicked.
235 * This handler is registered in the 'on-click' attribute of the element.
237 onControllerNextClicked: function() {
238 this.advance_(true /* forward */, true /* repeat */);
242 * Invoked when the previous button in the controller is clicked.
243 * This handler is registered in the 'on-click' attribute of the element.
245 onControllerPreviousClicked: function() {
246 this.advance_(false /* forward */, true /* repeat */);
250 * Invoked when the playback in the audio element is ended.
251 * This handler is registered in this.ready().
253 onAudioEnded: function() {
255 this.advance_(true /* forward */, this.repeat);
259 * Invoked when the playback in the audio element gets error.
260 * This handler is registered in this.ready().
262 onAudioError: function() {
263 this.scheduleAutoAdvance_(true /* forward */, this.repeat);
267 * Invoked when the time of playback in the audio element is updated.
268 * This handler is registered in this.ready().
271 onAudioStatusUpdate_: function() {
272 this.time = (this.lastAudioUpdateTime_ = this.$.audio.currentTime * 1000);
273 this.duration = this.$.audio.duration * 1000;
274 this.playing = !this.$.audio.paused;
278 * Invoked when receiving a request to replay the current music from the track
281 onReplayCurrentTrack: function() {
282 // Changes the current time back to the beginning, regardless of the current
283 // status (playing or paused).
284 this.$.audio.currentTime = 0;
289 * Goes to the previous or the next track.
290 * @param {boolean} forward True if next, false if previous.
291 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
294 advance_: function(forward, repeat) {
295 this.cancelAutoAdvance_();
297 var nextTrackIndex = this.$.trackList.getNextTrackIndex(forward, true);
298 var isNextTrackAvailable =
299 (this.$.trackList.getNextTrackIndex(forward, repeat) !== -1);
301 this.playing = isNextTrackAvailable;
303 // If there is only a single file in the list, 'currentTrackInde' is not
304 // changed and the handler is not invoked. Instead, plays here.
305 // TODO(yoshiki): clean up the code around here.
306 if (isNextTrackAvailable &&
307 this.$.trackList.currentTrackIndex == nextTrackIndex) {
311 this.$.trackList.currentTrackIndex = nextTrackIndex;
315 * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
316 * cancelAutoAdvance_().
320 autoAdvanceTimer_: null,
323 * Schedules automatic advance to the next track after a timeout.
324 * @param {boolean} forward True if next, false if previous.
325 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
328 scheduleAutoAdvance_: function(forward, repeat) {
329 this.cancelAutoAdvance_();
330 var currentTrackIndex = this.currentTrackIndex;
332 var timerId = setTimeout(
334 // If the other timer is scheduled, do nothing.
335 if (this.autoAdvanceTimer_ !== timerId)
338 this.autoAdvanceTimer_ = null;
340 // If the track has been changed since the advance was scheduled, do
342 if (this.currentTrackIndex !== currentTrackIndex)
345 // We are advancing only if the next track is not known to be invalid.
346 // This prevents an endless auto-advancing in the case when all tracks
347 // are invalid (we will only visit each track once).
348 this.advance_(forward, repeat);
352 this.autoAdvanceTimer_ = timerId;
356 * Cancels the scheduled auto advance.
359 cancelAutoAdvance_: function() {
360 if (this.autoAdvanceTimer_) {
361 clearTimeout(this.autoAdvanceTimer_);
362 this.autoAdvanceTimer_ = null;
367 * The list of the tracks in the playlist.
369 * When it changed, current operation including playback is stopped and
370 * restarts playback with new tracks if necessary.
372 * @type {Array<TrackInfo>}
375 return this.$.trackList ? this.$.trackList.tracks : null;
378 if (this.$.trackList.tracks === tracks)
381 this.cancelAutoAdvance_();
383 this.$.trackList.tracks = tracks;
384 var currentTrack = this.$.trackList.getCurrentTrack();
385 if (currentTrack && currentTrack.url != this.$.audio.src) {
386 this.$.audio.src = currentTrack.url;
392 * Invoked when the audio player is being unloaded.
394 onPageUnload: function() {
395 this.$.audio.src = ''; // Hack to prevent crashing.
399 * Invoked when the 'keydown' event is fired.
400 * @param {Event} event The event object.
402 onKeyDown_: function(event) {
403 switch (event.keyIdentifier) {
405 if (this.$.audioController.volumeSliderShown && this.model.volume < 100)
406 this.model.volume += 1;
409 if (this.$.audioController.volumeSliderShown && this.model.volume > 0)
410 this.model.volume -= 1;
413 if (this.$.audioController.volumeSliderShown && this.model.volume < 91)
414 this.model.volume += 10;
417 if (this.$.audioController.volumeSliderShown && this.model.volume > 9)
418 this.model.volume -= 10;
420 case 'MediaNextTrack':
421 this.onControllerNextClicked();
423 case 'MediaPlayPause':
424 this.playing = !this.playing;
426 case 'MediaPreviousTrack':
427 this.onControllerPreviousClicked();
430 // TODO: Define "Stop" behavior.
436 * Computes volume value for audio element. (should be in [0.0, 1.0])
437 * @param {number} volume Volume which is set in the UI. ([0, 100])
440 computeAudioVolume_: function(volume) {