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