Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / audio_player / elements / audio_player.js
blob3d5fc01410f187a30115251c438f044aed636760
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({
6   is: 'audio-player',
8   properties: {
9     /**
10      * Flag whether the audio is playing or paused. True if playing, or false
11      * paused.
12      */
13     playing: {
14       type: Boolean,
15       observer: 'playingChanged',
16       reflectToAttribute: true
17     },
19     /**
20      * Current elapsed time in the current music in millisecond.
21      */
22     time: {
23       type: Number,
24       observer: 'timeChanged'
25     },
27     /**
28      * Whether the shuffle button is ON.
29      */
30     shuffle: {
31       type: Boolean,
32       observer: 'shuffleChanged'
33     },
35     /**
36      * Whether the repeat button is ON.
37      */
38     repeat: {
39       type: Boolean,
40       observer: 'repeatChanged'
41     },
43     /**
44      * The audio volume. 0 is silent, and 100 is maximum loud.
45      */
46     volume: {
47       type: Number,
48       observer: 'volumeChanged'
49     },
51     /**
52      * Whether the expanded button is ON.
53      */
54     expanded: {
55       type: Boolean,
56       observer: 'expandedChanged'
57     },
59     /**
60      * Track index of the current track.
61      */
62     currentTrackIndex: {
63       type: Number,
64       observer: 'currentTrackIndexChanged'
65     },
67     /**
68      * Model object of the Audio Player.
69      * @type {AudioPlayerModel}
70      */
71     model: {
72       type: Object,
73       value: null,
74       observer: 'modelChanged'
75     },
77     /**
78      * URL of the current track. (exposed publicly for tests)
79      */
80     currenttrackurl: {
81       type: String,
82       value: '',
83       reflectToAttribute: true
84     },
86     /**
87      * The number of played tracks. (exposed publicly for tests)
88      */
89     playcount: {
90       type: Number,
91       value: 0,
92       reflectToAttribute: true
93     }
94   },
96   /**
97    * Handles change event for shuffle mode.
98    * @param {boolean} shuffle
99    */
100   shuffleChanged: function(shuffle) {
101     if (this.model)
102       this.model.shuffle = shuffle;
103   },
105   /**
106    * Handles change event for repeat mode.
107    * @param {boolean} repeat
108    */
109   repeatChanged: function(repeat) {
110     if (this.model)
111       this.model.repeat = repeat;
112   },
114   /**
115    * Handles change event for audio volume.
116    * @param {number} volume
117    */
118   volumeChanged: function(volume) {
119     if (this.model)
120       this.model.volume = volume;
121   },
123   /**
124    * Handles change event for expanded state of track list.
125    */
126   expandedChanged: function(expanded) {
127     if (this.model)
128       this.model.expanded = expanded;
129   },
131   /**
132    * Initializes an element. This method is called automatically when the
133    * element is ready.
134    */
135   ready: function() {
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);
152   },
154   /**
155    * Invoked when trackList.currentTrackIndex is changed.
156    * @param {number} newValue new value.
157    * @param {number} oldValue old value.
158    */
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;
167         if (this.playing)
168           this.$.audio.play();
169       }
170     }
172     // The attributes may be being watched, so we change it at the last.
173     this.currenttrackurl = currentTrackUrl;
174   },
176   /**
177    * Invoked when playing is changed.
178    * @param {boolean} newValue new value.
179    * @param {boolean} oldValue old value.
180    */
181   playingChanged: function(newValue, oldValue) {
182     if (newValue) {
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;
187         }
188       }
190       if (this.$.audio.src) {
191         this.currenttrackurl = this.$.audio.src;
192         this.$.audio.play();
193         return;
194       }
195     }
197     // When the new status is "stopped".
198     this.cancelAutoAdvance_();
199     this.$.audio.pause();
200     this.currenttrackurl = '';
201     this.lastAudioUpdateTime_ = null;
202   },
204   /**
205    * Invoked when the model changed.
206    * @param {AudioPlayerModel} newModel New model.
207    * @param {AudioPlayerModel} oldModel Old model.
208    */
209   modelChanged: function(newModel, oldModel) {
210     // Setting up the UI
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;
216     }
217   },
219   /**
220    * Invoked when time is changed.
221    * @param {number} newValue new time (in ms).
222    * @param {number} oldValue old time (in ms).
223    */
224   timeChanged: function(newValue, oldValue) {
225     // Ignores updates from the audio element.
226     if (this.lastAudioUpdateTime_ === newValue)
227       return;
229     if (this.$.audio.readyState !== 0)
230       this.$.audio.currentTime = this.time / 1000;
231   },
233   /**
234    * Invoked when the next button in the controller is clicked.
235    * This handler is registered in the 'on-click' attribute of the element.
236    */
237   onControllerNextClicked: function() {
238     this.advance_(true /* forward */, true /* repeat */);
239   },
241   /**
242    * Invoked when the previous button in the controller is clicked.
243    * This handler is registered in the 'on-click' attribute of the element.
244    */
245   onControllerPreviousClicked: function() {
246     this.advance_(false /* forward */, true /* repeat */);
247   },
249   /**
250    * Invoked when the playback in the audio element is ended.
251    * This handler is registered in this.ready().
252    */
253   onAudioEnded: function() {
254     this.playcount++;
255     this.advance_(true /* forward */, this.repeat);
256   },
258   /**
259    * Invoked when the playback in the audio element gets error.
260    * This handler is registered in this.ready().
261    */
262   onAudioError: function() {
263     this.scheduleAutoAdvance_(true /* forward */, this.repeat);
264   },
266   /**
267    * Invoked when the time of playback in the audio element is updated.
268    * This handler is registered in this.ready().
269    * @private
270    */
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;
275   },
277   /**
278    * Invoked when receiving a request to replay the current music from the track
279    * list element.
280    */
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;
285     this.time = 0;
286   },
288   /**
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.
292    * @private
293    */
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) {
308       this.$.audio.play();
309     }
311     this.$.trackList.currentTrackIndex = nextTrackIndex;
312   },
314   /**
315    * Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
316    *     cancelAutoAdvance_().
317    * @type {number?}
318    * @private
319    */
320   autoAdvanceTimer_: null,
322   /**
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.
326    * @private
327    */
328   scheduleAutoAdvance_: function(forward, repeat) {
329     this.cancelAutoAdvance_();
330     var currentTrackIndex = this.currentTrackIndex;
332     var timerId = setTimeout(
333         function() {
334           // If the other timer is scheduled, do nothing.
335           if (this.autoAdvanceTimer_ !== timerId)
336             return;
338           this.autoAdvanceTimer_ = null;
340           // If the track has been changed since the advance was scheduled, do
341           // nothing.
342           if (this.currentTrackIndex !== currentTrackIndex)
343             return;
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);
349         }.bind(this),
350         3000);
352     this.autoAdvanceTimer_ = timerId;
353   },
355   /**
356    * Cancels the scheduled auto advance.
357    * @private
358    */
359   cancelAutoAdvance_: function() {
360     if (this.autoAdvanceTimer_) {
361       clearTimeout(this.autoAdvanceTimer_);
362       this.autoAdvanceTimer_ = null;
363     }
364   },
366   /**
367    * The list of the tracks in the playlist.
368    *
369    * When it changed, current operation including playback is stopped and
370    * restarts playback with new tracks if necessary.
371    *
372    * @type {Array<TrackInfo>}
373    */
374   get tracks() {
375     return this.$.trackList ? this.$.trackList.tracks : null;
376   },
377   set tracks(tracks) {
378     if (this.$.trackList.tracks === tracks)
379       return;
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;
387       this.$.audio.play();
388     }
389   },
391   /**
392    * Invoked when the audio player is being unloaded.
393    */
394   onPageUnload: function() {
395     this.$.audio.src = '';  // Hack to prevent crashing.
396   },
398   /**
399    * Invoked when the 'keydown' event is fired.
400    * @param {Event} event The event object.
401    */
402   onKeyDown_: function(event) {
403     switch (event.keyIdentifier) {
404       case 'Up':
405         if (this.$.audioController.volumeSliderShown && this.model.volume < 100)
406           this.model.volume += 1;
407         break;
408       case 'Down':
409         if (this.$.audioController.volumeSliderShown && this.model.volume > 0)
410           this.model.volume -= 1;
411         break;
412       case 'PageUp':
413         if (this.$.audioController.volumeSliderShown && this.model.volume < 91)
414           this.model.volume += 10;
415         break;
416       case 'PageDown':
417         if (this.$.audioController.volumeSliderShown && this.model.volume > 9)
418           this.model.volume -= 10;
419         break;
420       case 'MediaNextTrack':
421         this.onControllerNextClicked();
422         break;
423       case 'MediaPlayPause':
424         this.playing = !this.playing;
425         break;
426       case 'MediaPreviousTrack':
427         this.onControllerPreviousClicked();
428         break;
429       case 'MediaStop':
430         // TODO: Define "Stop" behavior.
431         break;
432     }
433   },
435   /**
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])
438    * @return {number}
439    */
440   computeAudioVolume_: function(volume) {
441     return volume / 100;
442   }