Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / ui / file_manager / video_player / js / cast / cast_video_element.js
blob0aa1f3fc5c18c4961a2a8550c6fef9a1c65398ad
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 /**
6 * Interval for updating media info (in ms).
7 * @type {number}
8 * @const
9 */
10 var MEDIA_UPDATE_INTERVAL = 250;
12 /**
13 * The namespace for communication between the cast and the player.
14 * @type {string}
15 * @const
17 var CAST_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.chromeos.videoplayer';
19 /**
20 * This class is the dummy class which has same interface as VideoElement. This
21 * behaves like VideoElement, and is used for making Chromecast player
22 * controlled instead of the true Video Element tag.
24 * @param {MediaManager} media Media manager with the media to play.
25 * @param {chrome.cast.Session} session Session to play a video on.
26 * @constructor
27 * @struct
28 * @extends {cr.EventTarget}
30 function CastVideoElement(media, session) {
31 this.mediaManager_ = media;
32 this.mediaInfo_ = null;
34 this.castMedia_ = null;
35 this.castSession_ = session;
36 this.currentTime_ = null;
37 this.src_ = '';
38 this.volume_ = 100;
39 this.loop_ = false;
40 this.currentMediaPlayerState_ = null;
41 this.currentMediaCurrentTime_ = null;
42 this.currentMediaDuration_ = null;
43 this.playInProgress_ = false;
44 this.pauseInProgress_ = false;
45 this.errorCode_ = 0;
47 /**
48 * @type {number}
49 * @private
51 this.updateTimerId_ = 0;
53 /**
54 * @type {?string}
55 * @private
57 this.token_ = null;
59 this.onMessageBound_ = this.onMessage_.bind(this);
60 this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
61 this.castSession_.addMessageListener(
62 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
65 CastVideoElement.prototype = /** @struct */ {
66 __proto__: cr.EventTarget.prototype,
68 /**
69 * Prepares for unloading this objects.
71 dispose: function() {
72 this.unloadMedia_();
73 this.castSession_.removeMessageListener(
74 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
77 /**
78 * Returns a parent node. This must always be null.
79 * @type {Element}
81 get parentNode() {
82 return null;
85 /**
86 * The total time of the video (in sec).
87 * @type {?number}
89 get duration() {
90 return this.currentMediaDuration_;
93 /**
94 * The current timestamp of the video (in sec).
95 * @type {?number}
97 get currentTime() {
98 if (this.castMedia_) {
99 if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
100 return this.currentMediaDuration_; // Returns the duration.
101 else
102 return this.castMedia_.getEstimatedTime();
103 } else {
104 return null;
107 set currentTime(currentTime) {
108 var seekRequest = new chrome.cast.media.SeekRequest();
109 seekRequest.currentTime = currentTime;
110 this.castMedia_.seek(seekRequest,
111 function() {},
112 this.onCastCommandError_.wrap(this));
116 * If this video is pauses or not.
117 * @type {boolean}
119 get paused() {
120 if (!this.castMedia_)
121 return false;
123 return !this.playInProgress_ &&
124 (this.pauseInProgress_ ||
125 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
129 * If this video is ended or not.
130 * @type {boolean}
132 get ended() {
133 if (!this.castMedia_)
134 return true;
136 return !this.playInProgress_ &&
137 this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
141 * TimeRange object that represents the seekable ranges of the media
142 * resource.
143 * @type {TimeRanges}
145 get seekable() {
146 return {
147 length: 1,
148 start: function(index) { return 0; },
149 end: function(index) { return this.currentMediaDuration_; },
154 * Value of the volume
155 * @type {number}
157 get volume() {
158 return this.castSession_.receiver.volume.muted ?
160 this.castSession_.receiver.volume.level;
162 set volume(volume) {
163 var VOLUME_EPS = 0.01; // Threshold for ignoring a small change.
166 if (this.castSession_.receiver.volume.muted) {
167 if (volume < VOLUME_EPS)
168 return;
170 // Unmute before setting volume.
171 this.castSession_.setReceiverMuted(false,
172 function() {},
173 this.onCastCommandError_.wrap(this));
175 this.castSession_.setReceiverVolumeLevel(volume,
176 function() {},
177 this.onCastCommandError_.wrap(this));
178 } else {
179 // Ignores < 1% change.
180 var diff = this.castSession_.receiver.volume.level - volume;
181 if (Math.abs(diff) < VOLUME_EPS)
182 return;
184 if (volume < VOLUME_EPS) {
185 this.castSession_.setReceiverMuted(true,
186 function() {},
187 this.onCastCommandError_.wrap(this));
188 return;
191 this.castSession_.setReceiverVolumeLevel(volume,
192 function() {},
193 this.onCastCommandError_.wrap(this));
198 * Returns the source of the current video.
199 * @type {?string}
201 get src() {
202 return null;
204 set src(value) {
205 // Do nothing.
209 * Returns the flag if the video loops at end or not.
210 * @type {boolean}
212 get loop() {
213 return this.loop_;
215 set loop(value) {
216 this.loop_ = !!value;
220 * Returns the error object if available.
221 * @type {?Object}
223 get error() {
224 if (this.errorCode_ === 0)
225 return null;
227 return {code: this.errorCode_};
231 * Plays the video.
232 * @param {boolean=} opt_seeking True when seeking. False otherwise.
234 play: function(opt_seeking) {
235 if (this.playInProgress_)
236 return;
238 var play = function() {
239 // If the casted media is already playing and a pause request is not in
240 // progress, we can skip this play request.
241 if (this.castMedia_.playerState ===
242 chrome.cast.media.PlayerState.PLAYING &&
243 !this.pauseInProgress_) {
244 this.playInProgress_ = false;
245 return;
248 var playRequest = new chrome.cast.media.PlayRequest();
249 playRequest.customData = {seeking: !!opt_seeking};
251 this.castMedia_.play(
252 playRequest,
253 function() {
254 this.playInProgress_ = false;
255 }.wrap(this),
256 function(error) {
257 this.playInProgress_ = false;
258 this.onCastCommandError_(error);
259 }.wrap(this));
260 }.wrap(this);
262 this.playInProgress_ = true;
264 if (!this.castMedia_)
265 this.load(play);
266 else
267 play();
271 * Pauses the video.
272 * @param {boolean=} opt_seeking True when seeking. False otherwise.
274 pause: function(opt_seeking) {
275 if (!this.castMedia_)
276 return;
278 if (this.pauseInProgress_ ||
279 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED) {
280 return;
283 var pauseRequest = new chrome.cast.media.PauseRequest();
284 pauseRequest.customData = {seeking: !!opt_seeking};
286 this.pauseInProgress_ = true;
287 this.castMedia_.pause(
288 pauseRequest,
289 function() {
290 this.pauseInProgress_ = false;
291 }.wrap(this),
292 function(error) {
293 this.pauseInProgress_ = false;
294 this.onCastCommandError_(error);
295 }.wrap(this));
299 * Loads the video.
301 load: function(opt_callback) {
302 var sendTokenPromise = this.mediaManager_.getToken(false).then(
303 function(token) {
304 this.token_ = token;
305 this.sendMessage_({message: 'push-token', token: token});
306 }.bind(this));
308 // Resets the error code.
309 this.errorCode_ = 0;
311 Promise.all([
312 sendTokenPromise,
313 this.mediaManager_.getUrl(),
314 this.mediaManager_.getMime(),
315 this.mediaManager_.getThumbnail()]).
316 then(function(results) {
317 var url = results[1];
318 var mime = results[2]; // maybe empty
319 var thumbnailUrl = results[3]; // maybe empty
321 this.mediaInfo_ = new chrome.cast.media.MediaInfo(url, mime);
322 this.mediaInfo_.customData = {
323 tokenRequired: true,
324 thumbnailUrl: thumbnailUrl,
327 var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
328 return new Promise(
329 this.castSession_.loadMedia.bind(this.castSession_, request)).
330 then(function(media) {
331 this.onMediaDiscovered_(media);
332 if (opt_callback)
333 opt_callback();
334 }.bind(this));
335 }.bind(this)).catch(function(error) {
336 this.unloadMedia_();
337 this.dispatchEvent(new Event('error'));
338 console.error('Cast failed.', error.stack || error);
339 }.bind(this));
343 * Unloads the video.
344 * @private
346 unloadMedia_: function() {
347 if (this.castMedia_) {
348 this.castMedia_.stop(null,
349 function() {},
350 function(error) {
351 // Ignores session error, since session may already be closed.
352 if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
353 this.onCastCommandError_(error);
354 }.wrap(this));
356 this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
357 this.castMedia_ = null;
360 clearInterval(this.updateTimerId_);
364 * Sends the message to cast.
365 * @param {(!Object|string)} message Message to be sent (Must be JSON-able
366 * object).
367 * @private
369 sendMessage_: function(message) {
370 this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message,
371 function() {}, function(error) {});
375 * Invoked when receiving a message from the cast.
376 * @param {string} namespace Namespace of the message.
377 * @param {string} messageAsJson Content of message as json format.
378 * @private
380 onMessage_: function(namespace, messageAsJson) {
381 if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
382 return;
384 var message = JSON.parse(messageAsJson);
385 if (message['message'] === 'request-token') {
386 if (message['previousToken'] === this.token_) {
387 this.mediaManager_.getToken(true).then(function(token) {
388 this.token_ = token;
389 this.sendMessage_({message: 'push-token', token: token});
390 // TODO(yoshiki): Revokes the previous token.
391 }.bind(this)).catch(function(error) {
392 // Send an empty token as an error.
393 this.sendMessage_({message: 'push-token', token: ''});
394 // TODO(yoshiki): Revokes the previous token.
395 console.error(error.stack || error);
397 } else {
398 console.error(
399 'New token is requested, but the previous token mismatches.');
401 } else if (message['message'] === 'playback-error') {
402 if (message['detail'] === 'src-not-supported')
403 this.errorCode_ = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
408 * This method is called periodically to update media information while the
409 * media is loaded.
410 * @private
412 onPeriodicalUpdateTimer_: function() {
413 if (!this.castMedia_)
414 return;
416 if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
417 this.onCastMediaUpdated_(true);
421 * This method should be called when a media file is loaded.
422 * @param {chrome.cast.media.Media} media Media object which was discovered.
423 * @private
425 onMediaDiscovered_: function(media) {
426 if (this.castMedia_ !== null) {
427 this.unloadMedia_();
428 console.info('New media is found and the old media is overridden.');
431 this.castMedia_ = media;
432 this.onCastMediaUpdated_(true);
433 // Notify that the metadata of the video is ready.
434 this.dispatchEvent(new Event('loadedmetadata'));
436 media.addUpdateListener(this.onCastMediaUpdatedBound_);
437 this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
438 MEDIA_UPDATE_INTERVAL);
442 * This method should be called when a media command to cast is failed.
443 * @param {Object} error Object representing the error.
444 * @private
446 onCastCommandError_: function(error) {
447 this.unloadMedia_();
448 this.dispatchEvent(new Event('error'));
449 console.error('Error on sending command to cast.', error.stack || error);
453 * This is called when any media data is updated and by the periodical timer
454 * is fired.
456 * @param {boolean} alive Media availability. False if it's unavailable.
457 * @private
459 onCastMediaUpdated_: function(alive) {
460 if (!this.castMedia_)
461 return;
463 var media = this.castMedia_;
464 if (this.loop_ &&
465 media.idleReason === chrome.cast.media.IdleReason.FINISHED &&
466 !alive) {
467 // Resets the previous media silently.
468 this.castMedia_ = null;
470 // Replay the current media.
471 this.currentMediaPlayerState_ = chrome.cast.media.PlayerState.BUFFERING;
472 this.currentMediaCurrentTime_ = 0;
473 this.dispatchEvent(new Event('play'));
474 this.dispatchEvent(new Event('timeupdate'));
475 this.play();
476 return;
479 if (this.currentMediaPlayerState_ !== media.playerState) {
480 var oldPlayState = false;
481 var oldState = this.currentMediaPlayerState_;
482 if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
483 oldState === chrome.cast.media.PlayerState.PLAYING) {
484 oldPlayState = true;
486 var newPlayState = false;
487 var newState = media.playerState;
488 if (newState === chrome.cast.media.PlayerState.BUFFERING ||
489 newState === chrome.cast.media.PlayerState.PLAYING) {
490 newPlayState = true;
492 if (!oldPlayState && newPlayState)
493 this.dispatchEvent(new Event('play'));
494 if (oldPlayState && !newPlayState)
495 this.dispatchEvent(new Event('pause'));
497 this.currentMediaPlayerState_ = newState;
499 if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
500 this.currentMediaCurrentTime_ = media.getEstimatedTime();
501 this.dispatchEvent(new Event('timeupdate'));
504 if (this.currentMediaDuration_ !== media.media.duration) {
505 // Since recordMediumCount which is called inside recordCastedVideoLangth
506 // can take a value ranges from 1 to 10,000, we don't allow to pass 0
507 // here. i.e. length 0 is not recorded.
508 if (this.currentMediaDuration_)
509 metrics.recordCastedVideoLength(this.currentMediaDuration_);
511 this.currentMediaDuration_ = media.media.duration;
512 this.dispatchEvent(new Event('durationchange'));
515 // Media is being unloaded.
516 if (!alive) {
517 this.unloadMedia_();
518 return;