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.
7 * @extends {PolymerElement}
9 var AudioPlayerElement = function() {};
11 AudioPlayerElement.prototype = {
13 audioController: null,
22 // Attributes of the element (lower characters only).
23 // These values must be used only to data binding and shouldn't be assigned
24 // any value nowhere except in the handler.
41 * Model object of the Audio Player.
42 * @type {AudioPlayerModel}
47 * Initializes an element. This method is called automatically when the
51 this.audioController = this.$.audioController;
52 this.audioElement = this.$.audio;
53 this.trackList = this.$.trackList;
55 this.addEventListener('keydown', this.onKeyDown_.bind(this));
57 this.audioElement.volume = 0; // Temporary initial volume.
58 this.audioElement.addEventListener('ended', this.onAudioEnded.bind(this));
59 this.audioElement.addEventListener('error', this.onAudioError.bind(this));
61 var onAudioStatusUpdatedBound = this.onAudioStatusUpdate_.bind(this);
62 this.audioElement.addEventListener('timeupdate', onAudioStatusUpdatedBound);
63 this.audioElement.addEventListener('ended', onAudioStatusUpdatedBound);
64 this.audioElement.addEventListener('play', onAudioStatusUpdatedBound);
65 this.audioElement.addEventListener('pause', onAudioStatusUpdatedBound);
66 this.audioElement.addEventListener('suspend', onAudioStatusUpdatedBound);
67 this.audioElement.addEventListener('abort', onAudioStatusUpdatedBound);
68 this.audioElement.addEventListener('error', onAudioStatusUpdatedBound);
69 this.audioElement.addEventListener('emptied', onAudioStatusUpdatedBound);
70 this.audioElement.addEventListener('stalled', onAudioStatusUpdatedBound);
74 * Registers handlers for changing of external variables
77 'trackList.currentTrackIndex': 'onCurrentTrackIndexChanged',
78 'audioController.playing': 'onControllerPlayingChanged',
79 'audioController.time': 'onControllerTimeChanged',
80 'model.volume': 'onVolumeChanged',
84 * Invoked when trackList.currentTrackIndex is changed.
85 * @param {number} oldValue old value.
86 * @param {number} newValue new value.
88 onCurrentTrackIndexChanged: function(oldValue, newValue) {
89 var currentTrackUrl = '';
91 if (oldValue != newValue) {
92 var currentTrack = this.trackList.getCurrentTrack();
93 if (currentTrack && currentTrack.url != this.audioElement.src) {
94 this.audioElement.src = currentTrack.url;
95 currentTrackUrl = this.audioElement.src;
96 if (this.audioController.playing)
97 this.audioElement.play();
101 // The attributes may be being watched, so we change it at the last.
102 this.currenttrackurl = currentTrackUrl;
106 * Invoked when audioController.playing is changed.
107 * @param {boolean} oldValue old value.
108 * @param {boolean} newValue new value.
110 onControllerPlayingChanged: function(oldValue, newValue) {
111 this.playing = newValue;
114 if (!this.audioElement.src) {
115 var currentTrack = this.trackList.getCurrentTrack();
116 if (currentTrack && currentTrack.url != this.audioElement.src) {
117 this.audioElement.src = currentTrack.url;
121 if (this.audioElement.src) {
122 this.currenttrackurl = this.audioElement.src;
123 this.audioElement.play();
128 // When the new status is "stopped".
129 this.cancelAutoAdvance_();
130 this.audioElement.pause();
131 this.currenttrackurl = '';
132 this.lastAudioUpdateTime_ = null;
136 * Invoked when audioController.volume is changed.
137 * @param {number} oldValue old value.
138 * @param {number} newValue new value.
140 onVolumeChanged: function(oldValue, newValue) {
141 this.audioElement.volume = newValue / 100;
145 * Invoked when the model changed.
146 * @param {AudioPlayerModel} oldValue Old Value.
147 * @param {AudioPlayerModel} newValue New Value.
149 modelChanged: function(oldValue, newValue) {
150 this.trackList.model = newValue;
151 this.audioController.model = newValue;
153 // Invoke the handler manually.
154 this.onVolumeChanged(0, newValue.volume);
158 * Invoked when audioController.time is changed.
159 * @param {number} oldValue old time (in ms).
160 * @param {number} newValue new time (in ms).
162 onControllerTimeChanged: function(oldValue, newValue) {
163 // Ignores updates from the audio element.
164 if (this.lastAudioUpdateTime_ === newValue)
167 if (this.audioElement.readyState !== 0)
168 this.audioElement.currentTime = this.audioController.time / 1000;
172 * Invoked when the next button in the controller is clicked.
173 * This handler is registered in the 'on-click' attribute of the element.
175 onControllerNextClicked: function() {
176 this.advance_(true /* forward */, true /* repeat */);
180 * Invoked when the previous button in the controller is clicked.
181 * This handler is registered in the 'on-click' attribute of the element.
183 onControllerPreviousClicked: function() {
184 this.advance_(false /* forward */, true /* repeat */);
188 * Invoked when the playback in the audio element is ended.
189 * This handler is registered in this.ready().
191 onAudioEnded: function() {
193 this.advance_(true /* forward */, this.model.repeat);
197 * Invoked when the playback in the audio element gets error.
198 * This handler is registered in this.ready().
200 onAudioError: function() {
201 this.scheduleAutoAdvance_(true /* forward */, this.model.repeat);
205 * Invoked when the time of playback in the audio element is updated.
206 * This handler is registered in this.ready().
209 onAudioStatusUpdate_: function() {
210 this.audioController.time =
211 (this.lastAudioUpdateTime_ = this.audioElement.currentTime * 1000);
212 this.audioController.duration = this.audioElement.duration * 1000;
213 this.audioController.playing = !this.audioElement.paused;
217 * Invoked when receiving a request to replay the current music from the track
220 onReplayCurrentTrack: function() {
221 // Changes the current time back to the beginning, regardless of the current
222 // status (playing or paused).
223 this.audioElement.currentTime = 0;
224 this.audioController.time = 0;
228 * Goes to the previous or the next track.
229 * @param {boolean} forward True if next, false if previous.
230 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
233 advance_: function(forward, repeat) {
234 this.cancelAutoAdvance_();
236 var nextTrackIndex = this.trackList.getNextTrackIndex(forward, true);
237 var isNextTrackAvailable =
238 (this.trackList.getNextTrackIndex(forward, repeat) !== -1);
240 this.audioController.playing = isNextTrackAvailable;
242 // If there is only a single file in the list, 'currentTrackInde' is not
243 // changed and the handler is not invoked. Instead, plays here.
244 // TODO(yoshiki): clean up the code around here.
245 if (isNextTrackAvailable &&
246 this.trackList.currentTrackIndex == nextTrackIndex) {
247 this.audioElement.play();
250 this.trackList.currentTrackIndex = nextTrackIndex;
254 * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
255 * cancelAutoAdvance_().
259 autoAdvanceTimer_: null,
262 * Schedules automatic advance to the next track after a timeout.
263 * @param {boolean} forward True if next, false if previous.
264 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
267 scheduleAutoAdvance_: function(forward, repeat) {
268 this.cancelAutoAdvance_();
269 var currentTrackIndex = this.currentTrackIndex;
271 var timerId = setTimeout(
273 // If the other timer is scheduled, do nothing.
274 if (this.autoAdvanceTimer_ !== timerId)
277 this.autoAdvanceTimer_ = null;
279 // If the track has been changed since the advance was scheduled, do
281 if (this.currentTrackIndex !== currentTrackIndex)
284 // We are advancing only if the next track is not known to be invalid.
285 // This prevents an endless auto-advancing in the case when all tracks
286 // are invalid (we will only visit each track once).
287 this.advance_(forward, repeat, true /* only if valid */);
291 this.autoAdvanceTimer_ = timerId;
295 * Cancels the scheduled auto advance.
298 cancelAutoAdvance_: function() {
299 if (this.autoAdvanceTimer_) {
300 clearTimeout(this.autoAdvanceTimer_);
301 this.autoAdvanceTimer_ = null;
306 * The index of the current track.
307 * If the list has no tracks, the value must be -1.
311 get currentTrackIndex() {
312 return this.trackList.currentTrackIndex;
314 set currentTrackIndex(value) {
315 this.trackList.currentTrackIndex = value;
319 * The list of the tracks in the playlist.
321 * When it changed, current operation including playback is stopped and
322 * restarts playback with new tracks if necessary.
324 * @type {Array.<AudioPlayer.TrackInfo>}
327 return this.trackList ? this.trackList.tracks : null;
330 if (this.trackList.tracks === tracks)
333 this.cancelAutoAdvance_();
335 this.trackList.tracks = tracks;
336 var currentTrack = this.trackList.getCurrentTrack();
337 if (currentTrack && currentTrack.url != this.audioElement.src) {
338 this.audioElement.src = currentTrack.url;
339 this.audioElement.play();
344 * Invoked when the audio player is being unloaded.
346 onPageUnload: function() {
347 this.audioElement.src = ''; // Hack to prevent crashing.
351 * Invoked when the 'keydown' event is fired.
352 * @param {Event} event The event object.
354 onKeyDown_: function(event) {
355 switch (event.keyIdentifier) {
357 if (this.audioController.volumeSliderShown && this.model.volume < 100)
358 this.model.volume += 1;
361 if (this.audioController.volumeSliderShown && this.model.volume > 0)
362 this.model.volume -= 1;
365 if (this.audioController.volumeSliderShown && this.model.volume < 91)
366 this.model.volume += 10;
369 if (this.audioController.volumeSliderShown && this.model.volume > 9)
370 this.model.volume -= 10;
372 case 'MediaNextTrack':
373 this.onControllerNextClicked();
375 case 'MediaPlayPause':
376 var playing = this.audioController.playing;
377 this.onControllerPlayingChanged(playing, !playing);
379 case 'MediaPreviousTrack':
380 this.onControllerPreviousClicked();
383 // TODO: Define "Stop" behavior.
389 Polymer('audio-player', AudioPlayerElement.prototype);