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.
8 * Interval for updating media info (in ms).
12 var MEDIA_UPDATE_INTERVAL = 250;
15 * The namespace for communication between the cast and the player.
19 var CAST_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.chromeos.videoplayer';
22 * This class is the dummy class which has same interface as VideoElement. This
23 * behaves like VideoElement, and is used for making Chromecast player
24 * controlled instead of the true Video Element tag.
26 * @param {MediaManager} media Media manager with the media to play.
27 * @param {chrome.cast.Session} session Session to play a video on.
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;
40 this.currentMediaPlayerState_ = null;
41 this.currentMediaCurrentTime_ = null;
42 this.currentMediaDuration_ = null;
43 this.playInProgress_ = false;
44 this.pauseInProgress_ = false;
47 this.onMessageBound_ = this.onMessage_.bind(this);
48 this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
49 this.castSession_.addMessageListener(
50 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
53 CastVideoElement.prototype = {
54 __proto__: cr.EventTarget.prototype,
57 * Prepares for unloading this objects.
61 this.castSession_.removeMessageListener(
62 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
66 * Returns a parent node. This must always be null.
74 * The total time of the video (in sec).
78 return this.currentMediaDuration_;
82 * The current timestamp of the video (in sec).
86 if (this.castMedia_) {
87 if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
88 return this.currentMediaDuration_; // Returns the duration.
90 return this.castMedia_.getEstimatedTime();
95 set currentTime(currentTime) {
96 var seekRequest = new chrome.cast.media.SeekRequest();
97 seekRequest.currentTime = currentTime;
98 this.castMedia_.seek(seekRequest,
100 this.onCastCommandError_.wrap(this));
104 * If this video is pauses or not.
108 if (!this.castMedia_)
111 return !this.playInProgress_ &&
112 (this.pauseInProgress_ ||
113 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
117 * If this video is ended or not.
121 if (!this.castMedia_)
124 return !this.playInProgress &&
125 this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
129 * TimeRange object that represents the seekable ranges of the media
136 start: function(index) { return 0; },
137 end: function(index) { return this.currentMediaDuration_; },
142 * Value of the volume
146 return this.castSession_.receiver.volume.muted ?
148 this.castSession_.receiver.volume.level;
151 var VOLUME_EPS = 0.01; // Threshold for ignoring a small change.
154 if (this.castSession_.receiver.volume.muted) {
155 if (volume < VOLUME_EPS)
158 // Unmute before setting volume.
159 this.castSession_.setReceiverMuted(false,
161 this.onCastCommandError_.wrap(this));
163 this.castSession_.setReceiverVolumeLevel(volume,
165 this.onCastCommandError_.wrap(this));
167 // Ignores < 1% change.
168 var diff = this.castSession_.receiver.volume.level - volume;
169 if (Math.abs(diff) < VOLUME_EPS)
172 if (volume < VOLUME_EPS) {
173 this.castSession_.setReceiverMuted(true,
175 this.onCastCommandError_.wrap(this));
179 this.castSession_.setReceiverVolumeLevel(volume,
181 this.onCastCommandError_.wrap(this));
186 * Returns the source of the current video.
197 * Returns the flag if the video loops at end or not.
204 this.loop_ = !!value;
208 * Returns the error object if available.
212 if (this.errorCode_ === 0)
215 return {code: this.errorCode_};
220 * @param {boolean=} opt_seeking True when seeking. False otherwise.
222 play: function(opt_seeking) {
223 if (this.playInProgress_)
226 var play = function() {
227 // If the casted media is already playing and a pause request is not in
228 // progress, we can skip this play request.
229 if (this.castMedia_.playerState ===
230 chrome.cast.media.PlayerState.PLAYING &&
231 !this.pauseInProgress_) {
232 this.playInProgress_ = false;
236 var playRequest = new chrome.cast.media.PlayRequest();
237 playRequest.customData = {seeking: !!opt_seeking};
239 this.castMedia_.play(
242 this.playInProgress_ = false;
245 this.playInProgress_ = false;
246 this.onCastCommandError_(error);
250 this.playInProgress_ = true;
252 if (!this.castMedia_)
260 * @param {boolean=} opt_seeking True when seeking. False otherwise.
262 pause: function(opt_seeking) {
263 if (!this.castMedia_)
266 if (this.pauseInProgress_ ||
267 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED) {
271 var pauseRequest = new chrome.cast.media.PauseRequest();
272 pauseRequest.customData = {seeking: !!opt_seeking};
274 this.pauseInProgress_ = true;
275 this.castMedia_.pause(
278 this.pauseInProgress_ = false;
281 this.pauseInProgress_ = false;
282 this.onCastCommandError_(error);
289 load: function(opt_callback) {
290 var sendTokenPromise = this.mediaManager_.getToken().then(function(token) {
292 this.sendMessage_({message: 'push-token', token: token});
295 // Resets the error code.
300 this.mediaManager_.getUrl(),
301 this.mediaManager_.getMime(),
302 this.mediaManager_.getThumbnail()]).
303 then(function(results) {
304 var url = results[1];
305 var mime = results[2]; // maybe empty
306 var thumbnailUrl = results[3]; // maybe empty
308 this.mediaInfo_ = new chrome.cast.media.MediaInfo(url);
309 this.mediaInfo_.contentType = mime;
310 this.mediaInfo_.customData = {
312 thumbnailUrl: thumbnailUrl,
315 var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
317 this.castSession_.loadMedia.bind(this.castSession_, request)).
318 then(function(media) {
319 this.onMediaDiscovered_(media);
323 }.bind(this)).catch(function(error) {
325 this.dispatchEvent(new Event('error'));
326 console.error('Cast failed.', error.stack || error);
334 unloadMedia_: function() {
335 if (this.castMedia_) {
336 this.castMedia_.stop(null,
339 // Ignores session error, since session may already be closed.
340 if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
341 this.onCastCommandError_(error);
344 this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
345 this.castMedia_ = null;
348 clearInterval(this.updateTimerId_);
352 * Sends the message to cast.
353 * @param {Object} message Message to be sent (Must be JSON-able object).
356 sendMessage_: function(message) {
357 this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message);
361 * Invoked when receiving a message from the cast.
362 * @param {string} namespace Namespace of the message.
363 * @param {string} messageAsJson Content of message as json format.
366 onMessage_: function(namespace, messageAsJson) {
367 if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
370 var message = JSON.parse(messageAsJson);
371 if (message['message'] === 'request-token') {
372 if (message['previousToken'] === this.token_) {
373 this.mediaManager_.getToken(true).then(function(token) {
375 this.sendMessage_({message: 'push-token', token: token});
376 // TODO(yoshiki): Revokes the previous token.
377 }.bind(this)).catch(function(error) {
378 // Send an empty token as an error.
379 this.sendMessage_({message: 'push-token', token: ''});
380 // TODO(yoshiki): Revokes the previous token.
381 console.error(error.stack || error);
385 'New token is requested, but the previous token mismatches.');
387 } else if (message['message'] === 'playback-error') {
388 if (message['detail'] === 'src-not-supported')
389 this.errorCode_ = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
394 * This method is called periodically to update media information while the
398 onPeriodicalUpdateTimer_: function() {
399 if (!this.castMedia_)
402 if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
403 this.onCastMediaUpdated_(true);
407 * This method should be called when a media file is loaded.
408 * @param {chrome.cast.Media} media Media object which was discovered.
411 onMediaDiscovered_: function(media) {
412 if (this.castMedia_ !== null) {
414 console.info('New media is found and the old media is overridden.');
417 this.castMedia_ = media;
418 this.onCastMediaUpdated_(true);
419 // Notify that the metadata of the video is ready.
420 this.dispatchEvent(new Event('loadedmetadata'));
422 media.addUpdateListener(this.onCastMediaUpdatedBound_);
423 this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
424 MEDIA_UPDATE_INTERVAL);
428 * This method should be called when a media command to cast is failed.
429 * @param {Object} error Object representing the error.
432 onCastCommandError_: function(error) {
434 this.dispatchEvent(new Event('error'));
435 console.error('Error on sending command to cast.', error.stack || error);
439 * This is called when any media data is updated and by the periodical timer
442 * @param {boolean} alive Media availability. False if it's unavailable.
445 onCastMediaUpdated_: function(alive) {
446 if (!this.castMedia_)
449 var media = this.castMedia_;
451 media.idleReason === chrome.cast.media.IdleReason.FINISHED &&
453 // Resets the previous media silently.
454 this.castMedia_ = null;
456 // Replay the current media.
457 this.currentMediaPlayerState_ = chrome.cast.media.PlayerState.BUFFERING;
458 this.currentMediaCurrentTime_ = 0;
459 this.dispatchEvent(new Event('play'));
460 this.dispatchEvent(new Event('timeupdate'));
465 if (this.currentMediaPlayerState_ !== media.playerState) {
466 var oldPlayState = false;
467 var oldState = this.currentMediaPlayerState_;
468 if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
469 oldState === chrome.cast.media.PlayerState.PLAYING) {
472 var newPlayState = false;
473 var newState = media.playerState;
474 if (newState === chrome.cast.media.PlayerState.BUFFERING ||
475 newState === chrome.cast.media.PlayerState.PLAYING) {
478 if (!oldPlayState && newPlayState)
479 this.dispatchEvent(new Event('play'));
480 if (oldPlayState && !newPlayState)
481 this.dispatchEvent(new Event('pause'));
483 this.currentMediaPlayerState_ = newState;
485 if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
486 this.currentMediaCurrentTime_ = media.getEstimatedTime();
487 this.dispatchEvent(new Event('timeupdate'));
490 if (this.currentMediaDuration_ !== media.media.duration) {
491 this.currentMediaDuration_ = media.media.duration;
492 this.dispatchEvent(new Event('durationchange'));
495 // Media is being unloaded.