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.
5 Polymer('audio-player', {
13 // Attributes of the element (lower characters only).
14 // These values must be used only to data binding and shouldn't be assigned
15 // any value nowhere except in the handler.
32 * Model object of the Audio Player.
33 * @type {AudioPlayerModel}
38 * Initializes an element. This method is called automatically when the
42 this.audioController = this.$.audioController;
43 this.audioElement = this.$.audio;
44 this.trackList = this.$.trackList;
46 this.addEventListener('keydown', this.onKeyDown_.bind(this));
48 this.audioElement.volume = 0; // Temporary initial volume.
49 this.audioElement.addEventListener('ended', this.onAudioEnded.bind(this));
50 this.audioElement.addEventListener('error', this.onAudioError.bind(this));
52 var onAudioStatusUpdatedBound = this.onAudioStatusUpdate_.bind(this);
53 this.audioElement.addEventListener('timeupdate', onAudioStatusUpdatedBound);
54 this.audioElement.addEventListener('ended', onAudioStatusUpdatedBound);
55 this.audioElement.addEventListener('play', onAudioStatusUpdatedBound);
56 this.audioElement.addEventListener('pause', onAudioStatusUpdatedBound);
57 this.audioElement.addEventListener('suspend', onAudioStatusUpdatedBound);
58 this.audioElement.addEventListener('abort', onAudioStatusUpdatedBound);
59 this.audioElement.addEventListener('error', onAudioStatusUpdatedBound);
60 this.audioElement.addEventListener('emptied', onAudioStatusUpdatedBound);
61 this.audioElement.addEventListener('stalled', onAudioStatusUpdatedBound);
65 * Registers handlers for changing of external variables
68 'trackList.currentTrackIndex': 'onCurrentTrackIndexChanged',
69 'audioController.playing': 'onControllerPlayingChanged',
70 'audioController.time': 'onControllerTimeChanged',
71 'model.volume': 'onVolumeChanged',
75 * Invoked when trackList.currentTrackIndex is changed.
76 * @param {number} oldValue old value.
77 * @param {number} newValue new value.
79 onCurrentTrackIndexChanged: function(oldValue, newValue) {
80 var currentTrackUrl = '';
82 if (oldValue != newValue) {
83 var currentTrack = this.trackList.getCurrentTrack();
84 if (currentTrack && currentTrack.url != this.audioElement.src) {
85 this.audioElement.src = currentTrack.url;
86 currentTrackUrl = this.audioElement.src;
87 if (this.audioController.playing)
88 this.audioElement.play();
92 // The attributes may be being watched, so we change it at the last.
93 this.currenttrackurl = currentTrackUrl;
97 * Invoked when audioController.playing is changed.
98 * @param {boolean} oldValue old value.
99 * @param {boolean} newValue new value.
101 onControllerPlayingChanged: function(oldValue, newValue) {
102 this.playing = newValue;
105 if (!this.audioElement.src) {
106 var currentTrack = this.trackList.getCurrentTrack();
107 if (currentTrack && currentTrack.url != this.audioElement.src) {
108 this.audioElement.src = currentTrack.url;
112 if (this.audioElement.src) {
113 this.currenttrackurl = this.audioElement.src;
114 this.audioElement.play();
119 // When the new status is "stopped".
120 this.cancelAutoAdvance_();
121 this.audioElement.pause();
122 this.currenttrackurl = '';
123 this.lastAudioUpdateTime_ = null;
127 * Invoked when audioController.volume is changed.
128 * @param {number} oldValue old value.
129 * @param {number} newValue new value.
131 onVolumeChanged: function(oldValue, newValue) {
132 this.audioElement.volume = newValue / 100;
136 * Invoked when the model changed.
137 * @param {AudioPlayerModel} oldValue Old Value.
138 * @param {AudioPlayerModel} newValue New Value.
140 modelChanged: function(oldValue, newValue) {
141 this.trackList.model = newValue;
142 this.audioController.model = newValue;
144 // Invoke the handler manually.
145 this.onVolumeChanged(0, newValue.volume);
149 * Invoked when audioController.time is changed.
150 * @param {number} oldValue old time (in ms).
151 * @param {number} newValue new time (in ms).
153 onControllerTimeChanged: function(oldValue, newValue) {
154 // Ignores updates from the audio element.
155 if (this.lastAudioUpdateTime_ === newValue)
158 if (this.audioElement.readyState !== 0)
159 this.audioElement.currentTime = this.audioController.time / 1000;
163 * Invoked when the next button in the controller is clicked.
164 * This handler is registered in the 'on-click' attribute of the element.
166 onControllerNextClicked: function() {
167 this.advance_(true /* forward */, true /* repeat */);
171 * Invoked when the previous button in the controller is clicked.
172 * This handler is registered in the 'on-click' attribute of the element.
174 onControllerPreviousClicked: function() {
175 this.advance_(false /* forward */, true /* repeat */);
179 * Invoked when the playback in the audio element is ended.
180 * This handler is registered in this.ready().
182 onAudioEnded: function() {
184 this.advance_(true /* forward */, this.model.repeat);
188 * Invoked when the playback in the audio element gets error.
189 * This handler is registered in this.ready().
191 onAudioError: function() {
192 this.scheduleAutoAdvance_(true /* forward */, this.model.repeat);
196 * Invoked when the time of playback in the audio element is updated.
197 * This handler is registered in this.ready().
200 onAudioStatusUpdate_: function() {
201 this.audioController.time =
202 (this.lastAudioUpdateTime_ = this.audioElement.currentTime * 1000);
203 this.audioController.duration = this.audioElement.duration * 1000;
204 this.audioController.playing = !this.audioElement.paused;
208 * Invoked when receiving a request to replay the current music from the track
211 onReplayCurrentTrack: function() {
212 // Changes the current time back to the beginning, regardless of the current
213 // status (playing or paused).
214 this.audioElement.currentTime = 0;
215 this.audioController.time = 0;
219 * Goes to the previous or the next track.
220 * @param {boolean} forward True if next, false if previous.
221 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
224 advance_: function(forward, repeat) {
225 this.cancelAutoAdvance_();
227 var nextTrackIndex = this.trackList.getNextTrackIndex(forward, true);
228 var isNextTrackAvailable =
229 (this.trackList.getNextTrackIndex(forward, repeat) !== -1);
231 this.audioController.playing = isNextTrackAvailable;
233 // If there is only a single file in the list, 'currentTrackInde' is not
234 // changed and the handler is not invoked. Instead, plays here.
235 // TODO(yoshiki): clean up the code around here.
236 if (isNextTrackAvailable &&
237 this.trackList.currentTrackIndex == nextTrackIndex) {
238 this.audioElement.play();
241 this.trackList.currentTrackIndex = nextTrackIndex;
243 Platform.performMicrotaskCheckpoint();
247 * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
248 * cancelAutoAdvance_().
252 autoAdvanceTimer_: null,
255 * Schedules automatic advance to the next track after a timeout.
256 * @param {boolean} forward True if next, false if previous.
257 * @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
260 scheduleAutoAdvance_: function(forward, repeat) {
261 this.cancelAutoAdvance_();
262 var currentTrackIndex = this.currentTrackIndex;
264 var timerId = setTimeout(
266 // If the other timer is scheduled, do nothing.
267 if (this.autoAdvanceTimer_ !== timerId)
270 this.autoAdvanceTimer_ = null;
272 // If the track has been changed since the advance was scheduled, do
274 if (this.currentTrackIndex !== currentTrackIndex)
277 // We are advancing only if the next track is not known to be invalid.
278 // This prevents an endless auto-advancing in the case when all tracks
279 // are invalid (we will only visit each track once).
280 this.advance_(forward, repeat, true /* only if valid */);
284 this.autoAdvanceTimer_ = timerId;
288 * Cancels the scheduled auto advance.
291 cancelAutoAdvance_: function() {
292 if (this.autoAdvanceTimer_) {
293 clearTimeout(this.autoAdvanceTimer_);
294 this.autoAdvanceTimer_ = null;
299 * The index of the current track.
300 * If the list has no tracks, the value must be -1.
304 get currentTrackIndex() {
305 return this.trackList.currentTrackIndex;
307 set currentTrackIndex(value) {
308 this.trackList.currentTrackIndex = value;
312 * The list of the tracks in the playlist.
314 * When it changed, current operation including playback is stopped and
315 * restarts playback with new tracks if necessary.
317 * @type {Array.<AudioPlayer.TrackInfo>}
320 return this.trackList ? this.trackList.tracks : null;
323 if (this.trackList.tracks === tracks)
326 this.cancelAutoAdvance_();
328 this.trackList.tracks = tracks;
329 var currentTrack = this.trackList.getCurrentTrack();
330 if (currentTrack && currentTrack.url != this.audioElement.src) {
331 this.audioElement.src = currentTrack.url;
332 this.audioElement.play();
337 * Invoked when the audio player is being unloaded.
339 onPageUnload: function() {
340 this.audioElement.src = ''; // Hack to prevent crashing.
344 * Invoked when the 'keydown' event is fired.
345 * @param {Event} event The event object.
347 onKeyDown_: function(event) {
348 switch (event.keyIdentifier) {
350 if (this.audioController.volumeSliderShown && this.model.volume < 100)
351 this.model.volume += 1;
354 if (this.audioController.volumeSliderShown && this.model.volume > 0)
355 this.model.volume -= 1;
358 if (this.audioController.volumeSliderShown && this.model.volume < 91)
359 this.model.volume += 10;
362 if (this.audioController.volumeSliderShown && this.model.volume > 9)
363 this.model.volume -= 10;
365 case 'MediaNextTrack':
366 this.onControllerNextClicked();
368 case 'MediaPlayPause':
369 var playing = this.audioController.playing;
370 this.onControllerPlayingChanged(playing, !playing);
372 case 'MediaPreviousTrack':
373 this.onControllerPreviousClicked();
376 // TODO: Define "Stop" behavior.