| 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 |