-
Notifications
You must be signed in to change notification settings - Fork 73
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
Add xapi video verbs #17
base: master
Are you sure you want to change the base?
Changes from 74 commits
fa94009
6aea788
28c3266
6df9a91
ba72e7d
3ffd605
3a19499
910f714
749f896
73f9072
9ee8c07
6cc6009
fff638d
68c2482
3b357b4
8680f05
b559407
2cb2dce
920f80e
3527921
847bd71
157f527
60015e7
871fb29
e52a8dd
449df2d
4924b66
8c66406
8ef5ba9
e08d9c5
1c102c7
2b5ed92
9e8aa9b
9f22a59
828611e
c16cc62
1988a2d
6b57fc4
7245458
25ede9a
778330f
7561c34
a640440
dd3cefc
18be43b
c4e2861
3c6e0a9
1434112
129d56e
ebadcc9
c16d0fd
a95ae2c
b85a5ec
7100eb6
6c8dffa
954f51f
f82c2cb
a8e7b3f
215da89
52256cc
15473c2
15a58c8
b1e9a27
28b9654
5eb7492
9fe5f95
58d5638
02607f0
a3d024c
c516b82
a35b66c
56d8f0d
35daa0a
57f9e75
d292a6f
4086a79
66a7928
735bf09
745ab2a
44a36df
c9a68cf
5ab4d28
cb1b90e
0ef32d9
f210eba
467ee4c
4cf8602
2012f5b
ca9a196
48f51e1
ca6cd81
080f2ce
09583ab
a00b8b5
a4e7dd6
ff3ffb8
af8e985
5558d8f
ff9f1c0
ec75b45
719add4
ef6aef4
4995735
2eb31e9
7fc4d18
31ce7e6
c8940b2
a88c17b
f3581ac
0585bbb
94aa8c2
40db1c3
4346b92
6c0dbf9
2224edd
d0bc373
83c472a
f132369
f3ed616
d6e4f24
91936f3
166c8d3
5c1f995
1f18bdb
f977a64
283f2e3
6dd68ac
8223c20
12912a8
cbef273
4f70c16
94ff7b3
035d23a
3707be2
d243bb1
5501579
7ab090b
e1af0c1
9d612b7
3e6a584
85b0b86
ef6d166
4f595f7
26b8f94
0ea01a7
17052be
d9d2202
706855d
9bc078a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,12 @@ H5P.VideoHtml5 = (function ($) { | |
function Html5(sources, options, l10n) { | ||
var self = this; | ||
|
||
/** | ||
* xAPI Helper. | ||
* @private | ||
*/ | ||
var videoXAPI = new H5P.VideoXAPI(self); | ||
|
||
/** | ||
* Displayed when the video is buffering | ||
* @private | ||
|
@@ -35,6 +41,12 @@ H5P.VideoHtml5 = (function ($) { | |
var stateBeforeChangingQuality; | ||
var currentTimeBeforeChangingQuality; | ||
|
||
/** | ||
* Track xAPI statement data for video events. | ||
* @private | ||
*/ | ||
var lastSend = null; | ||
|
||
/** | ||
* Avoids firing the same event twice. | ||
* @private | ||
|
@@ -136,6 +148,29 @@ H5P.VideoHtml5 = (function ($) { | |
} | ||
}); | ||
|
||
/** | ||
* Create the xAPI object for the 'Loaded' event. | ||
*/ | ||
var getLoadedParams = function () { | ||
var ccEnabled = false; | ||
var ccLanguage; | ||
|
||
for (var i = 0; i < video.textTracks.length; i++) { | ||
if (video.textTracks[i].mode === 'showing') { | ||
ccEnabled = true; | ||
ccLanguage = video.textTracks[i].language; | ||
} | ||
} | ||
|
||
var isFullScreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen || false; | ||
|
||
return videoXAPI.getArgsXAPIInitialized(video.currentTime, video.videoWidth, video.videoHeight, video.playbackRate, video.volume, ccEnabled, ccLanguage); | ||
|
||
}; | ||
|
||
// Set duration used for xAPI statements. | ||
videoXAPI.duration = video.duration; | ||
|
||
/** | ||
* Helps registering events. | ||
* | ||
|
@@ -146,6 +181,8 @@ H5P.VideoHtml5 = (function ($) { | |
*/ | ||
var mapEvent = function (native, h5p, arg) { | ||
video.addEventListener(native, function () { | ||
var extraArg = null; | ||
var extraTrigger = null; | ||
switch (h5p) { | ||
case 'stateChange': | ||
if (lastState === arg) { | ||
|
@@ -158,8 +195,67 @@ H5P.VideoHtml5 = (function ($) { | |
delete options.startAt; | ||
} | ||
|
||
break; | ||
if (arg === H5P.Video.PLAYING) { | ||
if (videoXAPI.seeking === true) { | ||
extraArg = videoXAPI.getArgsXAPISeeked(videoXAPI.seekedTo); | ||
extraTrigger = 'seeked'; | ||
lastSend = 'seeked'; | ||
videoXAPI.seeking = false; | ||
} else if (lastSend !== 'play') { | ||
extraArg = videoXAPI.getArgsXAPIPlayed(video.currentTime); | ||
extraTrigger = 'play'; | ||
lastSend = 'play'; | ||
} | ||
} | ||
|
||
if (arg === H5P.Video.PAUSED) { | ||
// Put together extraArg for sending to xAPI statement. | ||
if (!video.seeking && videoXAPI.seeking === false && video.currentTime !== video.duration) { | ||
extraTrigger = "paused"; | ||
extraArg = videoXAPI.getArgsXAPIPaused(video.currentTime, video.duration); | ||
lastSend = 'paused'; | ||
} | ||
} | ||
|
||
if (arg === H5P.Video.ENDED) { | ||
// Send extra trigger for giving progress on ended call to xAPI. | ||
var length = video.duration; | ||
if (length > 0) { | ||
// Length passed in as current time, because at end of video when this is fired currentTime reset to 0 if on loop | ||
var progress = videoXAPI.getProgress(length, length); | ||
if (progress >= 1) { | ||
extraTrigger = "finished"; | ||
extraArg = videoXAPI.getArgsXAPICompleted(video.currentTime, video.duration); | ||
lastSend = 'finished'; | ||
} | ||
} | ||
} | ||
break; | ||
case 'seeked': | ||
return; // Seek is tracked differently based on time difference in timeupdate. | ||
break; | ||
case 'seeking': | ||
return; // Just need to store current time for seeked event. | ||
break; | ||
case 'volumechange' : | ||
arg = videoXAPI.getArgsXAPIVolumeChanged(video.currentTime, video.muted, video.volume); | ||
lastSend = 'volumechange'; | ||
break; | ||
case 'play': | ||
if (videoXAPI.seeking === false && lastSend != h5p) { | ||
arg = videoXAPI.getArgsXAPIPlayed(video.currentTime); | ||
lastSend = h5p; | ||
} else { | ||
arg = videoXAPI.getArgsXAPISeeked(videoXAPI.seekedTo); | ||
lastSend = 'seeked'; | ||
videoXAPI.seeking = false; | ||
h5p = 'seeked'; | ||
} | ||
break; | ||
case 'fullscreen': | ||
arg = videoXAPI.getArgsXAPIFullScreen(video.currentTime, video.videoWidth, video.videoHeight); | ||
lastSend = h5p; | ||
break; | ||
case 'loaded': | ||
isLoaded = true; | ||
|
||
|
@@ -181,8 +277,12 @@ H5P.VideoHtml5 = (function ($) { | |
video.addEventListener('durationchange', andLoaded, false); | ||
return; | ||
} | ||
break; | ||
|
||
extraTrigger = 'xAPIloaded'; | ||
extraArg = getLoadedParams(); | ||
lastSend = 'xAPIloaded'; | ||
|
||
break; | ||
case 'error': | ||
// Handle error and get message. | ||
arg = error(arguments[0], arguments[1]); | ||
|
@@ -207,6 +307,11 @@ H5P.VideoHtml5 = (function ($) { | |
break; | ||
} | ||
self.trigger(h5p, arg); | ||
|
||
// Make extra calls for events with needed values for xAPI statement. | ||
if (extraTrigger != null && extraArg != null) { | ||
self.trigger(extraTrigger, extraArg); | ||
} | ||
}, false); | ||
}; | ||
|
||
|
@@ -418,8 +523,12 @@ H5P.VideoHtml5 = (function ($) { | |
video.play(); | ||
video.pause(); | ||
} | ||
|
||
if (videoXAPI.seeking === false) { | ||
videoXAPI.previousTime = video.currentTime; | ||
} | ||
video.currentTime = time; | ||
videoXAPI.seeking = true; | ||
videoXAPI.seekedTo = time; | ||
}; | ||
|
||
/** | ||
|
@@ -589,14 +698,18 @@ H5P.VideoHtml5 = (function ($) { | |
mapEvent('loadedmetadata', 'loaded'); | ||
mapEvent('error', 'error'); | ||
mapEvent('ratechange', 'playbackRateChange'); | ||
mapEvent('seeking','seeking', H5P.Video.PAUSED); | ||
mapEvent('timeupdate', 'timeupdate', H5P.Video.PLAYING); | ||
mapEvent('volumechange', 'volumechange'); | ||
mapEvent('play', 'play', H5P.Video.PLAYING); | ||
mapEvent('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', 'fullscreen'); | ||
|
||
if (!video.controls) { | ||
// Disable context menu(right click) to prevent controls. | ||
video.addEventListener('contextmenu', function (event) { | ||
event.preventDefault(); | ||
}, false); | ||
} | ||
|
||
// Display throbber when buffering/loading video. | ||
self.on('stateChange', function (event) { | ||
var state = event.data; | ||
|
@@ -622,6 +735,32 @@ H5P.VideoHtml5 = (function ($) { | |
}); | ||
}); | ||
|
||
// xAPI extension events for video. | ||
self.on('seeked', function (event) { | ||
this.triggerXAPI('seeked', event.data); | ||
}); | ||
self.on('volumechange', function (event) { | ||
this.triggerXAPI('interacted', event.data); | ||
}); | ||
self.on('finished', function (event) { | ||
//triggered as finished to be seperate from H5Ps completed, | ||
//but statement is sent as completed and differentiated by object.id | ||
this.triggerXAPI('completed', event.data); | ||
}) | ||
self.on('fullscreen', function (event) { | ||
// @todo: Not currently used. | ||
this.triggerXAPI('interacted', event.data); | ||
}); | ||
self.on('play', function (event) { | ||
this.triggerXAPI('played', event.data); | ||
}); | ||
self.on('xAPIloaded', function (event) { | ||
this.triggerXAPI('initialized', event.data); | ||
}) | ||
self.on('paused', function (event) { | ||
this.triggerXAPI('paused', event.data); | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion There already is a listener for 'loaded', and you could put this code block there, then there's no need to have this code in every handler (for now html5.js and youtube.js). |
||
// Video controls are ready | ||
nextTick(function () { | ||
self.trigger('ready'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussion
This works nicely. In some of my test runs, I however got a progress like
0.999
andfinished
wasn't triggered. I also noticed that sometime (but not always), when you pause a video for the first time, the segment says it started at 0.01 instead of 0. Could be related.I have not yet looked in to this, and this may well be a problem in H5P, but maybe you have an idea since you worked with the video library lately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was getting this randomly in my testing, too (progress getting close to 1.0, but not quite reaching it). I noticed when repeatedly pausing/playing the video, the recorded end time when pausing and the recorded start time when playing would be a few milliseconds apart.
Example
playedSegments
where I paused an 8-second video twice, once around the 3 second mark, and once around the 5 second mark:0.018[.]3.116[,]3.124[.]5.056[,]5.074[.]8.021
We've talked at length about what "completing" a video means, and I'm pretty sure it's not "watched every last millisecond of the video." But it's hard to pin down (what if a video rolls credits for a couple minutes at the end, can you skip that? What if a student skips a few seconds of a repetitive part in the middle of the video?).
For our purposes, I would be ok with allowing some delta away from 100% that would still be considered "complete"; we could say
if (progress >= 0.95)
, or maybe define a class constant for some delta value, and then sayif (progress >= 1 - delta)
. Thoughts?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can expand on this a bit. The video profile CoP agrees that there are use cases (technical or otherwise) that would require complete to trigger on a progress value < 1.0.
We have a profile solution for this that we are finalizing. It requires adding an extension in the initialized statement that defines the minimum progress value used by the Activity Provider (H5P in this case) to trigger the completed statement. As long as this value exists then analysis can still be implement across data sets with different minimum values.
The issue is.. where is this value set for H5P? I think this is what @figureone is pointing out at the end. Short term solution is hard code
if (progress >= 0.95)
, but we would ideally prefer a UI element to set the delta. That being said, we are in a rush to get this done so that may be a change for down the road.Whether
0.999
issue is H5P or not.. I am not sure. @jhaag75 & @liveaspankaj did most of the development on the communities videojs version. Perhaps they can say if they ran into this issue on there as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had faced the issue of fractional time gap between paused/play even if they happen at the same point in the player.
For that what I did in VideoJS prototype was:
When a play happens, I checked the difference between time of play and last paused. And when the time difference was less than 1 seconds. I used the paused time instead of actual reported play time.
e.g. paused at 12.31 second. then play at 12.78 second. I use 12.31 second as the play time.
Probably, 1-2 second adjustment for margin of error could be done for progress calculation too?
Regarding adjusting progress value. I guess, it is a tough decision, and could be left to the user or decided by the authoring tool (H5P)