summaryrefslogtreecommitdiffstats
path: root/ui/file_manager
diff options
context:
space:
mode:
authoryoshiki@chromium.org <yoshiki@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-24 10:34:58 +0000
committeryoshiki@chromium.org <yoshiki@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-24 10:34:58 +0000
commitdd2b766cfa6d37ba980cfeb0f5f63961d24ecbc9 (patch)
treeabaf6bf4ed713795b52cee3d0a76ec81671c27a3 /ui/file_manager
parentfd968ebfc9c66d8c5273312b3fc0dc657f27a17d (diff)
downloadchromium_src-dd2b766cfa6d37ba980cfeb0f5f63961d24ecbc9.zip
chromium_src-dd2b766cfa6d37ba980cfeb0f5f63961d24ecbc9.tar.gz
chromium_src-dd2b766cfa6d37ba980cfeb0f5f63961d24ecbc9.tar.bz2
Video Player: Support casting a video
This patch adds the feature to controls a video on Chromecast using CastVideoElement and Cast Extension. BUG=305511 TEST=List of casts is shown R=hirono@chromium.org Review URL: https://codereview.chromium.org/412813002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@285170 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/file_manager')
-rw-r--r--ui/file_manager/video_player/js/cast/cast_video_element.js224
-rw-r--r--ui/file_manager/video_player/js/video_player.js75
2 files changed, 261 insertions, 38 deletions
diff --git a/ui/file_manager/video_player/js/cast/cast_video_element.js b/ui/file_manager/video_player/js/cast/cast_video_element.js
index 299fa2b..5e4bb3b 100644
--- a/ui/file_manager/video_player/js/cast/cast_video_element.js
+++ b/ui/file_manager/video_player/js/cast/cast_video_element.js
@@ -5,17 +5,34 @@
'use strict';
/**
+ * Inverval for updating media info (in ms).
+ * @type {number}
+ * @const
+ */
+var MEDIA_UPDATE_INTERVAL = 250;
+
+/**
* 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 {chrome.cast.media.MediaInfo} mediaInfo Data of the media to play.
+ * @param {chrome.cast.Session} session Session to play a video on.
* @constructor
*/
-function CastVideoElement() {
- this.duration_ = null;
+function CastVideoElement(mediaInfo, session) {
+ this.mediaInfo_ = mediaInfo;
+ this.castMedia_ = null;
+ this.castSession_ = session;
this.currentTime_ = null;
this.src_ = '';
this.volume_ = 100;
+ this.currentMediaPlayerState_ = null;
+ this.currentMediaCurrentTime_ = null;
+ this.currentMediaDuration_ = null;
+ this.pausing_ = false;
+
+ this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
}
CastVideoElement.prototype = {
@@ -23,29 +40,29 @@ CastVideoElement.prototype = {
/**
* Returns a parent node. This must always be null.
- * @return {Element}
+ * @type {Element}
*/
get parentNode() {
return null;
},
/**
- * The total time of the video.
- * @type {number}
+ * The total time of the video (in sec).
+ * @type {?number}
*/
get duration() {
- return this.duration_;
+ return this.currentMediaDuration_;
},
/**
- * The current timestamp of the video.
- * @type {number}
+ * The current timestamp of the video (in sec).
+ * @type {?number}
*/
get currentTime() {
- return this.currentTime_;
+ return this.castMedia_ ? this.castMedia_.getEstimatedTime() : null;
},
set currentTime(currentTime) {
- this.currentTime_ = currentTime;
+ // TODO(yoshiki): Support seek.
},
/**
@@ -53,7 +70,11 @@ CastVideoElement.prototype = {
* @type {boolean}
*/
get paused() {
- return false;
+ if (!this.castMedia_)
+ return false;
+
+ return this.pausing_ ||
+ this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED;
},
/**
@@ -61,7 +82,10 @@ CastVideoElement.prototype = {
* @type {boolean}
*/
get ended() {
- return false;
+ if (!this.castMedia_)
+ return true;
+
+ return this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
},
/**
@@ -69,6 +93,7 @@ CastVideoElement.prototype = {
* @type {boolean}
*/
get seekable() {
+ // TODO(yoshiki): Support seek.
return false;
},
@@ -77,38 +102,197 @@ CastVideoElement.prototype = {
* @type {number}
*/
get volume() {
- return this.volume_;
+ return this.castSession_.receiver.volume.muted ?
+ 0 :
+ this.castSession_.receiver.volume.level;
},
set volume(volume) {
- this.volume_ = volume;
+ var VOLUME_EPS = 0.01; // Threshold for ignoring a small change.
+
+ // Ignores < 1% change.
+ if (Math.abs(this.castSession_.receiver.volume.level - volume) < VOLUME_EPS)
+ return;
+
+ 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 {
+ 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.
- * @return {string}
+ * @type {?string}
*/
get src() {
- return this.src_;
+ return null;
},
/**
* Plays the video.
*/
play: function() {
- // TODO(yoshiki): Implement this.
+ if (!this.castMedia_) {
+ this.load(function() {
+ this.castMedia_.play(null,
+ function () {},
+ this.onCastCommandError_.wrap(this));
+ }.wrap(this));
+ return;
+ }
+
+ this.castMedia_.play(null,
+ function () {},
+ this.onCastCommandError_.wrap(this));
},
/**
* Pauses the video.
*/
pause: function() {
- // TODO(yoshiki): Implement this.
+ if (!this.castMedia_)
+ return;
+
+ this.pausing_ = true;
+ this.castMedia_.pause(null,
+ function () {
+ this.pausing_ = false;
+ }.wrap(this),
+ function () {
+ this.pausing_ = false;
+ this.onCastCommandError_();
+ }.wrap(this));
},
/**
* Loads the video.
*/
- load: function() {
- // TODO(yoshiki): Implement this.
+ load: function(opt_callback) {
+ var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
+ this.castSession_.loadMedia(request,
+ function(media) {
+ this.onMediaDiscovered_(media);
+ if (opt_callback)
+ opt_callback();
+ }.bind(this),
+ this.onCastCommandError_.wrap(this));
+ },
+
+ /**
+ * Unloads the video.
+ * @private
+ */
+ unloadMedia_: function() {
+ this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
+ this.castMedia_ = null;
+ clearInterval(this.updateTimerId_);
+ },
+
+ /**
+ * 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);
+ 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.
+ * @private
+ */
+ onCastCommandError_: function() {
+ this.unloadMedia_();
+ this.dispatchEvent(new Event('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.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.dispatchEvent(new Event('timeupdate'));
+ this.currentMediaCurrentTime_ = media.getEstimatedTime();
+ }
+
+ if (this.currentMediaDuration_ !== media.media.duration) {
+ this.dispatchEvent(new Event('durationchange'));
+ this.currentMediaDuration_ = media.media.duration;
+ }
+
+ // Media is being unloaded.
+ if (!alive) {
+ this.unloadMedia_();
+ return;
+ }
},
};
diff --git a/ui/file_manager/video_player/js/video_player.js b/ui/file_manager/video_player/js/video_player.js
index 08f368a..9ae1c67 100644
--- a/ui/file_manager/video_player/js/video_player.js
+++ b/ui/file_manager/video_player/js/video_player.js
@@ -75,9 +75,8 @@ FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
* Displays error message.
*
* @param {string} message Message id.
- * @private
*/
-FullWindowVideoControls.prototype.showErrorMessage_ = function(message) {
+FullWindowVideoControls.prototype.showErrorMessage = function(message) {
var errorBanner = document.querySelector('#error');
errorBanner.textContent =
loadTimeData.getString(message);
@@ -92,7 +91,7 @@ FullWindowVideoControls.prototype.showErrorMessage_ = function(message) {
* @private
*/
FullWindowVideoControls.prototype.onPlaybackError_ = function() {
- this.showErrorMessage_('GALLERY_VIDEO_DECODING_ERROR');
+ this.showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
this.decodeErrorOccured = true;
// Disable inactivity watcher, and disable the ui, by hiding tools manually.
@@ -273,37 +272,77 @@ VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
this.controls.inactivityWatcher.disabled = false;
this.controls.decodeErrorOccured = false;
+ var videoElementInitializePromise;
if (this.currentCast_) {
videoPlayerElement.setAttribute('casting', true);
- this.videoElement_ = new CastVideoElement();
- this.controls.attachMedia(this.videoElement_);
document.querySelector('#cast-name-label').textContent =
loadTimeData.getString('VIDEO_PLAYER_PLAYING_ON');
document.querySelector('#cast-name').textContent =
this.currentCast_.friendlyName;
+
+ var downloadUrlPromise = new Promise(function(fulfill, reject) {
+ chrome.fileBrowserPrivate.getDownloadUrl(video.url, fulfill);
+ });
+
+ var mimePromise = new Promise(function(fulfill, reject) {
+ chrome.fileBrowserPrivate.getDriveEntryProperties(
+ [video.entry.toURL()], fulfill);
+ });
+
+ videoElementInitializePromise =
+ Promise.all([downloadUrlPromise, mimePromise]).then(function(results) {
+ var downloadUrl = results[0];
+ var props = results[1];
+ var mime = '';
+ if (!props || props.length === 0 || !props[0].contentMimeType) {
+ // TODO(yoshiki): Adds a logic to guess the mime.
+ } else {
+ mime = props[0].contentMimeType;
+ }
+
+ return new Promise(function(fulfill, reject) {
+ chrome.cast.requestSession(
+ fulfill, reject, undefined, this.currentCast_.label);
+ }).then(function(session) {
+ var mediaInfo = new chrome.cast.media.MediaInfo(downloadUrl);
+ mediaInfo.contentType = mime;
+ this.videoElement_ = new CastVideoElement(mediaInfo, session);
+ this.controls.attachMedia(this.videoElement_);
+ }.bind(this));
+ }.bind(this));
} else {
videoPlayerElement.removeAttribute('casting');
this.videoElement_ = document.createElement('video');
- document.querySelector('#video-container').appendChild(this.videoElement_);
+ document.querySelector('#video-container').appendChild(
+ this.videoElement_);
this.controls.attachMedia(this.videoElement_);
this.videoElement_.src = video.url;
- }
- this.videoElement_.load();
-
- if (opt_callback) {
- var handler = function(currentPos, event) {
- console.log('loaded: ', currentPos, this.currentPos_);
- if (currentPos === this.currentPos_)
- opt_callback();
- this.videoElement_.removeEventListener('loadedmetadata', handler);
- }.wrap(this, this.currentPos_);
-
- this.videoElement_.addEventListener('loadedmetadata', handler);
+ videoElementInitializePromise = Promise.resolve();
}
+
+ videoElementInitializePromise.then(
+ function() {
+ this.videoElement_.load();
+
+ if (opt_callback) {
+ var handler = function(currentPos, event) {
+ if (currentPos === this.currentPos_)
+ opt_callback();
+ this.videoElement_.removeEventListener('loadedmetadata', handler);
+ }.wrap(this, this.currentPos_);
+
+ this.videoElement_.addEventListener('loadedmetadata', handler);
+ }
+ }.bind(this),
+ function videoElementInitializePromiseRejected() {
+ console.error('Failed to initialize the video element.',
+ error.stack || error);
+ this.controls_.showErrorMessage('GALLERY_VIDEO_ERROR');
+ }.bind(this));
};
/**