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;
39 this.currentMediaPlayerState_ = null;
40 this.currentMediaCurrentTime_ = null;
41 this.currentMediaDuration_ = null;
42 this.playInProgress_ = false;
43 this.pauseInProgress_ = false;
45 this.onMessageBound_ = this.onMessage_.bind(this);
46 this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
47 this.castSession_.addMessageListener(
48 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
51 CastVideoElement.prototype = {
52 __proto__: cr.EventTarget.prototype,
55 * Prepares for unloading this objects.
59 this.castSession_.removeMessageListener(
60 CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
64 * Returns a parent node. This must always be null.
72 * The total time of the video (in sec).
76 return this.currentMediaDuration_;
80 * The current timestamp of the video (in sec).
84 if (this.castMedia_) {
85 if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
86 return this.currentMediaDuration_; // Returns the duration.
88 return this.castMedia_.getEstimatedTime();
93 set currentTime(currentTime) {
94 // TODO(yoshiki): Support seek.
98 * If this video is pauses or not.
102 if (!this.castMedia_)
105 return !this.playInProgress_ &&
106 (this.pauseInProgress_ ||
107 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
111 * If this video is ended or not.
115 if (!this.castMedia_)
118 return this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
122 * If this video is seekable or not.
126 // TODO(yoshiki): Support seek.
131 * Value of the volume
135 return this.castSession_.receiver.volume.muted ?
137 this.castSession_.receiver.volume.level;
140 var VOLUME_EPS = 0.01; // Threshold for ignoring a small change.
142 // Ignores < 1% change.
143 if (Math.abs(this.castSession_.receiver.volume.level - volume) < VOLUME_EPS)
146 if (this.castSession_.receiver.volume.muted) {
147 if (volume < VOLUME_EPS)
150 // Unmute before setting volume.
151 this.castSession_.setReceiverMuted(false,
153 this.onCastCommandError_.wrap(this));
155 this.castSession_.setReceiverVolumeLevel(volume,
157 this.onCastCommandError_.wrap(this));
159 if (volume < VOLUME_EPS) {
160 this.castSession_.setReceiverMuted(true,
162 this.onCastCommandError_.wrap(this));
166 this.castSession_.setReceiverVolumeLevel(volume,
168 this.onCastCommandError_.wrap(this));
173 * Returns the source of the current video.
187 var play = function() {
188 this.castMedia_.play(null,
190 this.playInProgress_ = false;
193 this.playInProgress_ = false;
194 this.onCastCommandError_(error);
198 this.playInProgress_ = true;
200 if (!this.castMedia_)
210 if (!this.castMedia_)
213 this.pauseInProgress_ = true;
214 this.castMedia_.pause(null,
216 this.pauseInProgress_ = false;
219 this.pauseInProgress_ = false;
220 this.onCastCommandError_(error);
227 load: function(opt_callback) {
228 var sendTokenPromise = this.mediaManager_.getToken().then(function(token) {
230 this.sendMessage_({message: 'push-token', token: token});
235 this.mediaManager_.getUrl(),
236 this.mediaManager_.getMime(),
237 this.mediaManager_.getThumbnail()]).
238 then(function(results) {
239 var url = results[1];
240 var mime = results[2];
241 var thumbnailUrl = results[3];
243 this.mediaInfo_ = new chrome.cast.media.MediaInfo(url);
244 this.mediaInfo_.contentType = mime;
245 this.mediaInfo_.customData = {
247 thumbnailUrl: thumbnailUrl,
250 var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
252 this.castSession_.loadMedia.bind(this.castSession_, request)).
253 then(function(media) {
254 this.onMediaDiscovered_(media);
258 }.bind(this)).catch(function(error) {
260 this.dispatchEvent(new Event('error'));
261 console.error('Cast failed.', error.stack || error);
269 unloadMedia_: function() {
270 if (this.castMedia_) {
271 this.castMedia_.stop(null,
274 // Ignores session error, since session may already be closed.
275 if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
276 this.onCastCommandError_(error);
279 this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
280 this.castMedia_ = null;
282 clearInterval(this.updateTimerId_);
286 * Sends the message to cast.
287 * @param {Object} message Message to be sent (Must be JSON-able object).
290 sendMessage_: function(message) {
291 this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message);
295 * Invoked when receiving a message from the cast.
296 * @param {string} namespace Namespace of the message.
297 * @param {string} messageAsJson Content of message as json format.
300 onMessage_: function(namespace, messageAsJson) {
301 if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
304 var message = JSON.parse(messageAsJson);
305 if (message['message'] === 'request-token') {
306 if (message['previousToken'] === this.token_) {
307 this.mediaManager_.getToken().then(function(token) {
308 this.sendMessage_({message: 'push-token', token: token});
309 // TODO(yoshiki): Revokes the previous token.
310 }.bind(this)).catch(function(error) {
311 // Send an empty token as an error.
312 this.sendMessage_({message: 'push-token', token: ''});
313 // TODO(yoshiki): Revokes the previous token.
314 console.error(error.stack || error);
318 'New token is requested, but the previous token mismatches.');
324 * This method is called periodically to update media information while the
328 onPeriodicalUpdateTimer_: function() {
329 if (!this.castMedia_)
332 if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
333 this.onCastMediaUpdated_(true);
337 * This method should be called when a media file is loaded.
338 * @param {chrome.cast.Media} media Media object which was discovered.
341 onMediaDiscovered_: function(media) {
342 if (this.castMedia_ !== null) {
344 console.info('New media is found and the old media is overridden.');
347 this.castMedia_ = media;
348 this.onCastMediaUpdated_(true);
349 // Notify that the metadata of the video is ready.
350 this.dispatchEvent(new Event('loadedmetadata'));
352 media.addUpdateListener(this.onCastMediaUpdatedBound_);
353 this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
354 MEDIA_UPDATE_INTERVAL);
358 * This method should be called when a media command to cast is failed.
359 * @param {Object} error Object representing the error.
362 onCastCommandError_: function(error) {
364 this.dispatchEvent(new Event('error'));
365 console.error('Error on sending command to cast.', error.stack || error);
369 * This is called when any media data is updated and by the periodical timer
372 * @param {boolean} alive Media availability. False if it's unavailable.
375 onCastMediaUpdated_: function(alive) {
376 if (!this.castMedia_)
379 var media = this.castMedia_;
380 if (this.currentMediaPlayerState_ !== media.playerState) {
381 var oldPlayState = false;
382 var oldState = this.currentMediaPlayerState_;
383 if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
384 oldState === chrome.cast.media.PlayerState.PLAYING) {
387 var newPlayState = false;
388 var newState = media.playerState;
389 if (newState === chrome.cast.media.PlayerState.BUFFERING ||
390 newState === chrome.cast.media.PlayerState.PLAYING) {
393 if (!oldPlayState && newPlayState)
394 this.dispatchEvent(new Event('play'));
395 if (oldPlayState && !newPlayState)
396 this.dispatchEvent(new Event('pause'));
398 this.currentMediaPlayerState_ = newState;
400 if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
401 this.currentMediaCurrentTime_ = media.getEstimatedTime();
402 this.dispatchEvent(new Event('timeupdate'));
405 if (this.currentMediaDuration_ !== media.media.duration) {
406 this.currentMediaDuration_ = media.media.duration;
407 this.dispatchEvent(new Event('durationchange'));
410 // Media is being unloaded.