Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [RGOeX-26584] implement XBlock completion by the view video progress #690

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## Added
- Implement XBlock completion by the view video progress (RGOeX-26584)

## [1.3.1] 2024-05-14

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions video_xblock/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def get_frag(self, **context):
'static/js/videojs/videojs-tabindex.js',
'static/js/videojs/toggle-button.js',
'static/js/videojs/videojs-event-plugin.js',
'static/js/videojs/videojs-completions.js',
'static/js/videojs/fullscreen-extends.js',
]

Expand All @@ -273,6 +274,12 @@ def player_data_setup(context):
"playbackRates": [0.5, 1, 1.5, 2],
"plugins": {
"xblockEventPlugin": {},
"xblockCompletionPlugin": {
"completionEnabled": True,
"startTime": context['start_time'],
"endTime": context['end_time'],
"isComplete": False, # TODO: should be replaced with dynamic data from the Backend
},
"offset": {
"start": context['start_time'],
"end": context['end_time'],
Expand Down
1 change: 1 addition & 0 deletions video_xblock/backends/brightcove.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ def get_frag(self, **context):
'static/js/videojs/videojs-tabindex.js',
'static/js/videojs/toggle-button.js',
'static/js/videojs/videojs-event-plugin.js',
'static/js/videojs/videojs-completions.js',
'static/js/videojs/brightcove-videojs-init.js',
'static/js/videojs/fullscreen-extends.js',
]
Expand Down
3 changes: 3 additions & 0 deletions video_xblock/static/js/student-view/video-xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function VideoXBlockStudentViewInit(runtime, element) {
var xblockElement = typeof(element[0]) !== 'undefined' ? element[0] : element;
var stateHandlerUrl = runtime.handlerUrl(xblockElement, 'save_player_state');
var eventHandlerUrl = runtime.handlerUrl(xblockElement, 'publish_event');
var completionHandlerUrl = runtime.handlerUrl(xblockElement, 'publish_completion');
var downloadTranscriptHandlerUrl = runtime.handlerUrl(xblockElement, 'download_transcript');
var usageId = (
xblockElement.attributes['data-usage-id'] || // Open edX runtime
Expand All @@ -29,10 +30,12 @@ function VideoXBlockStudentViewInit(runtime, element) {
window.videoXBlockState.handlers || {
saveState: {},
analytics: {},
completions: {},
downloadTranscriptChanged: {}
};
handlers.saveState[usageId] = stateHandlerUrl;
handlers.analytics[usageId] = eventHandlerUrl;
handlers.completions[usageId] = completionHandlerUrl;
/** Send data to server by POSTing it to appropriate VideoXBlock handler */
function sendData(handlerUrl, data) {
$.ajax({
Expand Down
10 changes: 9 additions & 1 deletion video_xblock/static/js/videojs/brightcove-videojs-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ domReady(function() {
'use strict';
// Videojs 5/6 shim
var registerPlugin = videojs.registerPlugin || videojs.plugin;

var player = videojs(window.videoPlayerId);
window.videojs = videojs;

registerPlugin('xblockEventPlugin', window.xblockEventPlugin);
player.xblockEventPlugin();

registerPlugin('xblockCompletionPlugin', window.xblockCompletionPlugin);
player.xblockCompletionPlugin({
completionEnabled: true,
startTime: window.playerStartTime,
endTime: window.playerEndTime,
isComplete: false, // TODO: should be replaced with dynamic data from the Backend
});

registerPlugin('offset', window.vjsoffset);
player.offset({
start: window.playerStartTime,
Expand Down
110 changes: 110 additions & 0 deletions video_xblock/static/js/videojs/videojs-completions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* This part is responsible for completion of the video player.
*/
(function() {
'use strict';
/**
* Videojs plugin.
* Listens for progress and send event to parent frame to be processed as a completion request
* @param {Object} options - Plugin options passed in at initialization time.
*/
function XBlockCompletionPlugin(options) {
var player = this;
this.options = options;
this.lastSentTime = undefined;
this.isComplete = this.options.isComplete;
this.completionPercentage = this.options.completionPercentage || 0.95;
this.startTime = this.options.startTime;
this.endTime = this.options.endTime;
this.isEnabled = this.options.completionEnabled;

/** Determine what point in the video (in seconds from the beginning) counts as complete. */
this.calculateCompleteAfterTime = function(startTime, endTime) {
return (endTime - startTime) * this.completionPercentage;
};

/** How many seconds to wait after a POST fails to try again. */
this.repostDelaySeconds = function() {
return 3.0;
};

if (this.endTime) {
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, this.endTime);
}

if (this.isEnabled) {
/** Event handler to check if the video is complete, and submit
* a completion if it is.
*
* If the timeupdate handler doesn't fire after the required
* percentage, this will catch any fully complete videos.
*/
player.on('ended', function() {
player.handleEnded();
});

/** Event handler to check video progress, and mark complete if
* greater than completionPercentage
*/
player.on('timeupdate', function() {
player.handleTimeUpdate(player.currentTime());
});
}

/** Handler to call when the ended event is triggered */
this.handleEnded = function() {
if (this.isComplete) {
return;
}
this.markCompletion();
};

/** Handler to call when a timeupdate event is triggered */
this.handleTimeUpdate = function(currentTime) {
var duration;

if (this.isComplete) {
return;
}

if (this.lastSentTime !== undefined && currentTime - this.lastSentTime < this.repostDelaySeconds()) {
// Throttle attempts to submit in case of network issues
return;
}

if (this.completeAfterTime === undefined) {
// Duration is not available at initialization time
duration = player.duration();
if (!duration) {
// duration is not yet set. Wait for another event,
// or fall back to 'ended' handler.
return;
}
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, duration);
}

if (currentTime > this.completeAfterTime) {
this.markCompletion(currentTime);
}
};

/** Send event to parent frame to be processed as a completion request */
this.markCompletion = function(currentTime) {
this.isComplete = true;
this.lastSentTime = currentTime;
parent.postMessage({
action: 'completions',
info: { completion: 1.0 },
xblockUsageId: getXblockUsageId(),
xblockFullUsageId: getXblockFullUsageId(),
}, document.location.protocol + '//' + document.location.host);
};

return this;
}
window.xblockCompletionPlugin = XBlockCompletionPlugin;
// add plugin if player has already initialized
if (window.videojs) {
window.videojs.plugin('xblockCompletionPlugin', xblockCompletionPlugin); // eslint-disable-line no-undef
}
}).call(this);
4 changes: 4 additions & 0 deletions video_xblock/video_xblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from opaque_keys.edx.keys import CourseKey
from webob import Response
from xblock.core import XBlock
from xblock.completable import XBlockCompletionMode
from xblock.fields import Boolean, Dict, Scope, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
Expand Down Expand Up @@ -70,6 +71,9 @@ class VideoXBlock(
See `BaseVideoPlayer.basic_fields` and `BaseVideoPlayer.advanced_fields`.
"""

has_custom_completion = True
completion_mode = XBlockCompletionMode.COMPLETABLE

icon_class = "video"

display_name = String(
Expand Down