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' && change.type == 'update') {
28 this.onModelExpandedChanged(change.oldValue, change.object.expanded);
35 this.currentTrackIndex_ = -1;
36 this.playlistGeneration_ = 0;
39 * Whether if the playlist is expanded or not. This value is changed by
40 * this.syncExpanded().
41 * True: expanded, false: collapsed, null: unset.
46 this.isExpanded_ = null; // Initial value is null. It'll be set in load().
49 /** @type {AudioPlayerElement} */ (document.querySelector('audio-player'));
50 // TODO(yoshiki): Move tracks into the model.
51 this.player_.tracks = [];
52 this.player_.model = this.model_;
54 // Run asynchronously after an event of model change is delivered.
55 setTimeout(function() {
56 this.errorString_ = '';
57 this.offlineString_ = '';
58 chrome.fileManagerPrivate.getStrings(function(strings) {
59 container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
60 this.errorString_ = strings['AUDIO_ERROR'];
61 this.offlineString_ = strings['AUDIO_OFFLINE'];
62 AudioPlayer.TrackInfo.DEFAULT_ARTIST =
63 strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
66 this.volumeManager_.addEventListener('externally-unmounted',
67 this.onExternallyUnmounted_.bind(this));
69 window.addEventListener('resize', this.onResize_.bind(this));
71 // Show the window after DOM is processed.
72 var currentWindow = chrome.app.window.current();
74 setTimeout(currentWindow.show.bind(currentWindow), 0);
79 * Initial load method (static).
81 AudioPlayer.load = function() {
82 document.ondragstart = function(e) { e.preventDefault(); };
84 AudioPlayer.instance =
85 new AudioPlayer(document.querySelector('.audio-player'));
94 if (AudioPlayer.instance)
95 AudioPlayer.instance.onUnload();
102 AudioPlayer.instance.load(window.appState);
106 * Loads a new playlist.
107 * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
109 AudioPlayer.prototype.load = function(playlist) {
110 this.playlistGeneration_++;
111 this.currentTrackIndex_ = -1;
113 // Save the app state, in case of restart. Make a copy of the object, so the
114 // playlist member is not changed after entries are resolved.
115 window.appState = JSON.parse(JSON.stringify(playlist)); // cloning
118 this.isExpanded_ = this.model_.expanded;
120 // Resolving entries has to be done after the volume manager is initialized.
121 this.volumeManager_.ensureInitialized(function() {
122 util.URLsToEntries(playlist.items, function(entries) {
123 this.entries_ = entries;
125 var position = playlist.position || 0;
126 var time = playlist.time || 0;
128 if (this.entries_.length == 0)
132 var currentTracks = this.player_.tracks;
133 var unchanged = (currentTracks.length === this.entries_.length);
135 for (var i = 0; i != this.entries_.length; i++) {
136 var entry = this.entries_[i];
137 var onClick = this.select_.bind(this, i);
138 newTracks.push(new AudioPlayer.TrackInfo(entry, onClick));
140 if (unchanged && entry.toURL() !== currentTracks[i].url)
145 this.player_.tracks = newTracks;
147 // Run asynchronously, to makes it sure that the handler of the track list
148 // is called, before the handler of the track index.
149 setTimeout(function() {
150 this.select_(position, !!time);
152 // Load the selected track metadata first, then load the rest.
153 this.loadMetadata_(position);
154 for (i = 0; i != this.entries_.length; i++) {
156 this.loadMetadata_(i);
164 * Loads metadata for a track.
165 * @param {number} track Track number.
168 AudioPlayer.prototype.loadMetadata_ = function(track) {
170 this.entries_[track], this.displayMetadata_.bind(this, track));
174 * Displays track's metadata.
175 * @param {number} track Track number.
176 * @param {Object} metadata Metadata object.
177 * @param {string=} opt_error Error message.
180 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
181 this.player_.tracks[track].setMetadata(metadata, opt_error);
185 * Closes audio player when a volume containing the selected item is unmounted.
186 * @param {Event} event The unmount event.
189 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
190 if (!this.selectedEntry_)
193 if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
199 * Called on window is being unloaded.
201 AudioPlayer.prototype.onUnload = function() {
203 this.player_.onPageUnload();
205 if (this.volumeManager_)
206 this.volumeManager_.dispose();
210 * Selects a new track to play.
211 * @param {number} newTrack New track number.
212 * @param {number} time New playback position (in second).
215 AudioPlayer.prototype.select_ = function(newTrack, time) {
216 if (this.currentTrackIndex_ == newTrack) return;
218 this.currentTrackIndex_ = newTrack;
219 this.player_.currentTrackIndex = this.currentTrackIndex_;
220 this.player_.audioController.time = time;
222 // Run asynchronously after an event of current track change is delivered.
223 setTimeout(function() {
224 if (!window.appReopen)
225 this.player_.audioElement.play();
227 window.appState.position = this.currentTrackIndex_;
228 window.appState.time = 0;
231 var entry = this.entries_[this.currentTrackIndex_];
233 this.fetchMetadata_(entry, function(metadata) {
234 if (this.currentTrackIndex_ != newTrack)
237 this.selectedEntry_ = entry;
243 * @param {FileEntry} entry Track file entry.
244 * @param {function(Object)} callback Callback.
247 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
248 this.metadataModel_.get(
249 [entry], ['mediaTitle', 'mediaArtist', 'present']).then(
250 function(generation, metadata) {
251 // Do nothing if another load happened since the metadata request.
252 if (this.playlistGeneration_ == generation)
253 callback(metadata[0]);
254 }.bind(this, this.playlistGeneration_));
258 * Media error handler.
261 AudioPlayer.prototype.onError_ = function() {
262 var track = this.currentTrackIndex_;
264 this.invalidTracks_[track] = true;
267 this.entries_[track],
269 var error = (!navigator.onLine && !metadata.present) ?
270 this.offlineString_ : this.errorString_;
271 this.displayMetadata_(track, metadata, error);
272 this.scheduleAutoAdvance_();
277 * Toggles the expanded mode when resizing.
279 * @param {Event} event Resize event.
282 AudioPlayer.prototype.onResize_ = function(event) {
283 if (!this.isExpanded_ &&
284 window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
285 this.isExpanded_ = true;
286 this.model_.expanded = true;
287 } else if (this.isExpanded_ &&
288 window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
289 this.isExpanded_ = false;
290 this.model_.expanded = false;
294 /* Keep the below constants in sync with the CSS. */
297 * Window header size in pixels.
301 AudioPlayer.HEADER_HEIGHT = 33; // 32px + border 1px
304 * Track height in pixels.
308 AudioPlayer.TRACK_HEIGHT = 44;
311 * Controls bar height in pixels.
315 AudioPlayer.CONTROLS_HEIGHT = 73; // 72px + border 1px
318 * Default number of items in the expanded mode.
322 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
325 * Minimum size of the window in the expanded mode in pixels.
329 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
330 AudioPlayer.TRACK_HEIGHT * 2;
333 * Invoked when the 'expanded' property in the model is changed.
334 * @param {boolean} oldValue Old value.
335 * @param {boolean} newValue New value.
337 AudioPlayer.prototype.onModelExpandedChanged = function(oldValue, newValue) {
338 if (this.isExpanded_ !== null &&
339 this.isExpanded_ === newValue)
342 if (this.isExpanded_ && !newValue)
343 this.lastExpandedHeight_ = window.innerHeight;
345 if (this.isExpanded_ !== newValue) {
346 this.isExpanded_ = newValue;
350 window.appState.expanded = newValue;
358 AudioPlayer.prototype.syncHeight_ = function() {
361 if (this.model_.expanded) {
363 if (!this.lastExpandedHeight_ ||
364 this.lastExpandedHeight_ < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
365 var expandedListHeight =
366 Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
367 AudioPlayer.TRACK_HEIGHT;
368 targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
369 this.lastExpandedHeight_ = targetHeight;
371 targetHeight = this.lastExpandedHeight_;
375 targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
378 window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
382 * Create a TrackInfo object encapsulating the information about one track.
384 * @param {FileEntry} entry FileEntry to be retrieved the track info from.
385 * @param {function(MouseEvent)} onClick Click handler.
388 AudioPlayer.TrackInfo = function(entry, onClick) {
389 this.url = entry.toURL();
390 this.title = this.getDefaultTitle();
391 this.artist = this.getDefaultArtist();
393 // TODO(yoshiki): implement artwork.
399 * @return {string} Default track title (file name extracted from the url).
401 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
402 var title = this.url.split('/').pop();
403 var dotIndex = title.lastIndexOf('.');
404 if (dotIndex >= 0) title = title.substr(0, dotIndex);
405 title = decodeURIComponent(title);
410 * TODO(kaznacheev): Localize.
412 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
415 * @return {string} 'Unknown artist' string.
417 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
418 return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
422 * @param {Object} metadata The metadata object.
423 * @param {string} error Error string.
425 AudioPlayer.TrackInfo.prototype.setMetadata = function(
427 // TODO(yoshiki): Handle error in better way.
428 // TODO(yoshiki): implement artwork (metadata.thumbnail)
429 this.title = metadata.mediaTitle || this.getDefaultTitle();
430 this.artist = error || metadata.mediaArtist || this.getDefaultArtist();
433 // Starts loading the audio player.
434 window.addEventListener('polymer-ready', function(e) {