diff --git a/xmodule/assets/video/public/js/commands.js b/xmodule/assets/video/public/js/commands.js
new file mode 100644
index 000000000000..0044973d958e
--- /dev/null
+++ b/xmodule/assets/video/public/js/commands.js
@@ -0,0 +1,137 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Video commands module.
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoCommands(state, i18n) {
+ if (!(this instanceof VideoCommands)) {
+ return new VideoCommands(state, i18n);
+ }
+
+ _.bindAll(this, 'destroy');
+ this.state = state;
+ this.state.videoCommands = this;
+ this.i18n = i18n;
+ this.commands = [];
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoCommands.prototype = {
+ /**
+ * Initializes the module by loading commands and binding events.
+ */
+ initialize: function () {
+ this.commands = this.getCommands();
+ this.state.el.on('destroy', this.destroy);
+ },
+
+ /**
+ * Cleans up the module by removing event handlers and deleting the instance.
+ */
+ destroy: function () {
+ this.state.el.off('destroy', this.destroy);
+ delete this.state.videoCommands;
+ },
+
+ /**
+ * Executes a given command with optional arguments.
+ *
+ * @param {String} command - The name of the command to execute
+ * @param {...*} args - Additional arguments to pass to the command
+ */
+ execute: function (command, ...args) {
+ if (_.has(this.commands, command)) {
+ this.commands[command].execute(this.state, ...args);
+ } else {
+ console.log(`Command "${command}" is not available.`);
+ }
+ },
+
+ /**
+ * Returns the available commands as an object.
+ *
+ * @return {Object} - A dictionary of available commands
+ */
+ getCommands: function () {
+ const commands = {};
+ const commandsList = [
+ playCommand,
+ pauseCommand,
+ togglePlaybackCommand,
+ toggleMuteCommand,
+ toggleFullScreenCommand,
+ setSpeedCommand,
+ skipCommand,
+ ];
+
+ _.each(commandsList, (command) => {
+ commands[command.name] = command;
+ });
+
+ return commands;
+ },
+};
+
+/**
+ * Command constructor.
+ *
+ * @constructor
+ * @param {String} name - The name of the command
+ * @param {Function} execute - The function to execute the command
+ */
+function Command(name, execute) {
+ this.name = name;
+ this.execute = execute;
+}
+
+// Individual command definitions
+const playCommand = new Command('play', (state) => {
+ state.videoPlayer.play();
+});
+
+const pauseCommand = new Command('pause', (state) => {
+ state.videoPlayer.pause();
+});
+
+const togglePlaybackCommand = new Command('togglePlayback', (state) => {
+ if (state.videoPlayer.isPlaying()) {
+ pauseCommand.execute(state);
+ } else {
+ playCommand.execute(state);
+ }
+});
+
+const toggleMuteCommand = new Command('toggleMute', (state) => {
+ state.videoVolumeControl.toggleMute();
+});
+
+const toggleFullScreenCommand = new Command('toggleFullScreen', (state) => {
+ state.videoFullScreen.toggle();
+});
+
+const setSpeedCommand = new Command(
+ 'speed',
+ (state, speed) => {
+ state.videoSpeedControl.setSpeed(state.speedToString(speed));
+ }
+);
+
+const skipCommand = new Command('skip', (state, doNotShowAgain) => {
+ if (doNotShowAgain) {
+ state.videoBumper.skipAndDoNotShowAgain();
+ } else {
+ state.videoBumper.skip();
+ }
+});
+
+export {VideoCommands};
diff --git a/xmodule/assets/video/public/js/events_bumper_plugin.js b/xmodule/assets/video/public/js/events_bumper_plugin.js
new file mode 100644
index 000000000000..0b93b947a294
--- /dev/null
+++ b/xmodule/assets/video/public/js/events_bumper_plugin.js
@@ -0,0 +1,179 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Events module.
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @param {Object} options - Additional options for the plugin
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoEventsBumperPlugin(state, i18n, options) {
+ if (!(this instanceof VideoEventsBumperPlugin)) {
+ return new VideoEventsBumperPlugin(state, i18n, options);
+ }
+
+ _.bindAll(
+ this,
+ 'onReady',
+ 'onPlay',
+ 'onEnded',
+ 'onShowLanguageMenu',
+ 'onHideLanguageMenu',
+ 'onSkip',
+ 'onShowCaptions',
+ 'onHideCaptions',
+ 'destroy'
+ );
+
+ this.state = state;
+ this.options = _.extend({}, options);
+ this.state.videoEventsBumperPlugin = this;
+ this.i18n = i18n;
+
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoEventsBumperPlugin.moduleName = 'EventsBumperPlugin';
+
+VideoEventsBumperPlugin.prototype = {
+ /**
+ * Initialize the plugin by binding the required event handlers
+ */
+ initialize: function () {
+ this.events = {
+ ready: this.onReady,
+ play: this.onPlay,
+ 'ended stop': this.onEnded,
+ skip: this.onSkip,
+ 'language_menu:show': this.onShowLanguageMenu,
+ 'language_menu:hide': this.onHideLanguageMenu,
+ 'captions:show': this.onShowCaptions,
+ 'captions:hide': this.onHideCaptions,
+ destroy: this.destroy,
+ };
+ this.bindHandlers();
+ },
+
+ /**
+ * Bind event handlers to the video state element
+ */
+ bindHandlers: function () {
+ this.state.el.on(this.events);
+ },
+
+ /**
+ * Cleanup by removing event handlers and destroying the plugin instance
+ */
+ destroy: function () {
+ this.state.el.off(this.events);
+ delete this.state.videoEventsBumperPlugin;
+ },
+
+ /**
+ * Handle the `ready` event
+ */
+ onReady: function () {
+ this.log('edx.video.bumper.loaded');
+ },
+
+ /**
+ * Handle the `play` event
+ */
+ onPlay: function () {
+ this.log('edx.video.bumper.played', { currentTime: this.getCurrentTime() });
+ },
+
+ /**
+ * Handle the `ended` and `stop` events
+ */
+ onEnded: function () {
+ this.log('edx.video.bumper.stopped', { currentTime: this.getCurrentTime() });
+ },
+
+ /**
+ * Handle the `skip` event
+ */
+ onSkip: function (event, doNotShowAgain) {
+ const info = { currentTime: this.getCurrentTime() };
+ const eventName = `edx.video.bumper.${doNotShowAgain ? 'dismissed' : 'skipped'}`;
+ this.log(eventName, info);
+ },
+
+ /**
+ * Handle when the language menu is shown
+ */
+ onShowLanguageMenu: function () {
+ this.log('edx.video.bumper.transcript.menu.shown');
+ },
+
+ /**
+ * Handle when the language menu is hidden
+ */
+ onHideLanguageMenu: function () {
+ this.log('edx.video.bumper.transcript.menu.hidden');
+ },
+
+ /**
+ * Handle when captions are shown
+ */
+ onShowCaptions: function () {
+ this.log('edx.video.bumper.transcript.shown', { currentTime: this.getCurrentTime() });
+ },
+
+ /**
+ * Handle when captions are hidden
+ */
+ onHideCaptions: function () {
+ this.log('edx.video.bumper.transcript.hidden', { currentTime: this.getCurrentTime() });
+ },
+
+ /**
+ * Get the current time of the video
+ *
+ * @return {Number} - The current time of the video in seconds
+ */
+ getCurrentTime: function () {
+ const player = this.state.videoPlayer;
+ return player ? player.currentTime : 0;
+ },
+
+ /**
+ * Get the duration of the video
+ *
+ * @return {Number} - The duration of the video in seconds
+ */
+ getDuration: function () {
+ const player = this.state.videoPlayer;
+ return player ? player.duration() : 0;
+ },
+
+ /**
+ * Log an event
+ *
+ * @param {String} eventName - The name of the event to log
+ * @param {Object} data - Additional data to log with the event
+ */
+ log: function (eventName, data) {
+ const logInfo = _.extend(
+ {
+ host_component_id: this.state.id,
+ bumper_id: this.state.config.sources[0] || '',
+ duration: this.getDuration(),
+ code: 'html5',
+ },
+ data,
+ this.options.data
+ );
+
+ Logger.log(eventName, logInfo);
+ },
+};
+
+export { VideoEventsBumperPlugin };
\ No newline at end of file
diff --git a/xmodule/assets/video/public/js/events_plugin.js b/xmodule/assets/video/public/js/events_plugin.js
new file mode 100644
index 000000000000..9c315b9f4443
--- /dev/null
+++ b/xmodule/assets/video/public/js/events_plugin.js
@@ -0,0 +1,200 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Events module.
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @param {Object} options - Additional options for the plugin
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoEventsPlugin(state, i18n, options) {
+ if (!(this instanceof VideoEventsPlugin)) {
+ return new VideoEventsPlugin(state, i18n, options);
+ }
+
+ _.bindAll(
+ this,
+ 'onReady',
+ 'onPlay',
+ 'onPause',
+ 'onComplete',
+ 'onEnded',
+ 'onSeek',
+ 'onSpeedChange',
+ 'onAutoAdvanceChange',
+ 'onShowLanguageMenu',
+ 'onHideLanguageMenu',
+ 'onSkip',
+ 'onShowTranscript',
+ 'onHideTranscript',
+ 'onShowCaptions',
+ 'onHideCaptions',
+ 'destroy'
+ );
+
+ this.state = state;
+ this.options = _.extend({}, options);
+ this.state.videoEventsPlugin = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoEventsPlugin.moduleName = 'EventsPlugin';
+VideoEventsPlugin.prototype = {
+ /**
+ * Initialize the EventsPlugin module.
+ */
+ initialize: function () {
+ this.events = {
+ ready: this.onReady,
+ play: this.onPlay,
+ pause: this.onPause,
+ complete: this.onComplete,
+ 'ended stop': this.onEnded,
+ seek: this.onSeek,
+ skip: this.onSkip,
+ speedchange: this.onSpeedChange,
+ autoadvancechange: this.onAutoAdvanceChange,
+ 'language_menu:show': this.onShowLanguageMenu,
+ 'language_menu:hide': this.onHideLanguageMenu,
+ 'transcript:show': this.onShowTranscript,
+ 'transcript:hide': this.onHideTranscript,
+ 'captions:show': this.onShowCaptions,
+ 'captions:hide': this.onHideCaptions,
+ destroy: this.destroy,
+ };
+
+ this.bindHandlers();
+ this.emitPlayVideoEvent = true;
+ },
+
+ /**
+ * Destroy the EventPlugin instance and cleanup.
+ */
+ destroy: function () {
+ this.state.el.off(this.events);
+ delete this.state.videoEventsPlugin;
+ },
+
+ bindHandlers: function () {
+ this.state.el.on(this.events);
+ },
+
+ onReady: function () {
+ this.log('load_video');
+ },
+
+ onPlay: function () {
+ if (this.emitPlayVideoEvent) {
+ this.log('play_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = false;
+ }
+ },
+
+ onPause: function () {
+ this.log('pause_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = true;
+ },
+
+ onComplete: function () {
+ this.log('complete_video', {currentTime: this.getCurrentTime()});
+ },
+
+ onEnded: function () {
+ this.log('stop_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = true;
+ },
+
+ onSkip: function (event, doNotShowAgain) {
+ var info = {currentTime: this.getCurrentTime()},
+ eventName = doNotShowAgain ? 'do_not_show_again_video' : 'skip_video';
+ this.log(eventName, info);
+ },
+
+ onSeek: function (event, time, oldTime, type) {
+ this.log('seek_video', {
+ old_time: oldTime,
+ new_time: time,
+ type: type
+ });
+ this.emitPlayVideoEvent = true;
+ },
+
+ onSpeedChange: function (event, newSpeed, oldSpeed) {
+ this.log('speed_change_video', {
+ current_time: this.getCurrentTime(),
+ old_speed: this.state.speedToString(oldSpeed),
+ new_speed: this.state.speedToString(newSpeed)
+ });
+ },
+
+ onAutoAdvanceChange: function (event, enabled) {
+ this.log('auto_advance_change_video', {
+ enabled: enabled
+ });
+ },
+
+ onShowLanguageMenu: function () {
+ this.log('edx.video.language_menu.shown');
+ },
+
+ onHideLanguageMenu: function () {
+ this.log('edx.video.language_menu.hidden', {language: this.getCurrentLanguage()});
+ },
+
+ onShowTranscript: function () {
+ this.log('show_transcript', {current_time: this.getCurrentTime()});
+ },
+
+ onHideTranscript: function () {
+ this.log('hide_transcript', {current_time: this.getCurrentTime()});
+ },
+
+ onShowCaptions: function () {
+ this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()});
+ },
+
+ onHideCaptions: function () {
+ this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()});
+ },
+
+ getCurrentTime: function () {
+ var player = this.state.videoPlayer,
+ startTime = this.state.config.startTime,
+ currentTime;
+ currentTime = player ? player.currentTime : 0;
+ // if video didn't start from 0(it's a subsection of video), subtract the additional time at start
+ if (startTime) {
+ currentTime = currentTime ? currentTime - startTime : 0;
+ }
+ return currentTime;
+ },
+
+ getCurrentLanguage: function () {
+ var language = this.state.lang;
+ return language;
+ },
+
+ log: function (eventName, data) {
+ // use startTime and endTime to calculate the duration to handle the case where only a subsection of video is used
+ var endTime = this.state.config.endTime || this.state.duration,
+ startTime = this.state.config.startTime;
+
+ var logInfo = _.extend({
+ id: this.state.id,
+ // eslint-disable-next-line no-nested-ternary
+ code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5',
+ duration: endTime - startTime
+ }, data, this.options.data);
+ Logger.log(eventName, logInfo);
+ }
+};
+
+export {VideoEventsPlugin};
diff --git a/xmodule/assets/video/public/js/focus_grabber.js b/xmodule/assets/video/public/js/focus_grabber.js
new file mode 100644
index 000000000000..66b7ca713d99
--- /dev/null
+++ b/xmodule/assets/video/public/js/focus_grabber.js
@@ -0,0 +1,132 @@
+/*
+ * 025_focus_grabber.js
+ *
+ * Purpose: Provide a way to focus on autohidden Video controls.
+ *
+ *
+ * Because in HTML player mode we have a feature of autohiding controls on
+ * mouse inactivity, sometimes focus is lost from the currently selected
+ * control. What's more, when all controls are autohidden, we can't get to any
+ * of them because by default browser does not place hidden elements on the
+ * focus chain.
+ *
+ * To get around this minor annoyance, this module will manage 2 placeholder
+ * elements that will be invisible to the user's eye, but visible to the
+ * browser. This will allow for a sneaky stealing of focus and placing it where
+ * we need (on hidden controls).
+ *
+ * This code has been moved to a separate module because it provides a concrete
+ * block of functionality that can be turned on (off).
+ */
+
+/*
+ * "If you want to climb a mountain, begin at the top."
+ *
+ * ~ Zen saying
+ */
+
+import $ from 'jquery';
+
+// FocusGrabber module.
+const FocusGrabber = function (state) {
+ var dfd = $.Deferred();
+
+ state.focusGrabber = {};
+
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+ _bindHandlers(state);
+
+ dfd.resolve();
+ return dfd.promise();
+};
+
+// Private functions.
+
+function _makeFunctionsPublic(state) {
+ var methodsDict = {
+ disableFocusGrabber: disableFocusGrabber,
+ enableFocusGrabber: enableFocusGrabber,
+ onFocus: onFocus
+ };
+
+ state.bindTo(methodsDict, state.focusGrabber, state);
+}
+
+function _renderElements(state) {
+ state.focusGrabber.elFirst = state.el.find('.focus_grabber.first');
+ state.focusGrabber.elLast = state.el.find('.focus_grabber.last');
+
+ // From the start, the Focus Grabber must be disabled so that
+ // tabbing (switching focus) does not land the user on one of the
+ // placeholder elements (elFirst, elLast).
+ state.focusGrabber.disableFocusGrabber();
+}
+
+function _bindHandlers(state) {
+ state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus);
+ state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus);
+
+ // When the video container element receives programmatic focus, then
+ // on un-focus ('blur' event) we should trigger a 'mousemove' event so
+ // as to reveal autohidden controls.
+ state.el.on('blur', function () {
+ state.el.trigger('mousemove');
+ });
+}
+
+// Public functions.
+
+function enableFocusGrabber() {
+ var tabIndex;
+
+ // When the Focus Grabber is being enabled, there are two different
+ // scenarios:
+ //
+ // 1.) Currently focused element was inside the video player.
+ // 2.) Currently focused element was somewhere else on the page.
+ //
+ // In the first case we must make sure that the video player doesn't
+ // loose focus, even though the controls are autohidden.
+ if ($(document.activeElement).parents().hasClass('video')) {
+ tabIndex = -1;
+ } else {
+ tabIndex = 0;
+ }
+
+ this.focusGrabber.elFirst.attr('tabindex', tabIndex);
+ this.focusGrabber.elLast.attr('tabindex', tabIndex);
+
+ // Don't loose focus. We are inside video player on some control, but
+ // because we can't remain focused on a hidden element, we will shift
+ // focus to the main video element.
+ //
+ // Once the main element will receive the un-focus ('blur') event, a
+ // 'mousemove' event will be triggered, and the video controls will
+ // receive focus once again.
+ if (tabIndex === -1) {
+ this.el.focus();
+
+ this.focusGrabber.elFirst.attr('tabindex', 0);
+ this.focusGrabber.elLast.attr('tabindex', 0);
+ }
+}
+
+function disableFocusGrabber() {
+ // Only programmatic focusing on these elements will be available.
+ // We don't want the user to focus on them (for example with the 'Tab'
+ // key).
+ this.focusGrabber.elFirst.attr('tabindex', -1);
+ this.focusGrabber.elLast.attr('tabindex', -1);
+}
+
+function onFocus(event, params) {
+ // Once the Focus Grabber placeholder elements will gain focus, we will
+ // trigger 'mousemove' event so that the autohidden controls will
+ // become visible.
+ this.el.trigger('mousemove');
+
+ this.focusGrabber.disableFocusGrabber();
+}
+
+export {FocusGrabber};
diff --git a/xmodule/assets/video/public/js/play_pause_control.js b/xmodule/assets/video/public/js/play_pause_control.js
new file mode 100644
index 000000000000..c2b2bdb2aa0e
--- /dev/null
+++ b/xmodule/assets/video/public/js/play_pause_control.js
@@ -0,0 +1,95 @@
+import $ from 'jquery'; // jQuery import
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * PlayPauseControl function
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoPlayPauseControl(state, i18n) {
+ if (!(this instanceof VideoPlayPauseControl)) {
+ return new VideoPlayPauseControl(state, i18n);
+ }
+
+ _.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
+ this.state = state;
+ this.state.videoPlayPauseControl = this;
+ this.i18n = i18n;
+
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoPlayPauseControl.prototype = {
+ template: [
+ '',
+ ].join(''),
+
+ /** Initializes the module. */
+ initialize: function () {
+ this.el = $(this.template);
+ this.render();
+ this.bindHandlers();
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function () {
+ this.state.el.find('.vcr').prepend(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function () {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ play: this.play,
+ 'pause ended': this.pause,
+ destroy: this.destroy,
+ });
+ },
+
+ onClick: function (event) {
+ event.preventDefault();
+ this.state.videoCommands.execute('togglePlayback');
+ },
+
+ play: function () {
+ this.el
+ .addClass('pause')
+ .removeClass('play')
+ .attr({title: gettext('Pause'), 'aria-label': gettext('Pause')})
+ .find('.icon')
+ .removeClass('fa-play')
+ .addClass('fa-pause');
+ },
+
+ pause: function () {
+ this.el
+ .removeClass('pause')
+ .addClass('play')
+ .attr({title: gettext('Play'), 'aria-label': gettext('Play')})
+ .find('.icon')
+ .removeClass('fa-pause')
+ .addClass('fa-play');
+ },
+
+ destroy: function () {
+ this.el.remove();
+ this.state.el.off('destroy', this.destroy);
+ delete this.state.videoPlayPauseControl;
+ },
+};
+
+export {VideoPlayPauseControl};
diff --git a/xmodule/assets/video/public/js/play_placeholder.js b/xmodule/assets/video/public/js/play_placeholder.js
new file mode 100644
index 000000000000..d3d7384573bb
--- /dev/null
+++ b/xmodule/assets/video/public/js/play_placeholder.js
@@ -0,0 +1,87 @@
+import $ from 'jquery'; // jQuery import
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Video Play placeholder control function.
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoPlayPlaceholder(state, i18n) {
+ if (!(this instanceof VideoPlayPlaceholder)) {
+ return new VideoPlayPlaceholder(state, i18n);
+ }
+
+ _.bindAll(this, 'onClick', 'hide', 'show', 'destroy');
+ this.state = state;
+ this.state.videoPlayPlaceholder = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoPlayPlaceholder.prototype = {
+ destroy: function () {
+ this.el.off('click', this.onClick);
+ this.state.el.off({
+ destroy: this.destroy,
+ play: this.hide,
+ 'ended pause': this.show,
+ });
+ this.hide();
+ delete this.state.videoPlayPlaceholder;
+ },
+
+ /**
+ * Indicates whether the placeholder should be shown.
+ * We display it for HTML5 videos on iPad and Android devices.
+ * @return {Boolean}
+ */
+ shouldBeShown: function () {
+ return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType();
+ },
+
+ /** Initializes the module. */
+ initialize: function () {
+ if (!this.shouldBeShown()) {
+ return false;
+ }
+
+ this.el = this.state.el.find('.btn-play');
+ this.bindHandlers();
+ this.show();
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function () {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ destroy: this.destroy,
+ play: this.hide,
+ 'ended pause': this.show,
+ });
+ },
+
+ onClick: function () {
+ this.state.videoCommands.execute('play');
+ },
+
+ hide: function () {
+ this.el
+ .addClass('is-hidden')
+ .attr({'aria-hidden': 'true', tabindex: -1});
+ },
+
+ show: function () {
+ this.el
+ .removeClass('is-hidden')
+ .attr({'aria-hidden': 'false', tabindex: 0});
+ }
+};
+
+export {VideoPlayPlaceholder};
\ No newline at end of file
diff --git a/xmodule/assets/video/public/js/play_skip_control.js b/xmodule/assets/video/public/js/play_skip_control.js
new file mode 100644
index 000000000000..3be7d8b05398
--- /dev/null
+++ b/xmodule/assets/video/public/js/play_skip_control.js
@@ -0,0 +1,90 @@
+import $ from 'jquery'; // jQuery import
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Play/skip control module
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoPlaySkipControl(state, i18n) {
+ if (!(this instanceof VideoPlaySkipControl)) {
+ return new VideoPlaySkipControl(state, i18n);
+ }
+
+ _.bindAll(this, 'play', 'onClick', 'destroy');
+ this.state = state;
+ this.state.videoPlaySkipControl = this;
+ this.i18n = i18n;
+
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoPlaySkipControl.prototype = {
+ template: [
+ '',
+ ].join(''),
+
+ /** Initializes the module. */
+ initialize: function () {
+ this.el = $(this.template);
+ this.render();
+ this.bindHandlers();
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function () {
+ this.state.el.find('.vcr').prepend(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function () {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ play: this.play,
+ destroy: this.destroy,
+ });
+ },
+
+ onClick: function (event) {
+ event.preventDefault();
+ if (this.state.videoPlayer.isPlaying()) {
+ this.state.videoCommands.execute('skip');
+ } else {
+ this.state.videoCommands.execute('play');
+ }
+ },
+
+ play: function () {
+ this.el
+ .removeClass('play')
+ .addClass('skip')
+ .attr('title', gettext('Skip'))
+ .find('.icon')
+ .removeClass('fa-play')
+ .addClass('fa-step-forward');
+ // Disable possibility to pause the video.
+ this.state.el.find('video').off('click');
+ },
+
+ destroy: function () {
+ this.el.remove();
+ this.state.el.off('destroy', this.destroy);
+ delete this.state.videoPlaySkipControl;
+ },
+};
+
+export {VideoPlaySkipControl};
\ No newline at end of file
diff --git a/xmodule/assets/video/public/js/save_state_plugin.js b/xmodule/assets/video/public/js/save_state_plugin.js
new file mode 100644
index 000000000000..524699fd923b
--- /dev/null
+++ b/xmodule/assets/video/public/js/save_state_plugin.js
@@ -0,0 +1,189 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import Time from 'time.js';
+
+'use strict';
+
+/**
+ * Save state module.
+ *
+ * @constructor
+ * @param {Object} state - The object containing the state of the video
+ * @param {Object} i18n - The object containing strings with translations
+ * @param {Object} options - Options (e.g., events to handle)
+ * @return {jQuery.Promise} - A resolved jQuery promise
+ */
+function VideoSaveStatePlugin(state, i18n, options) {
+ if (!(this instanceof VideoSaveStatePlugin)) {
+ return new VideoSaveStatePlugin(state, i18n, options);
+ }
+
+ _.bindAll(
+ this,
+ 'onSpeedChange',
+ 'onAutoAdvanceChange',
+ 'saveStateHandler',
+ 'bindUnloadHandler',
+ 'onUnload',
+ 'onYoutubeAvailability',
+ 'onLanguageChange',
+ 'destroy'
+ );
+
+ this.state = state;
+ this.options = _.extend({events: []}, options);
+ this.state.videoSaveStatePlugin = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoSaveStatePlugin.moduleName = 'SaveStatePlugin';
+VideoSaveStatePlugin.prototype = {
+ /**
+ * Initializes the save state plugin and binds required handlers
+ */
+ initialize: function () {
+ this.events = {
+ speedchange: this.onSpeedChange,
+ autoadvancechange: this.onAutoAdvanceChange,
+ play: this.bindUnloadHandler,
+ 'pause destroy': this.saveStateHandler,
+ 'language_menu:change': this.onLanguageChange,
+ youtube_availability: this.onYoutubeAvailability,
+ };
+ this.bindHandlers();
+ },
+
+ /**
+ * Binds the appropriate event handlers to the state element or user-provided events
+ */
+ bindHandlers: function () {
+ if (this.options.events.length) {
+ _.each(
+ this.options.events,
+ function (eventName) {
+ if (_.has(this.events, eventName)) {
+ const callback = this.events[eventName];
+ this.state.el.on(eventName, callback);
+ }
+ },
+ this
+ );
+ } else {
+ this.state.el.on(this.events);
+ }
+ this.state.el.on('destroy', this.destroy);
+ },
+
+ /**
+ * Binds the unload event handler once
+ */
+ bindUnloadHandler: _.once(function () {
+ $(window).on('unload.video', this.onUnload);
+ }),
+
+ /**
+ * Cleans up the plugin by removing event handlers and deleting the instance
+ */
+ destroy: function () {
+ this.state.el.off(this.events).off('destroy', this.destroy);
+ $(window).off('unload.video', this.onUnload);
+ delete this.state.videoSaveStatePlugin;
+ },
+
+ /**
+ * Handles speed change events
+ *
+ * @param {Event} event - The event object
+ * @param {number} newSpeed - The new playback speed
+ */
+ onSpeedChange: function (event, newSpeed) {
+ this.saveState(true, {speed: newSpeed});
+ this.state.storage.setItem('speed', newSpeed, true);
+ this.state.storage.setItem('general_speed', newSpeed);
+ },
+
+ /**
+ * Handles auto-advance toggle events
+ *
+ * @param {Event} event - The event object
+ * @param {boolean} enabled - Whether auto-advance is enabled
+ */
+ onAutoAdvanceChange: function (event, enabled) {
+ this.saveState(true, {auto_advance: enabled});
+ this.state.storage.setItem('auto_advance', enabled);
+ },
+
+ /**
+ * Saves the state when triggered directly by an event
+ */
+ saveStateHandler: function () {
+ this.saveState(true);
+ },
+
+ /**
+ * Saves the state during a `window.unload` event
+ */
+ onUnload: function () {
+ this.saveState();
+ },
+
+ /**
+ * Handles language change events
+ *
+ * @param {Event} event - The event object
+ * @param {string} langCode - The new language code
+ */
+ onLanguageChange: function (event, langCode) {
+ this.state.storage.setItem('language', langCode);
+ },
+
+ /**
+ * Handles YouTube availability changes
+ *
+ * @param {Event} event - The event object
+ * @param {boolean} youtubeIsAvailable - Whether YouTube is available
+ */
+ onYoutubeAvailability: function (event, youtubeIsAvailable) {
+ if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) {
+ this.saveState(true, {youtube_is_available: youtubeIsAvailable});
+ }
+ },
+
+ /**
+ * Saves the current state of the video
+ *
+ * @param {boolean} async - Whether to save asynchronously
+ * @param {Object} [additionalData] - Additional data to save
+ */
+ saveState: function (async, data) {
+ if (this.state.config.saveStateEnabled) {
+ if (!($.isPlainObject(data))) {
+ data = {
+ saved_video_position: this.state.videoPlayer.currentTime
+ };
+ }
+
+ if (data.speed) {
+ this.state.storage.setItem('speed', data.speed, true);
+ }
+
+ if (_.has(data, 'saved_video_position')) {
+ this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true);
+ data.saved_video_position = Time.formatFull(data.saved_video_position);
+ }
+
+ $.ajax({
+ url: this.state.config.saveStateUrl,
+ type: 'POST',
+ async: !!async,
+ dataType: 'json',
+ data: data
+ });
+ }
+ },
+};
+
+export {VideoSaveStatePlugin};
diff --git a/xmodule/assets/video/public/js/skip_control.js b/xmodule/assets/video/public/js/skip_control.js
new file mode 100644
index 000000000000..326c8e8fbb64
--- /dev/null
+++ b/xmodule/assets/video/public/js/skip_control.js
@@ -0,0 +1,70 @@
+import $ from 'jquery'; // jQuery import
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * VideoSkipControl function
+ *
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations
+ * @return {jQuery.Promise} Returns a resolved jQuery promise
+ */
+function VideoSkipControl(state, i18n) {
+ if (!(this instanceof VideoSkipControl)) {
+ return new VideoSkipControl(state, i18n);
+ }
+
+ _.bindAll(this, 'onClick', 'render', 'destroy');
+ this.state = state;
+ this.state.videoSkipControl = this;
+ this.i18n = i18n;
+
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+}
+
+VideoSkipControl.prototype = {
+ template: [
+ '',
+ ].join(''),
+
+ initialize: function () {
+ this.el = $(this.template);
+ this.bindHandlers();
+ },
+
+ /** Creates any necessary DOM elements, attach them, and set their, initial configuration. */
+ render: function () {
+ this.state.el.find('.vcr .control').after(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function () {
+ this.el.on('click', this.onClick);
+
+ this.state.el.on({
+ 'play.skip': _.once(this.render),
+ 'destroy.skip': this.destroy,
+ });
+ },
+
+ onClick: function (event) {
+ event.preventDefault();
+ this.state.videoCommands.execute('skip', true);
+ },
+
+ destroy: function () {
+ this.el.remove();
+ this.state.el.off('.skip');
+ delete this.state.videoSkipControl;
+ },
+};
+
+export {VideoSkipControl};
\ No newline at end of file
diff --git a/xmodule/assets/video/public/js/video_block_main.js b/xmodule/assets/video/public/js/video_block_main.js
index 902c6e704f21..c3f7372fb8dc 100644
--- a/xmodule/assets/video/public/js/video_block_main.js
+++ b/xmodule/assets/video/public/js/video_block_main.js
@@ -4,31 +4,32 @@ import _ from 'underscore';
import {VideoStorage} from './video_storage';
import {VideoPoster} from './poster';
import {VideoTranscriptDownloadHandler} from './video_accessible_menu';
+import {VideoSkipControl} from './skip_control';
+import {VideoPlayPlaceholder} from './play_placeholder';
+import {VideoPlaySkipControl} from './play_skip_control';
+import {VideoPlayPauseControl} from './play_pause_control';
+import {VideoSocialSharingHandler} from './video_social_sharing';
+import {FocusGrabber} from './focus_grabber';
+import {VideoCommands} from "./commands";
+import {VideoEventsBumperPlugin} from "./events_bumper_plugin";
+import {VideoEventsPlugin} from "./events_plugin";
+import {VideoSaveStatePlugin} from "./save_state_plugin";
+
// TODO: Uncomment the imports
// import { initialize } from './initialize'; // Assuming this function is imported
// import {
-// FocusGrabber,
// VideoControl,
-// VideoPlayPlaceholder,
-// VideoPlayPauseControl,
// VideoProgressSlider,
// VideoSpeedControl,
// VideoVolumeControl,
// VideoQualityControl,
// VideoFullScreen,
// VideoCaption,
-// VideoCommands,
// VideoContextMenu,
-// VideoSaveStatePlugin,
-// VideoEventsPlugin,
// VideoCompletionHandler,
// VideoTranscriptFeedback,
// VideoAutoAdvanceControl,
-// VideoPlaySkipControl,
-// VideoSkipControl,
-// VideoEventsBumperPlugin,
-// VideoSocialSharing,
// VideoBumper,
// } from './video_modules'; // Assuming all necessary modules are grouped here
@@ -57,37 +58,36 @@ console.log('In video_block_main.js file');
const bumperMetadata = el.data('bumper-metadata');
const autoAdvanceEnabled = el.data('autoadvance-enabled') === 'True';
- const mainVideoModules = []
- // TODO: Uncomment the code
- // const mainVideoModules = [
- // FocusGrabber,
- // VideoControl,
- // VideoPlayPlaceholder,
- // VideoPlayPauseControl,
- // VideoProgressSlider,
- // VideoSpeedControl,
- // VideoVolumeControl,
- // VideoQualityControl,
- // VideoFullScreen,
- // VideoCaption,
- // VideoCommands,
- // VideoContextMenu,
- // VideoSaveStatePlugin,
- // VideoEventsPlugin,
- // VideoCompletionHandler,
- // VideoTranscriptFeedback,
- // ].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []);
+ const mainVideoModules = [
+ FocusGrabber,
+ // VideoControl,
+ VideoPlayPlaceholder,
+ VideoPlayPauseControl,
+ // VideoProgressSlider,
+ // VideoSpeedControl,
+ // VideoVolumeControl,
+ // VideoQualityControl,
+ // VideoFullScreen,
+ // VideoCaption,
+ VideoCommands,
+ // VideoContextMenu,
+ VideoSaveStatePlugin,
+ VideoEventsPlugin,
+ // VideoCompletionHandler,
+ // VideoTranscriptFeedback,
+ // ].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []);
+ ]
const bumperVideoModules = [
// VideoControl,
- // VideoPlaySkipControl,
- // VideoSkipControl,
+ VideoPlaySkipControl,
+ VideoSkipControl,
// VideoVolumeControl,
// VideoCaption,
- // VideoCommands,
- // VideoSaveStatePlugin,
+ VideoCommands,
+ VideoSaveStatePlugin,
// VideoTranscriptFeedback,
- // VideoEventsBumperPlugin,
+ VideoEventsBumperPlugin,
// VideoCompletionHandler,
];
@@ -125,7 +125,7 @@ console.log('In video_block_main.js file');
saveStateUrl: state.metadata.saveStateUrl,
});
- // VideoSocialSharing(el);
+ VideoSocialSharingHandler(el);
if (bumperMetadata) {
VideoPoster(el, {
@@ -184,4 +184,3 @@ console.log('In video_block_main.js file');
// oldVideo(null, true);
}());
-
diff --git a/xmodule/assets/video/public/js/video_social_sharing.js b/xmodule/assets/video/public/js/video_social_sharing.js
new file mode 100644
index 000000000000..6d8bd19e5413
--- /dev/null
+++ b/xmodule/assets/video/public/js/video_social_sharing.js
@@ -0,0 +1,77 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * Video Social Sharing control module.
+ *
+ * @constructor
+ * @param {jQuery.Element} element - The container element for the video social sharing controls
+ * @param {Object} options - Additional options for the module
+ */
+function VideoSocialSharingHandler(element, options) {
+ if (!(this instanceof VideoSocialSharingHandler)) {
+ return new VideoSocialSharingHandler(element, options);
+ }
+
+ _.bindAll(this, 'clickHandler', 'copyHandler', 'hideHandler', 'showHandler');
+
+ this.container = element;
+
+ if (this.container.find('.wrapper-downloads .wrapper-social-share')) {
+ this.initialize();
+ }
+
+ return false;
+}
+
+VideoSocialSharingHandler.prototype = {
+ // Initializes the module.
+ initialize: function () {
+ this.el = this.container.find('.wrapper-social-share');
+ this.baseVideoUrl = this.el.data('url');
+ this.course_id = this.container.data('courseId');
+ this.block_id = this.container.data('blockId');
+ this.el.on('click', '.social-share-link', this.clickHandler);
+
+ this.closeBtn = this.el.find('.close-btn');
+ this.toggleBtn = this.el.find('.social-toggle-btn');
+ this.copyBtn = this.el.find('.public-video-copy-btn');
+ this.shareContainer = this.el.find('.container-social-share');
+ this.closeBtn.on('click', this.hideHandler);
+ this.toggleBtn.on('click', this.showHandler);
+ this.copyBtn.on('click', this.copyHandler);
+ },
+
+ // Fire an analytics event on share button click.
+ clickHandler: function (event) {
+ const source = $(event.currentTarget).data('source');
+ this.sendAnalyticsEvent(source);
+ },
+
+ hideHandler: function (event) {
+ this.shareContainer.hide();
+ this.toggleBtn.show();
+ },
+
+ showHandler: function (event) {
+ this.shareContainer.show();
+ this.toggleBtn.hide();
+ },
+
+ copyHandler: function (event) {
+ navigator.clipboard.writeText(this.copyBtn.data('url'));
+ },
+
+ // Send an analytics event for share button tracking.
+ sendAnalyticsEvent: function (source) {
+ window.analytics.track('edx.social.video.share_button.clicked', {
+ source: source,
+ video_block_id: this.container.data('blockId'),
+ course_id: this.container.data('courseId'),
+ });
+ },
+};
+
+export {VideoSocialSharingHandler};
\ No newline at end of file