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.
6 * Interval for updating media info (in ms).
10 var MEDIA_UPDATE_INTERVAL
= 250;
13 * The namespace for communication between the cast and the player.
17 var CAST_MESSAGE_NAMESPACE
= 'urn:x-cast:com.google.chromeos.videoplayer';
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.
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;
40 this.currentMediaPlayerState_
= null;
41 this.currentMediaCurrentTime_
= null;
42 this.currentMediaDuration_
= null;
43 this.playInProgress_
= false;
44 this.pauseInProgress_
= false;
51 this.updateTimerId_
= 0;
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,
69 * Prepares for unloading this objects.
73 this.castSession_
.removeMessageListener(
74 CAST_MESSAGE_NAMESPACE
, this.onMessageBound_
);
78 * Returns a parent node. This must always be null.
86 * The total time of the video (in sec).
90 return this.currentMediaDuration_
;
94 * The current timestamp of the video (in sec).
98 if (this.castMedia_
) {
99 if (this.castMedia_
.idleReason
=== chrome
.cast
.media
.IdleReason
.FINISHED
)
100 return this.currentMediaDuration_
; // Returns the duration.
102 return this.castMedia_
.getEstimatedTime();
107 set currentTime(currentTime
) {
108 var seekRequest
= new chrome
.cast
.media
.SeekRequest();
109 seekRequest
.currentTime
= currentTime
;
110 this.castMedia_
.seek(seekRequest
,
112 this.onCastCommandError_
.wrap(this));
116 * If this video is pauses or not.
120 if (!this.castMedia_
)
123 return !this.playInProgress_
&&
124 (this.pauseInProgress_
||
125 this.castMedia_
.playerState
=== chrome
.cast
.media
.PlayerState
.PAUSED
);
129 * If this video is ended or not.
133 if (!this.castMedia_
)
136 return !this.playInProgress_
&&
137 this.castMedia_
.idleReason
=== chrome
.cast
.media
.IdleReason
.FINISHED
;
141 * TimeRange object that represents the seekable ranges of the media
148 start: function(index
) { return 0; },
149 end: function(index
) { return this.currentMediaDuration_
; },
154 * Value of the volume
158 return this.castSession_
.receiver
.volume
.muted
?
160 this.castSession_
.receiver
.volume
.level
;
163 var VOLUME_EPS
= 0.01; // Threshold for ignoring a small change.
166 if (this.castSession_
.receiver
.volume
.muted
) {
167 if (volume
< VOLUME_EPS
)
170 // Unmute before setting volume.
171 this.castSession_
.setReceiverMuted(false,
173 this.onCastCommandError_
.wrap(this));
175 this.castSession_
.setReceiverVolumeLevel(volume
,
177 this.onCastCommandError_
.wrap(this));
179 // Ignores < 1% change.
180 var diff
= this.castSession_
.receiver
.volume
.level
- volume
;
181 if (Math
.abs(diff
) < VOLUME_EPS
)
184 if (volume
< VOLUME_EPS
) {
185 this.castSession_
.setReceiverMuted(true,
187 this.onCastCommandError_
.wrap(this));
191 this.castSession_
.setReceiverVolumeLevel(volume
,
193 this.onCastCommandError_
.wrap(this));
198 * Returns the source of the current video.
209 * Returns the flag if the video loops at end or not.
216 this.loop_
= !!value
;
220 * Returns the error object if available.
224 if (this.errorCode_
=== 0)
227 return {code
: this.errorCode_
};
232 * @param {boolean=} opt_seeking True when seeking. False otherwise.
234 play: function(opt_seeking
) {
235 if (this.playInProgress_
)
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;
248 var playRequest
= new chrome
.cast
.media
.PlayRequest();
249 playRequest
.customData
= {seeking
: !!opt_seeking
};
251 this.castMedia_
.play(
254 this.playInProgress_
= false;
257 this.playInProgress_
= false;
258 this.onCastCommandError_(error
);
262 this.playInProgress_
= true;
264 if (!this.castMedia_
)
272 * @param {boolean=} opt_seeking True when seeking. False otherwise.
274 pause: function(opt_seeking
) {
275 if (!this.castMedia_
)
278 if (this.pauseInProgress_
||
279 this.castMedia_
.playerState
=== chrome
.cast
.media
.PlayerState
.PAUSED
) {
283 var pauseRequest
= new chrome
.cast
.media
.PauseRequest();
284 pauseRequest
.customData
= {seeking
: !!opt_seeking
};
286 this.pauseInProgress_
= true;
287 this.castMedia_
.pause(
290 this.pauseInProgress_
= false;
293 this.pauseInProgress_
= false;
294 this.onCastCommandError_(error
);
301 load: function(opt_callback
) {
302 var sendTokenPromise
= this.mediaManager_
.getToken(false).then(
305 this.sendMessage_({message
: 'push-token', token
: token
});
308 // Resets the error code.
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
= {
324 thumbnailUrl
: thumbnailUrl
,
327 var request
= new chrome
.cast
.media
.LoadRequest(this.mediaInfo_
);
329 this.castSession_
.loadMedia
.bind(this.castSession_
, request
)).
330 then(function(media
) {
331 this.onMediaDiscovered_(media
);
335 }.bind(this)).catch(function(error
) {
337 this.dispatchEvent(new Event('error'));
338 console
.error('Cast failed.', error
.stack
|| error
);
346 unloadMedia_: function() {
347 if (this.castMedia_
) {
348 this.castMedia_
.stop(null,
351 // Ignores session error, since session may already be closed.
352 if (error
.code
!== chrome
.cast
.ErrorCode
.SESSION_ERROR
)
353 this.onCastCommandError_(error
);
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
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.
380 onMessage_: function(namespace, messageAsJson
) {
381 if (namespace !== CAST_MESSAGE_NAMESPACE
|| !messageAsJson
)
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
) {
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
);
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
412 onPeriodicalUpdateTimer_: function() {
413 if (!this.castMedia_
)
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.
425 onMediaDiscovered_: function(media
) {
426 if (this.castMedia_
!== null) {
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.
446 onCastCommandError_: function(error
) {
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
456 * @param {boolean} alive Media availability. False if it's unavailable.
459 onCastMediaUpdated_: function(alive
) {
460 if (!this.castMedia_
)
463 var media
= this.castMedia_
;
465 media
.idleReason
=== chrome
.cast
.media
.IdleReason
.FINISHED
&&
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'));
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
) {
486 var newPlayState
= false;
487 var newState
= media
.playerState
;
488 if (newState
=== chrome
.cast
.media
.PlayerState
.BUFFERING
||
489 newState
=== chrome
.cast
.media
.PlayerState
.PLAYING
) {
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.