1 // Copyright (c) 2012 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.
6 * Overrided metadata worker's path.
9 ContentMetadataProvider
.WORKER_SCRIPT
= '/js/metadata_worker.js';
12 * @param {Element} container Container element.
15 function AudioPlayer(container
) {
16 this.container_
= container
;
17 this.volumeManager_
= new VolumeManagerWrapper(
18 VolumeManagerWrapper
.NonNativeVolumeStatus
.ENABLED
);
19 this.metadataModel_
= MetadataModel
.create(this.volumeManager_
);
20 this.selectedEntry_
= null;
21 this.invalidTracks_
= {};
23 this.model_
= new AudioPlayerModel();
24 Object
.observe(this.model_
, function(changes
) {
25 for (var i
= 0; i
< changes
.length
; i
++) {
26 var change
= changes
[i
];
27 if (change
.name
== 'expanded' &&
28 (change
.type
== 'add' || change
.type
== 'update')) {
29 this.onModelExpandedChanged(change
.oldValue
, change
.object
.expanded
);
36 this.currentTrackIndex_
= -1;
37 this.playlistGeneration_
= 0;
40 * Whether if the playlist is expanded or not. This value is changed by
41 * this.syncExpanded().
42 * True: expanded, false: collapsed, null: unset.
47 this.isExpanded_
= null; // Initial value is null. It'll be set in load().
50 /** @type {AudioPlayerElement} */ (document
.querySelector('audio-player'));
51 // TODO(yoshiki): Move tracks into the model.
52 this.player_
.tracks
= [];
53 this.model_
.initialize(function() {
54 this.player_
.model
= this.model_
;
57 // Run asynchronously after an event of model change is delivered.
58 setTimeout(function() {
59 this.errorString_
= '';
60 this.offlineString_
= '';
61 chrome
.fileManagerPrivate
.getStrings(function(strings
) {
62 container
.ownerDocument
.title
= strings
['AUDIO_PLAYER_TITLE'];
63 this.errorString_
= strings
['AUDIO_ERROR'];
64 this.offlineString_
= strings
['AUDIO_OFFLINE'];
65 AudioPlayer
.TrackInfo
.DEFAULT_ARTIST
=
66 strings
['AUDIO_PLAYER_DEFAULT_ARTIST'];
69 this.volumeManager_
.addEventListener('externally-unmounted',
70 this.onExternallyUnmounted_
.bind(this));
72 window
.addEventListener('resize', this.onResize_
.bind(this));
73 document
.addEventListener('keydown', this.onKeyDown_
.bind(this));
75 // Show the window after DOM is processed.
76 var currentWindow
= chrome
.app
.window
.current();
78 setTimeout(currentWindow
.show
.bind(currentWindow
), 0);
83 * Initial load method (static).
85 AudioPlayer
.load = function() {
86 document
.ondragstart = function(e
) { e
.preventDefault(); };
88 AudioPlayer
.instance
=
89 new AudioPlayer(document
.querySelector('.audio-player'));
98 if (AudioPlayer
.instance
)
99 AudioPlayer
.instance
.onUnload();
103 * Reloads the player.
106 AudioPlayer
.instance
.load(/** @type {Playlist} */ (window
.appState
));
110 * Loads a new playlist.
111 * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
113 AudioPlayer
.prototype.load = function(playlist
) {
114 this.playlistGeneration_
++;
115 this.currentTrackIndex_
= -1;
117 // Save the app state, in case of restart. Make a copy of the object, so the
118 // playlist member is not changed after entries are resolved.
119 window
.appState
= /** @type {Playlist} */ (
120 JSON
.parse(JSON
.stringify(playlist
))); // cloning
123 this.isExpanded_
= this.player_
.expanded
;
125 // Resolving entries has to be done after the volume manager is initialized.
126 this.volumeManager_
.ensureInitialized(function() {
127 util
.URLsToEntries(playlist
.items
|| [], function(entries
) {
128 this.entries_
= entries
;
130 var position
= playlist
.position
|| 0;
131 var time
= playlist
.time
|| 0;
133 if (this.entries_
.length
== 0)
137 var currentTracks
= this.player_
.tracks
;
138 var unchanged
= (currentTracks
.length
=== this.entries_
.length
);
140 for (var i
= 0; i
!= this.entries_
.length
; i
++) {
141 var entry
= this.entries_
[i
];
142 newTracks
.push(new AudioPlayer
.TrackInfo(entry
));
144 if (unchanged
&& entry
.toURL() !== currentTracks
[i
].url
)
149 this.player_
.tracks
= newTracks
;
151 // Run asynchronously, to makes it sure that the handler of the track list
152 // is called, before the handler of the track index.
153 setTimeout(function() {
154 this.select_(position
);
156 // Load the selected track metadata first, then load the rest.
157 this.loadMetadata_(position
);
158 for (i
= 0; i
!= this.entries_
.length
; i
++) {
160 this.loadMetadata_(i
);
168 * Loads metadata for a track.
169 * @param {number} track Track number.
172 AudioPlayer
.prototype.loadMetadata_ = function(track
) {
174 this.entries_
[track
], this.displayMetadata_
.bind(this, track
));
178 * Displays track's metadata.
179 * @param {number} track Track number.
180 * @param {Object} metadata Metadata object.
181 * @param {string=} opt_error Error message.
184 AudioPlayer
.prototype.displayMetadata_ = function(track
, metadata
, opt_error
) {
185 this.player_
.tracks
[track
].setMetadata(metadata
, opt_error
);
189 * Closes audio player when a volume containing the selected item is unmounted.
190 * @param {Event} event The unmount event.
193 AudioPlayer
.prototype.onExternallyUnmounted_ = function(event
) {
194 if (!this.selectedEntry_
)
197 if (this.volumeManager_
.getVolumeInfo(this.selectedEntry_
) ===
203 * Called on window is being unloaded.
205 AudioPlayer
.prototype.onUnload = function() {
207 this.player_
.onPageUnload();
209 if (this.volumeManager_
)
210 this.volumeManager_
.dispose();
214 * Selects a new track to play.
215 * @param {number} newTrack New track number.
218 AudioPlayer
.prototype.select_ = function(newTrack
) {
219 if (this.currentTrackIndex_
== newTrack
) return;
221 this.currentTrackIndex_
= newTrack
;
222 this.player_
.currentTrackIndex
= this.currentTrackIndex_
;
223 this.player_
.time
= 0;
225 // Run asynchronously after an event of current track change is delivered.
226 setTimeout(function() {
227 if (!window
.appReopen
)
228 this.player_
.$.audio
.play();
230 window
.appState
.position
= this.currentTrackIndex_
;
231 window
.appState
.time
= 0;
234 var entry
= this.entries_
[this.currentTrackIndex_
];
236 this.fetchMetadata_(entry
, function(metadata
) {
237 if (this.currentTrackIndex_
!= newTrack
)
240 this.selectedEntry_
= entry
;
246 * @param {FileEntry} entry Track file entry.
247 * @param {function(Object)} callback Callback.
250 AudioPlayer
.prototype.fetchMetadata_ = function(entry
, callback
) {
251 this.metadataModel_
.get(
252 [entry
], ['mediaTitle', 'mediaArtist', 'present']).then(
253 function(generation
, metadata
) {
254 // Do nothing if another load happened since the metadata request.
255 if (this.playlistGeneration_
== generation
)
256 callback(metadata
[0]);
257 }.bind(this, this.playlistGeneration_
));
261 * Media error handler.
264 AudioPlayer
.prototype.onError_ = function() {
265 var track
= this.currentTrackIndex_
;
267 this.invalidTracks_
[track
] = true;
270 this.entries_
[track
],
272 var error
= (!navigator
.onLine
&& !metadata
.present
) ?
273 this.offlineString_
: this.errorString_
;
274 this.displayMetadata_(track
, metadata
, error
);
275 this.player_
.onAudioError();
280 * Toggles the expanded mode when resizing.
282 * @param {Event} event Resize event.
285 AudioPlayer
.prototype.onResize_ = function(event
) {
286 if (!this.isExpanded_
&&
287 window
.innerHeight
>= AudioPlayer
.EXPANDED_MODE_MIN_HEIGHT
) {
288 this.isExpanded_
= true;
289 this.player_
.expanded
= true;
290 } else if (this.isExpanded_
&&
291 window
.innerHeight
< AudioPlayer
.EXPANDED_MODE_MIN_HEIGHT
) {
292 this.isExpanded_
= false;
293 this.player_
.expanded
= false;
298 * Handles keydown event to open inspector with shortcut keys.
300 * @param {Event} event KeyDown event.
303 AudioPlayer
.prototype.onKeyDown_ = function(event
) {
304 switch (util
.getKeyModifiers(event
) + event
.keyIdentifier
) {
305 // Handle debug shortcut keys.
306 case 'Ctrl-Shift-U+0049': // Ctrl+Shift+I
307 chrome
.fileManagerPrivate
.openInspector('normal');
309 case 'Ctrl-Shift-U+004A': // Ctrl+Shift+J
310 chrome
.fileManagerPrivate
.openInspector('console');
312 case 'Ctrl-Shift-U+0043': // Ctrl+Shift+C
313 chrome
.fileManagerPrivate
.openInspector('element');
315 case 'Ctrl-Shift-U+0042': // Ctrl+Shift+B
316 chrome
.fileManagerPrivate
.openInspector('background');
321 /* Keep the below constants in sync with the CSS. */
324 * Window header size in pixels.
328 AudioPlayer
.HEADER_HEIGHT
= 33; // 32px + border 1px
331 * Track height in pixels.
335 AudioPlayer
.TRACK_HEIGHT
= 44;
338 * Controls bar height in pixels.
342 AudioPlayer
.CONTROLS_HEIGHT
= 73; // 72px + border 1px
345 * Default number of items in the expanded mode.
349 AudioPlayer
.DEFAULT_EXPANDED_ITEMS
= 5;
352 * Minimum size of the window in the expanded mode in pixels.
356 AudioPlayer
.EXPANDED_MODE_MIN_HEIGHT
= AudioPlayer
.CONTROLS_HEIGHT
+
357 AudioPlayer
.TRACK_HEIGHT
* 2;
360 * Invoked when the 'expanded' property in the model is changed.
361 * @param {boolean} oldValue Old value.
362 * @param {boolean} newValue New value.
364 AudioPlayer
.prototype.onModelExpandedChanged = function(oldValue
, newValue
) {
365 if (this.isExpanded_
!== null &&
366 this.isExpanded_
=== newValue
)
369 if (this.isExpanded_
&& !newValue
)
370 this.lastExpandedHeight_
= window
.innerHeight
;
372 if (this.isExpanded_
!== newValue
) {
373 this.isExpanded_
= newValue
;
377 window
.appState
.expanded
= newValue
;
385 AudioPlayer
.prototype.syncHeight_ = function() {
388 if (this.player_
.expanded
) {
390 if (!this.lastExpandedHeight_
||
391 this.lastExpandedHeight_
< AudioPlayer
.EXPANDED_MODE_MIN_HEIGHT
) {
392 var expandedListHeight
=
393 Math
.min(this.entries_
.length
, AudioPlayer
.DEFAULT_EXPANDED_ITEMS
) *
394 AudioPlayer
.TRACK_HEIGHT
;
395 targetHeight
= AudioPlayer
.CONTROLS_HEIGHT
+ expandedListHeight
;
396 this.lastExpandedHeight_
= targetHeight
;
398 targetHeight
= this.lastExpandedHeight_
;
402 targetHeight
= AudioPlayer
.CONTROLS_HEIGHT
+ AudioPlayer
.TRACK_HEIGHT
;
405 window
.resizeTo(window
.innerWidth
, targetHeight
+ AudioPlayer
.HEADER_HEIGHT
);
409 * Create a TrackInfo object encapsulating the information about one track.
411 * @param {FileEntry} entry FileEntry to be retrieved the track info from.
414 AudioPlayer
.TrackInfo = function(entry
) {
415 this.url
= entry
.toURL();
416 this.title
= this.getDefaultTitle();
417 this.artist
= this.getDefaultArtist();
419 // TODO(yoshiki): implement artwork.
425 * @return {string} Default track title (file name extracted from the url).
427 AudioPlayer
.TrackInfo
.prototype.getDefaultTitle = function() {
428 var title
= this.url
.split('/').pop();
429 var dotIndex
= title
.lastIndexOf('.');
430 if (dotIndex
>= 0) title
= title
.substr(0, dotIndex
);
431 title
= decodeURIComponent(title
);
436 * TODO(kaznacheev): Localize.
438 AudioPlayer
.TrackInfo
.DEFAULT_ARTIST
= 'Unknown Artist';
441 * @return {string} 'Unknown artist' string.
443 AudioPlayer
.TrackInfo
.prototype.getDefaultArtist = function() {
444 return AudioPlayer
.TrackInfo
.DEFAULT_ARTIST
;
448 * @param {Object} metadata The metadata object.
449 * @param {string} error Error string.
451 AudioPlayer
.TrackInfo
.prototype.setMetadata = function(
453 // TODO(yoshiki): Handle error in better way.
454 // TODO(yoshiki): implement artwork (metadata.thumbnail)
455 this.title
= metadata
.mediaTitle
|| this.getDefaultTitle();
456 this.artist
= error
|| metadata
.mediaArtist
|| this.getDefaultArtist();
459 // Starts loading the audio player.
460 window
.addEventListener('DOMContentLoaded', function(e
) {