diff options
author | yoshiki@chromium.org <yoshiki@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-24 10:34:58 +0000 |
---|---|---|
committer | yoshiki@chromium.org <yoshiki@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-24 10:34:58 +0000 |
commit | dd2b766cfa6d37ba980cfeb0f5f63961d24ecbc9 (patch) | |
tree | abaf6bf4ed713795b52cee3d0a76ec81671c27a3 /ui/file_manager | |
parent | fd968ebfc9c66d8c5273312b3fc0dc657f27a17d (diff) | |
download | chromium_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.js | 224 | ||||
-rw-r--r-- | ui/file_manager/video_player/js/video_player.js | 75 |
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)); }; /** |