[CrOS Video Player] Change to ES6 class and arrow annotation. Add missing dependencies and type check.
Bug: 909963
Change-Id: Ie024a155e423ea05e81e389c4de7c7ab57564a8e
Reviewed-on: https://chromium-review.googlesource.com/c/1474800
Reviewed-by: Trent Apted <tapted@chromium.org>
Commit-Queue: Jazz Xu <jazzhsu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#632881}
diff --git a/ui/file_manager/video_player/js/BUILD.gn b/ui/file_manager/video_player/js/BUILD.gn
index 1090e8a..496ba8c 100644
--- a/ui/file_manager/video_player/js/BUILD.gn
+++ b/ui/file_manager/video_player/js/BUILD.gn
@@ -4,7 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
-js_type_check("closure_compile") {
+js_type_check("closure_compile_module") {
deps = [
":background",
":closure_compile_externs",
@@ -13,6 +13,7 @@
":mouse_inactivity_watcher",
":video_player",
":video_player_metrics",
+ ":video_player_native_controls",
]
}
@@ -51,6 +52,11 @@
}
js_library("video_player_native_controls") {
+ deps = [
+ "//ui/file_manager/base/js:app_util",
+ "//ui/webui/resources/js:load_time_data",
+ "//ui/webui/resources/js:util",
+ ]
}
js_library("video_player") {
@@ -78,3 +84,10 @@
"../../file_manager/common/js:metrics_base",
]
}
+
+group("closure_compile") {
+ testonly = true
+ deps = [
+ ":closure_compile_module",
+ ]
+}
diff --git a/ui/file_manager/video_player/js/video_player_native_controls.js b/ui/file_manager/video_player/js/video_player_native_controls.js
index a96e1fe..8efcba4 100644
--- a/ui/file_manager/video_player/js/video_player_native_controls.js
+++ b/ui/file_manager/video_player/js/video_player_native_controls.js
@@ -2,48 +2,363 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-/**
- * Video player with native controls
- *
- * @constructor
- * @struct
- */
-function NativeControlsVideoPlayer() {
- this.videos_ = null;
- this.currentPos_ = 0;
- this.videoElement_ = null;
+class NativeControlsVideoPlayer {
+ constructor() {
+ this.videos_ = null;
+ this.currentPos_ = 0;
+ this.videoElement_ = null;
+ }
+
+ /**
+ * Initializes the video player window. This method must be called after DOM
+ * initialization.
+ * @param {!Array<!FileEntry>} videos List of videos.
+ */
+ prepare(videos) {
+ this.videos_ = videos;
+
+ // TODO: Move these setting to html and css file when
+ // we are confident to remove the feature flag
+ this.videoElement_ = document.createElement('video');
+ this.videoElement_.controls = true;
+ this.videoElement_.controlsList = 'nodownload';
+ this.videoElement_.style.pointerEvents = 'auto';
+ getRequiredElement('video-container').appendChild(this.videoElement_);
+
+ // TODO: remove the element in html when remove the feature flag
+ getRequiredElement('controls-wrapper').style.display = 'none';
+ getRequiredElement('spinner-container').style.display = 'none';
+ getRequiredElement('error-wrapper').style.display = 'none';
+ getRequiredElement('thumbnail').style.display = 'none';
+ getRequiredElement('cast-container').style.display = 'none';
+
+ this.videoElement_.addEventListener('pause', this.onPause_.bind(this));
+
+ this.preparePlayList_();
+ this.addKeyControls_();
+ }
+
+ /**
+ * Attach arrow box for previous/next track to document and set
+ * 'multiple' attribute if user opens more than 1 videos.
+ *
+ * @private
+ */
+ preparePlayList_() {
+ let videoPlayerElement = getRequiredElement('video-player');
+ if (this.videos_.length > 1) {
+ videoPlayerElement.setAttribute('multiple', true);
+ } else {
+ videoPlayerElement.removeAttribute('multiple');
+ }
+
+ let arrowRight = queryRequiredElement('.arrow-box .arrow.right');
+ arrowRight.addEventListener(
+ 'click', this.advance_.bind(this, true /* next track */));
+ let arrowLeft = queryRequiredElement('.arrow-box .arrow.left');
+ arrowLeft.addEventListener(
+ 'click', this.advance_.bind(this, false /* previous track */));
+ }
+
+ /**
+ * Add keyboard controls to document.
+ *
+ * @private
+ */
+ addKeyControls_() {
+ document.addEventListener('keydown', (/** KeyboardEvent */ event) => {
+ const key =
+ (event.ctrlKey && event.shiftKey ? 'Ctrl-Shift-' : '') + event.key;
+ switch (key) {
+ // Handle debug shortcut keys.
+ case 'Ctrl-Shift-I': // Ctrl+Shift+I
+ chrome.fileManagerPrivate.openInspector('normal');
+ break;
+ case 'Ctrl-Shift-J': // Ctrl+Shift+J
+ chrome.fileManagerPrivate.openInspector('console');
+ break;
+ case 'Ctrl-Shift-C': // Ctrl+Shift+C
+ chrome.fileManagerPrivate.openInspector('element');
+ break;
+ case 'Ctrl-Shift-B': // Ctrl+Shift+B
+ chrome.fileManagerPrivate.openInspector('background');
+ break;
+
+ case 'k':
+ case 'MediaPlayPause':
+ this.togglePlayState_();
+ break;
+ case 'MediaTrackNext':
+ this.advance_(true /* next track */);
+ break;
+ case 'MediaTrackPrevious':
+ this.advance_(false /* previous track */);
+ break;
+ case 'l':
+ this.skip_(true /* forward */);
+ break;
+ case 'j':
+ this.skip_(false /* backward */);
+ break;
+ case 'BrowserBack':
+ chrome.app.window.current().close();
+ break;
+ case 'MediaStop':
+ // TODO: Define "Stop" behavior.
+ break;
+ }
+ });
+
+ getRequiredElement('video-container')
+ .addEventListener('click', (/** MouseEvent */ event) => {
+ // Turn on loop mode when ctrl+click while the video is playing.
+ // If the video is paused, ignore ctrl and play the video.
+ if (event.ctrlKey && !this.videoElement_.paused) {
+ this.setLoopedModeWithFeedback_(true);
+ event.preventDefault();
+ }
+ });
+ }
+
+ /**
+ * Skips forward/backward.
+ * @param {boolean} forward Whether to skip forward or backward.
+ * @private
+ */
+ skip_(forward) {
+ let secondsToSkip = Math.min(
+ NativeControlsVideoPlayer.PROGRESS_MAX_SECONDS_TO_SKIP,
+ this.videoElement_.duration *
+ NativeControlsVideoPlayer.PROGRESS_MAX_RATIO_TO_SKIP);
+
+ if (!forward) {
+ secondsToSkip *= -1;
+ }
+
+ this.videoElement_.currentTime = Math.max(
+ Math.min(
+ this.videoElement_.currentTime + secondsToSkip,
+ this.videoElement_.duration),
+ 0);
+ }
+
+ /**
+ * Toggle play/pause.
+ *
+ * @private
+ */
+ togglePlayState_() {
+ if (this.videoElement_.paused) {
+ this.videoElement_.play();
+ } else {
+ this.videoElement_.pause();
+ }
+ }
+
+ /**
+ * Set the looped mode with feedback.
+ *
+ * @param {boolean} on Whether enabled or not.
+ * @private
+ */
+ setLoopedModeWithFeedback_(on) {
+ this.videoElement_.loop = on;
+ if (on) {
+ this.showNotification_('VIDEO_PLAYER_LOOPED_MODE');
+ }
+ }
+
+ /**
+ * Briefly show a text notification at top left corner.
+ *
+ * @param {string} identifier String identifier.
+ * @private
+ */
+ showNotification_(identifier) {
+ getRequiredElement('toast-content').textContent =
+ loadTimeData.getString(identifier);
+ getRequiredElement('toast').show();
+ }
+
+ /**
+ * Plays the first video.
+ */
+ playFirstVideo() {
+ this.currentPos_ = 0;
+ this.reloadCurrentVideo_(this.onFirstVideoReady_.bind(this));
+ }
+
+ /**
+ * Called when the first video is ready after starting to load.
+ * Make the video player app window the same dimension as the video source
+ * Restrict the app window inside the screen.
+ *
+ * @private
+ */
+ onFirstVideoReady_() {
+ let videoWidth = this.videoElement_.videoWidth;
+ let videoHeight = this.videoElement_.videoHeight;
+
+ let aspect = videoWidth / videoHeight;
+ let newWidth = videoWidth;
+ let newHeight = videoHeight;
+
+ let shrinkX = newWidth / window.screen.availWidth;
+ let shrinkY = newHeight / window.screen.availHeight;
+ if (shrinkX > 1 || shrinkY > 1) {
+ if (shrinkY > shrinkX) {
+ newHeight = newHeight / shrinkY;
+ newWidth = newHeight * aspect;
+ } else {
+ newWidth = newWidth / shrinkX;
+ newHeight = newWidth / aspect;
+ }
+ }
+
+ let oldLeft = window.screenX;
+ let oldTop = window.screenY;
+ let oldWidth = window.innerWidth;
+ let oldHeight = window.innerHeight;
+
+ if (!oldWidth && !oldHeight) {
+ oldLeft = window.screen.availWidth / 2;
+ oldTop = window.screen.availHeight / 2;
+ }
+
+ let appWindow = chrome.app.window.current();
+ appWindow.innerBounds.width = Math.round(newWidth);
+ appWindow.innerBounds.height = Math.round(newHeight);
+ appWindow.outerBounds.left =
+ Math.max(0, Math.round(oldLeft - (newWidth - oldWidth) / 2));
+ appWindow.outerBounds.top =
+ Math.max(0, Math.round(oldTop - (newHeight - oldHeight) / 2));
+ appWindow.show();
+
+ this.videoElement_.focus();
+ this.videoElement_.play();
+ }
+
+ /**
+ * Advance to next video when the current one ends.
+ * Not using 'ended' event because dragging the timeline
+ * thumb to the end when seeking will trigger 'ended' event.
+ *
+ * @private
+ */
+ onPause_() {
+ this.videoElement_.loop = false;
+ if (this.videoElement_.currentTime == this.videoElement_.duration) {
+ this.advance_(true);
+ }
+ }
+
+ /**
+ * Advances to the next (or previous) track.
+ *
+ * @param {boolean} direction True to the next, false to the previous.
+ * @private
+ */
+ advance_(direction) {
+ let newPos = this.currentPos_ + (direction ? 1 : -1);
+ if (newPos < 0 || newPos >= this.videos_.length) {
+ return;
+ }
+
+ this.currentPos_ = newPos;
+ this.reloadCurrentVideo_(() => {
+ this.videoElement_.play();
+ });
+ }
+
+ /**
+ * Reloads the current video.
+ *
+ * @param {function()=} opt_callback Completion callback.
+ */
+ reloadCurrentVideo_(opt_callback) {
+ let videoPlayerElement = getRequiredElement('video-player');
+ if (this.currentPos_ == (this.videos_.length - 1)) {
+ videoPlayerElement.setAttribute('last-video', true);
+ } else {
+ videoPlayerElement.removeAttribute('last-video');
+ }
+
+ if (this.currentPos_ === 0) {
+ videoPlayerElement.setAttribute('first-video', true);
+ } else {
+ videoPlayerElement.removeAttribute('first-video');
+ }
+
+ let currentVideo = this.videos_[this.currentPos_];
+ this.loadVideo_(currentVideo, opt_callback);
+ }
+
+ /**
+ * Loads the video file.
+ * @param {!FileEntry} video Entry of the video to be played.
+ * @param {function()=} opt_callback Completion callback.
+ * @private
+ */
+ async loadVideo_(video, opt_callback) {
+ document.title = video.name;
+ const videoUrl = video.toURL();
+
+ if (opt_callback) {
+ this.videoElement_.addEventListener(
+ 'loadedmetadata', opt_callback, {once: true});
+ }
+
+ this.videoElement_.src = videoUrl;
+
+ const subtitleUrl = await this.searchSubtitle_(videoUrl);
+ if (subtitleUrl) {
+ const track =
+ assertInstanceof(document.createElement('track'), HTMLTrackElement);
+ track.src = subtitleUrl;
+ track.kind = 'subtitles';
+ track.default = true;
+ this.videoElement_.appendChild(track);
+ }
+ }
+
+ /**
+ * Search subtitle file corresponding to a video.
+ * @param {string} url a url of a video.
+ * @return {Promise} a Promise returns url of subtitle file, or an empty
+ * string.
+ */
+ async searchSubtitle_(url) {
+ const resolveLocalFileSystemWithExtension = (extension) => {
+ const subtitleUrl = this.getSubtitleUrl_(url, extension);
+ return new Promise(
+ window.webkitResolveLocalFileSystemURL.bind(null, subtitleUrl));
+ };
+
+ try {
+ const subtitle = await resolveLocalFileSystemWithExtension('.vtt');
+ return subtitle.toURL();
+ } catch (error) {
+ // TODO: figure out if there could be any error other than
+ // file not found or not accessible. If not, remove this log.
+ console.error(error);
+ return '';
+ }
+ }
+
+ /**
+ * Get the subtitle url.
+ *
+ * @private
+ * @param {string} srcUrl Source url of the video file.
+ * @param {string} extension Extension of the subtitle we are looking for.
+ * @return {string} Subtitle url.
+ */
+ getSubtitleUrl_(srcUrl, extension) {
+ return srcUrl.replace(/\.[^\.]+$/, extension);
+ }
}
/**
- * Initializes the video player window. This method must be called after DOM
- * initialization.
- * @param {!Array<!FileEntry>} videos List of videos.
- */
-NativeControlsVideoPlayer.prototype.prepare = function(videos) {
- this.videos_ = videos;
-
- // TODO: Move these setting to html and css file when
- // we are confident to remove the feature flag
- this.videoElement_ = document.createElement('video');
- this.videoElement_.controls = true;
- this.videoElement_.controlsList = 'nodownload';
- this.videoElement_.style.pointerEvents = 'auto';
- getRequiredElement('video-container').appendChild(this.videoElement_);
-
- // TODO: remove the element in html when remove the feature flag
- getRequiredElement('controls-wrapper').style.display = 'none';
- getRequiredElement('spinner-container').style.display = 'none';
- getRequiredElement('error-wrapper').style.display = 'none';
- getRequiredElement('thumbnail').style.display = 'none';
- getRequiredElement('cast-container').style.display = 'none';
-
- this.videoElement_.addEventListener('pause', this.onPause_.bind(this));
-
- this.preparePlayList_();
- this.addKeyControls_();
-};
-
-/**
* 10 seconds should be skipped when J/L key is pressed.
*/
NativeControlsVideoPlayer.PROGRESS_MAX_SECONDS_TO_SKIP = 10;
@@ -53,307 +368,3 @@
* seconds.
*/
NativeControlsVideoPlayer.PROGRESS_MAX_RATIO_TO_SKIP = 0.2;
-
-/**
- * Attach arrow box for previous/next track to document and set
- * 'multiple' attribute if user opens more than 1 videos.
- */
-NativeControlsVideoPlayer.prototype.preparePlayList_ = function() {
- let videoPlayerElement = getRequiredElement('video-player');
- if (this.videos_.length > 1) {
- videoPlayerElement.setAttribute('multiple', true);
- } else {
- videoPlayerElement.removeAttribute('multiple');
- }
-
- let arrowRight = queryRequiredElement('.arrow-box .arrow.right');
- arrowRight.addEventListener(
- 'click', this.advance_.wrap(this, true /* next track */));
- let arrowLeft = queryRequiredElement('.arrow-box .arrow.left');
- arrowLeft.addEventListener(
- 'click', this.advance_.wrap(this, false /* previous track */));
-};
-
-/**
- * Add keyboard controls to document.
- */
-NativeControlsVideoPlayer.prototype.addKeyControls_ = function() {
- document.addEventListener('keydown', function(e) {
- switch (util.getKeyModifiers(e) + e.key) {
- // Handle debug shortcut keys.
- case 'Ctrl-Shift-I': // Ctrl+Shift+I
- chrome.fileManagerPrivate.openInspector('normal');
- break;
- case 'Ctrl-Shift-J': // Ctrl+Shift+J
- chrome.fileManagerPrivate.openInspector('console');
- break;
- case 'Ctrl-Shift-C': // Ctrl+Shift+C
- chrome.fileManagerPrivate.openInspector('element');
- break;
- case 'Ctrl-Shift-B': // Ctrl+Shift+B
- chrome.fileManagerPrivate.openInspector('background');
- break;
-
- case 'k':
- case 'MediaPlayPause':
- this.togglePlayState_();
- break;
- case 'Escape':
- util.toggleFullScreen(
- chrome.app.window.current(),
- false); // Leave the full screen mode.
- break;
- case 'MediaTrackNext':
- this.advance_(true /* next track */);
- break;
- case 'MediaTrackPrevious':
- this.advance_(false /* previous track */);
- break;
- case 'l':
- this.skip_(true /* forward */);
- break;
- case 'j':
- this.skip_(false /* backward */);
- break;
- case 'BrowserBack':
- chrome.app.window.current().close();
- break;
- case 'MediaStop':
- // TODO: Define "Stop" behavior.
- break;
- }
- }.wrap(this));
-
- getRequiredElement('video-container')
- .addEventListener('click', (/** MouseEvent */ event) => {
- // Turn on loop mode when ctrl+click while the video is playing.
- // If the video is paused, ignore ctrl and play the video.
- if (event.ctrlKey && !this.videoElement_.paused) {
- this.setLoopedModeWithFeedback_(true);
- event.preventDefault();
- }
- });
-};
-
-/**
- * Skips forward/backward.
- * @param {boolean} forward Whether to skip forward or backward.
- * @private
- */
-NativeControlsVideoPlayer.prototype.skip_ = function(forward) {
- let secondsToSkip = Math.min(
- NativeControlsVideoPlayer.PROGRESS_MAX_SECONDS_TO_SKIP,
- this.videoElement_.duration *
- NativeControlsVideoPlayer.PROGRESS_MAX_RATIO_TO_SKIP);
-
- if (!forward) {
- secondsToSkip *= -1;
- }
-
- this.videoElement_.currentTime = Math.max(
- Math.min(
- this.videoElement_.currentTime + secondsToSkip,
- this.videoElement_.duration),
- 0);
-};
-
-/**
- * Toggle play/pause.
- */
-NativeControlsVideoPlayer.prototype.togglePlayState_ = function() {
- if (this.videoElement_.paused) {
- this.videoElement_.play();
- } else {
- this.videoElement_.pause();
- }
-};
-
-/**
- * Set the looped mode with feedback.
- *
- * @param {boolean} on Whether enabled or not.
- * @private
- */
-NativeControlsVideoPlayer.prototype.setLoopedModeWithFeedback_ = function(on) {
- this.videoElement_.loop = on;
- if (on) {
- this.showNotification_('VIDEO_PLAYER_LOOPED_MODE');
- }
-};
-
-/**
- * Briefly show a text notification at top left corner.
- *
- * @param {string} identifier String identifier.
- * @private
- */
-NativeControlsVideoPlayer.prototype.showNotification_ = function(identifier) {
- getRequiredElement('toast-content').textContent = str(identifier);
- getRequiredElement('toast').show();
-};
-
-/**
- * Plays the first video.
- */
-NativeControlsVideoPlayer.prototype.playFirstVideo = function() {
- this.currentPos_ = 0;
- this.reloadCurrentVideo_(this.onFirstVideoReady_.wrap(this));
-};
-
-/**
- * Called when the first video is ready after starting to load.
- * Make the video player app window the same dimension as the video source
- * Restrict the app window inside the screen.
- *
- * @private
- */
-NativeControlsVideoPlayer.prototype.onFirstVideoReady_ = function() {
- let videoWidth = this.videoElement_.videoWidth;
- let videoHeight = this.videoElement_.videoHeight;
-
- let aspect = videoWidth / videoHeight;
- let newWidth = videoWidth;
- let newHeight = videoHeight;
-
- let shrinkX = newWidth / window.screen.availWidth;
- let shrinkY = newHeight / window.screen.availHeight;
- if (shrinkX > 1 || shrinkY > 1) {
- if (shrinkY > shrinkX) {
- newHeight = newHeight / shrinkY;
- newWidth = newHeight * aspect;
- } else {
- newWidth = newWidth / shrinkX;
- newHeight = newWidth / aspect;
- }
- }
-
- let oldLeft = window.screenX;
- let oldTop = window.screenY;
- let oldWidth = window.innerWidth;
- let oldHeight = window.innerHeight;
-
- if (!oldWidth && !oldHeight) {
- oldLeft = window.screen.availWidth / 2;
- oldTop = window.screen.availHeight / 2;
- }
-
- let appWindow = chrome.app.window.current();
- appWindow.innerBounds.width = Math.round(newWidth);
- appWindow.innerBounds.height = Math.round(newHeight);
- appWindow.outerBounds.left =
- Math.max(0, Math.round(oldLeft - (newWidth - oldWidth) / 2));
- appWindow.outerBounds.top =
- Math.max(0, Math.round(oldTop - (newHeight - oldHeight) / 2));
- appWindow.show();
-
- this.videoElement_.focus();
- this.videoElement_.play();
-};
-
-/**
- * Advance to next video when the current one ends.
- * Not using 'ended' event because dragging the timeline
- * thumb to the end when seeking will trigger 'ended' event.
- *
- * @private
- */
-NativeControlsVideoPlayer.prototype.onPause_ = function() {
- this.videoElement_.loop = false;
- if (this.videoElement_.currentTime == this.videoElement_.duration) {
- this.advance_(true);
- }
-};
-
-/**
- * Advances to the next (or previous) track.
- *
- * @param {boolean} direction True to the next, false to the previous.
- * @private
- */
-NativeControlsVideoPlayer.prototype.advance_ = function(direction) {
- let newPos = this.currentPos_ + (direction ? 1 : -1);
- if (newPos < 0 || newPos >= this.videos_.length) {
- return;
- }
-
- this.currentPos_ = newPos;
- this.reloadCurrentVideo_(function() {
- this.videoElement_.play();
- }.wrap(this));
-};
-
-/**
- * Reloads the current video.
- *
- * @param {function()=} opt_callback Completion callback.
- */
-NativeControlsVideoPlayer.prototype.reloadCurrentVideo_ = function(
- opt_callback) {
- let videoPlayerElement = getRequiredElement('video-player');
- if (this.currentPos_ == (this.videos_.length - 1)) {
- videoPlayerElement.setAttribute('last-video', true);
- } else {
- videoPlayerElement.removeAttribute('last-video');
- }
-
- if (this.currentPos_ === 0) {
- videoPlayerElement.setAttribute('first-video', true);
- } else {
- videoPlayerElement.removeAttribute('first-video');
- }
-
- let currentVideo = this.videos_[this.currentPos_];
- this.loadVideo_(currentVideo, opt_callback);
-};
-
-/**
- * Loads the video file.
- * @param {!FileEntry} video Entry of the video to be played.
- * @param {function()=} opt_callback Completion callback.
- * @private
- */
-NativeControlsVideoPlayer.prototype.loadVideo_ =
- async function(video, opt_callback) {
- document.title = video.name;
- const videoUrl = video.toURL();
-
- if (opt_callback) {
- this.videoElement_.addEventListener(
- 'loadedmetadata', opt_callback, {once: true});
- }
-
- this.videoElement_.src = videoUrl;
-
- const subtitleUrl = await this.searchSubtitle_(videoUrl);
- if (subtitleUrl) {
- const track =
- assertInstanceof(document.createElement('track'), HTMLTrackElement);
- track.src = subtitleUrl;
- track.kind = 'subtitles';
- track.default = true;
- this.videoElement_.appendChild(track);
- }
-};
-
-/**
- * Search subtitle file corresponding to a video.
- * @param {string} url a url of a video.
- * @return {Promise} a Promise returns url of subtitle file, or an empty string.
- */
-NativeControlsVideoPlayer.prototype.searchSubtitle_ = async function(url) {
- const baseUrl = util.splitExtension(url)[0];
- const resolveLocalFileSystemWithExtension = function(extension) {
- return new Promise(
- window.webkitResolveLocalFileSystemURL.bind(null, baseUrl + extension));
- };
-
- try {
- const subtitle = await resolveLocalFileSystemWithExtension('.vtt');
- return subtitle.toURL();
- } catch (error) {
- // TODO: figure out if there could be any error other than
- // file not found or not accessible. If not, remove this log.
- console.error(error);
- return '';
- }
-};