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 Polymer('audio-player', {
11 audioController: null,
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.
23 * Model object of the Audio Player.
24 * @type {AudioPlayerModel}
29 * Initializes an element. This method is called automatically when the
33 this.audioController = this.$.audioController;
34 this.audioElement = this.$.audio;
35 this.trackList = this.$.trackList;
37 this.addEventListener('keydown', this.onKeyDown_.bind(this));
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));
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);
56 * Registers handlers for changing of external variables
59 'trackList.currentTrackIndex': 'onCurrentTrackIndexChanged',
60 'audioController.playing': 'onControllerPlayingChanged',
61 'audioController.time': 'onControllerTimeChanged',
62 'model.volume': 'onVolumeChanged',
66 * Invoked when trackList.currentTrackIndex is changed.
67 * @param {number} oldValue old value.
68 * @param {number} newValue new value.
70 onCurrentTrackIndexChanged: function(oldValue, newValue) {
71 var currentTrackUrl = '';
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();
83 // The attributes may be being watched, so we change it at the last.
84 this.currenttrackurl = currentTrackUrl;
88 * Invoked when audioController.playing is changed.
89 * @param {boolean} oldValue old value.
90 * @param {boolean} newValue new value.
92 onControllerPlayingChanged: function(oldValue, newValue) {
93 this.playing = 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;
103 if (this.audioElement.src) {
104 this.currenttrackurl = this.audioElement.src;
105 this.audioElement.play();
110 // When the new status is "stopped".
111 this.cancelAutoAdvance_();
112 this.audioElement.pause();
113 this.currenttrackurl = '';
114 this.lastAudioUpdateTime_ = null;
118 * Invoked when audioController.volume is changed.
119 * @param {number} oldValue old value.
120 * @param {number} newValue new value.
122 onVolumeChanged: function(oldValue, newValue) {
123 this.audioElement.volume = newValue / 100;
127 * Invoked when the model changed.
128 * @param {AudioPlayerModel} oldValue Old Value.
129 * @param {AudioPlayerModel} newValue New Value.
131 modelChanged: function(oldValue, newValue) {
132 this.trackList.model = newValue;
133 this.audioController.model = newValue;
135 // Invoke the handler manually.
136 this.onVolumeChanged(0, newValue.volume);
140 * Invoked when audioController.time is changed.
141 * @param {number} oldValue old time (in ms).
142 * @param {number} newValue new time (in ms).
144 onControllerTimeChanged: function(oldValue, newValue) {
145 // Ignores updates from the audio element.
146 if (this.lastAudioUpdateTime_ === newValue)
149 if (this.audioElement.readyState !== 0)
150 this.audioElement.currentTime = this.audioController.time / 1000;
154 * Invoked when the next button in the controller is clicked.
155 * This handler is registered in the 'on-click' attribute of the element.
157 onControllerNextClicked: function() {
158 this.advance_(true /* forward */, true /* repeat */);
162 * Invoked when the previous button in the controller is clicked.
163 * This handler is registered in the 'on-click' attribute of the element.
165 onControllerPreviousClicked: function() {
166 this.advance_(false /* forward */, true /* repeat */);
170 * Invoked when the playback in the audio element is ended.
171 * This handler is registered in this.ready().
173 onAudioEnded: function() {
175 this.advance_(true /* forward */, this.model.repeat);
179 * Invoked when the playback in the audio element gets error.
180 * This handler is registered in this.ready().
182 onAudioError: function() {
183 this.scheduleAutoAdvance_(true /* forward */, this.model.repeat);
187 * Invoked when the time of playback in the audio element is updated.
188 * This handler is registered in this.ready().
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;
199 * Invoked when receiving a request to replay the current music from the track
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;
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.
215 advance_: function(forward, repeat) {
216 this.cancelAutoAdvance_();
218 var nextTrackIndex = this.trackList.getNextTrackIndex(forward, true);
219 var isNextTrackAvailable =
220 (this.trackList.getNextTrackIndex(forward, repeat) !== -1);
222 this.audioController.playing = isNextTrackAvailable;
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();
232 this.trackList.currentTrackIndex = nextTrackIndex;
234 Platform.performMicrotaskCheckpoint();
238 * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
239 * cancelAutoAdvance_().
243 autoAdvanceTimer_: null,
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.
251 scheduleAutoAdvance_: function(forward, repeat) {
252 this.cancelAutoAdvance_();
253 this.autoAdvanceTimer_ = setTimeout(
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 */);
265 * Cancels the scheduled auto advance.
268 cancelAutoAdvance_: function() {
269 if (this.autoAdvanceTimer_) {
270 clearTimeout(this.autoAdvanceTimer_);
271 this.autoAdvanceTimer_ = null;
276 * The index of the current track.
277 * If the list has no tracks, the value must be -1.
281 get currentTrackIndex() {
282 return this.trackList.currentTrackIndex;
284 set currentTrackIndex(value) {
285 this.trackList.currentTrackIndex = value;
289 * The list of the tracks in the playlist.
291 * When it changed, current operation including playback is stopped and
292 * restarts playback with new tracks if necessary.
294 * @type {Array.<AudioPlayer.TrackInfo>}
297 return this.trackList ? this.trackList.tracks : null;
300 if (this.trackList.tracks === tracks)
303 this.cancelAutoAdvance_();
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();
314 * Invoked when the audio player is being unloaded.
316 onPageUnload: function() {
317 this.audioElement.src = ''; // Hack to prevent crashing.
321 * Invoked the 'keydown' event is fired.
322 * @param {Event} event The event object.
324 onKeyDown_: function(event) {
325 switch (event.keyIdentifier) {
327 if (this.audioController.volumeSliderShown && this.model.volume < 100)
328 this.model.volume += 1;
331 if (this.audioController.volumeSliderShown && this.model.volume > 0)
332 this.model.volume -= 1;
335 if (this.audioController.volumeSliderShown && this.model.volume < 91)
336 this.model.volume += 10;
339 if (this.audioController.volumeSliderShown && this.model.volume > 9)
340 this.model.volume -= 10;