blob: ccbc342f5dbb6f133fe92d3e5dff1b3d881ffd3e [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Interval for updating media info (in ms).
* @type {number}
* @const
*/
var MEDIA_UPDATE_INTERVAL = 250;
/**
* The namespace for communication between the cast and the player.
* @type {string}
* @const
*/
var CAST_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.chromeos.videoplayer';
/**
* This class is the dummy class which has same interface as VideoElement. This
* behaves like VideoElement, and is used for making Chromecast player
* controlled instead of the true Video Element tag.
*
* @param {MediaManager} media Media manager with the media to play.
* @param {chrome.cast.Session} session Session to play a video on.
* @constructor
*/
function CastVideoElement(media, session) {
this.mediaManager_ = media;
this.mediaInfo_ = null;
this.castMedia_ = null;
this.castSession_ = session;
this.currentTime_ = null;
this.src_ = '';
this.volume_ = 100;
this.loop_ = false;
this.currentMediaPlayerState_ = null;
this.currentMediaCurrentTime_ = null;
this.currentMediaDuration_ = null;
this.playInProgress_ = false;
this.pauseInProgress_ = false;
this.errorCode_ = 0;
this.onMessageBound_ = this.onMessage_.bind(this);
this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
this.castSession_.addMessageListener(
CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
}
CastVideoElement.prototype = {
__proto__: cr.EventTarget.prototype,
/**
* Prepares for unloading this objects.
*/
dispose: function() {
this.unloadMedia_();
this.castSession_.removeMessageListener(
CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
},
/**
* Returns a parent node. This must always be null.
* @type {Element}
*/
get parentNode() {
return null;
},
/**
* The total time of the video (in sec).
* @type {?number}
*/
get duration() {
return this.currentMediaDuration_;
},
/**
* The current timestamp of the video (in sec).
* @type {?number}
*/
get currentTime() {
if (this.castMedia_) {
if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
return this.currentMediaDuration_; // Returns the duration.
else
return this.castMedia_.getEstimatedTime();
} else {
return null;
}
},
set currentTime(currentTime) {
var seekRequest = new chrome.cast.media.SeekRequest();
seekRequest.currentTime = currentTime;
this.castMedia_.seek(seekRequest,
function() {},
this.onCastCommandError_.wrap(this));
},
/**
* If this video is pauses or not.
* @type {boolean}
*/
get paused() {
if (!this.castMedia_)
return false;
return !this.playInProgress_ &&
(this.pauseInProgress_ ||
this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
},
/**
* If this video is ended or not.
* @type {boolean}
*/
get ended() {
if (!this.castMedia_)
return true;
return !this.playInProgress &&
this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
},
/**
* TimeRange object that represents the seekable ranges of the media
* resource.
* @type {TimeRanges}
*/
get seekable() {
return {
length: 1,
start: function(index) { return 0; },
end: function(index) { return this.currentMediaDuration_; },
};
},
/**
* Value of the volume
* @type {number}
*/
get volume() {
return this.castSession_.receiver.volume.muted ?
0 :
this.castSession_.receiver.volume.level;
},
set volume(volume) {
var VOLUME_EPS = 0.01; // Threshold for ignoring a small change.
if (this.castSession_.receiver.volume.muted) {
if (volume < VOLUME_EPS)
return;
// Unmute before setting volume.
this.castSession_.setReceiverMuted(false,
function() {},
this.onCastCommandError_.wrap(this));
this.castSession_.setReceiverVolumeLevel(volume,
function() {},
this.onCastCommandError_.wrap(this));
} else {
// Ignores < 1% change.
var diff = this.castSession_.receiver.volume.level - volume;
if (Math.abs(diff) < VOLUME_EPS)
return;
if (volume < VOLUME_EPS) {
this.castSession_.setReceiverMuted(true,
function() {},
this.onCastCommandError_.wrap(this));
return;
}
this.castSession_.setReceiverVolumeLevel(volume,
function() {},
this.onCastCommandError_.wrap(this));
}
},
/**
* Returns the source of the current video.
* @type {?string}
*/
get src() {
return null;
},
set src(value) {
// Do nothing.
},
/**
* Returns the flag if the video loops at end or not.
* @type {boolean}
*/
get loop() {
return this.loop_;
},
set loop(value) {
this.loop_ = !!value;
},
/**
* Returns the error object if available.
* @type {?Object}
*/
get error() {
if (this.errorCode_ === 0)
return null;
return {code: this.errorCode_};
},
/**
* Plays the video.
*/
play: function() {
var play = function() {
this.castMedia_.play(null,
function() {
this.playInProgress_ = false;
}.wrap(this),
function(error) {
this.playInProgress_ = false;
this.onCastCommandError_(error);
}.wrap(this));
}.wrap(this);
this.playInProgress_ = true;
if (!this.castMedia_)
this.load(play);
else
play();
},
/**
* Pauses the video.
*/
pause: function() {
if (!this.castMedia_)
return;
this.pauseInProgress_ = true;
this.castMedia_.pause(null,
function() {
this.pauseInProgress_ = false;
}.wrap(this),
function(error) {
this.pauseInProgress_ = false;
this.onCastCommandError_(error);
}.wrap(this));
},
/**
* Loads the video.
*/
load: function(opt_callback) {
var sendTokenPromise = this.mediaManager_.getToken().then(function(token) {
this.token_ = token;
this.sendMessage_({message: 'push-token', token: token});
}.bind(this));
// Resets the error code.
this.errorCode_ = 0;
Promise.all([
sendTokenPromise,
this.mediaManager_.getUrl(),
this.mediaManager_.getMime(),
this.mediaManager_.getThumbnail()]).
then(function(results) {
var url = results[1];
var mime = results[2]; // maybe empty
var thumbnailUrl = results[3]; // maybe empty
this.mediaInfo_ = new chrome.cast.media.MediaInfo(url);
this.mediaInfo_.contentType = mime;
this.mediaInfo_.customData = {
tokenRequired: true,
thumbnailUrl: thumbnailUrl,
};
var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
return new Promise(
this.castSession_.loadMedia.bind(this.castSession_, request)).
then(function(media) {
this.onMediaDiscovered_(media);
if (opt_callback)
opt_callback();
}.bind(this));
}.bind(this)).catch(function(error) {
this.unloadMedia_();
this.dispatchEvent(new Event('error'));
console.error('Cast failed.', error.stack || error);
}.bind(this));
},
/**
* Unloads the video.
* @private
*/
unloadMedia_: function() {
if (this.castMedia_) {
this.castMedia_.stop(null,
function() {},
function(error) {
// Ignores session error, since session may already be closed.
if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
this.onCastCommandError_(error);
}.wrap(this));
this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
this.castMedia_ = null;
}
clearInterval(this.updateTimerId_);
},
/**
* Sends the message to cast.
* @param {Object} message Message to be sent (Must be JSON-able object).
* @private
*/
sendMessage_: function(message) {
this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message);
},
/**
* Invoked when receiving a message from the cast.
* @param {string} namespace Namespace of the message.
* @param {string} messageAsJson Content of message as json format.
* @private
*/
onMessage_: function(namespace, messageAsJson) {
if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
return;
var message = JSON.parse(messageAsJson);
if (message['message'] === 'request-token') {
if (message['previousToken'] === this.token_) {
this.mediaManager_.getToken(true).then(function(token) {
this.token_ = token;
this.sendMessage_({message: 'push-token', token: token});
// TODO(yoshiki): Revokes the previous token.
}.bind(this)).catch(function(error) {
// Send an empty token as an error.
this.sendMessage_({message: 'push-token', token: ''});
// TODO(yoshiki): Revokes the previous token.
console.error(error.stack || error);
});
} else {
console.error(
'New token is requested, but the previous token mismatches.');
}
} else if (message['message'] === 'playback-error') {
if (message['detail'] === 'src-not-supported')
this.errorCode_ = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
}
},
/**
* This method is called periodically to update media information while the
* media is loaded.
* @private
*/
onPeriodicalUpdateTimer_: function() {
if (!this.castMedia_)
return;
if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
this.onCastMediaUpdated_(true);
},
/**
* This method should be called when a media file is loaded.
* @param {chrome.cast.Media} media Media object which was discovered.
* @private
*/
onMediaDiscovered_: function(media) {
if (this.castMedia_ !== null) {
this.unloadMedia_();
console.info('New media is found and the old media is overridden.');
}
this.castMedia_ = media;
this.onCastMediaUpdated_(true);
// Notify that the metadata of the video is ready.
this.dispatchEvent(new Event('loadedmetadata'));
media.addUpdateListener(this.onCastMediaUpdatedBound_);
this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
MEDIA_UPDATE_INTERVAL);
},
/**
* This method should be called when a media command to cast is failed.
* @param {Object} error Object representing the error.
* @private
*/
onCastCommandError_: function(error) {
this.unloadMedia_();
this.dispatchEvent(new Event('error'));
console.error('Error on sending command to cast.', error.stack || error);
},
/**
* This is called when any media data is updated and by the periodical timer
* is fired.
*
* @param {boolean} alive Media availability. False if it's unavailable.
* @private
*/
onCastMediaUpdated_: function(alive) {
if (!this.castMedia_)
return;
var media = this.castMedia_;
if (this.loop_ &&
media.idleReason === chrome.cast.media.IdleReason.FINISHED &&
!alive) {
// Resets the previous media silently.
this.castMedia_ = null;
// Replay the current media.
this.currentMediaPlayerState_ = chrome.cast.media.PlayerState.BUFFERING;
this.currentMediaCurrentTime_ = 0;
this.dispatchEvent(new Event('play'));
this.dispatchEvent(new Event('timeupdate'));
this.play();
return;
}
if (this.currentMediaPlayerState_ !== media.playerState) {
var oldPlayState = false;
var oldState = this.currentMediaPlayerState_;
if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
oldState === chrome.cast.media.PlayerState.PLAYING) {
oldPlayState = true;
}
var newPlayState = false;
var newState = media.playerState;
if (newState === chrome.cast.media.PlayerState.BUFFERING ||
newState === chrome.cast.media.PlayerState.PLAYING) {
newPlayState = true;
}
if (!oldPlayState && newPlayState)
this.dispatchEvent(new Event('play'));
if (oldPlayState && !newPlayState)
this.dispatchEvent(new Event('pause'));
this.currentMediaPlayerState_ = newState;
}
if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
this.currentMediaCurrentTime_ = media.getEstimatedTime();
this.dispatchEvent(new Event('timeupdate'));
}
if (this.currentMediaDuration_ !== media.media.duration) {
this.currentMediaDuration_ = media.media.duration;
this.dispatchEvent(new Event('durationchange'));
}
// Media is being unloaded.
if (!alive) {
this.unloadMedia_();
return;
}
},
};