-
Notifications
You must be signed in to change notification settings - Fork 732
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5631 from bjester/interactive-video-transcript
Interactive video transcript
- Loading branch information
Showing
38 changed files
with
2,261 additions
and
139 deletions.
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
integration_testing/features/learner/learner-engage-video-transcript.feature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
Feature: Learner engages with content of the video kind using transcript | ||
Leaner can engage with video content using the accompanying transcript feature | ||
|
||
Background: | ||
Given I am signed in as a learner user | ||
And there are one or more channels imported on the device with video content containing captions | ||
And I am on the *Channels* page for a channel with captioned video content | ||
|
||
Scenario: Browse and find captioned video content | ||
When I am on the *Channels* page for <channel> | ||
Then I see the *Channels > '<channel>'* breadcrumb | ||
And I see all the topics for the channel <channel> | ||
When I click the topic <topic> | ||
Then I see the *Channels > '<channel'> > '<topic>' breadcrumb | ||
And I see all the subtopics and resources of the topic <topic> | ||
When I click the subtopic <subtopic> | ||
Then I see the *Channels > '<channel'> > '<topic>' > '<subtopic>' breadcrumb | ||
And I see all the subtopics and resources of the subtopic <subtopic> | ||
And I recognize <resource> resource as a video by the content type icon in the upper left corner | ||
|
||
Scenario: Open video | ||
Given that <resource> resource is a video | ||
When I click the <resource> resource | ||
Then I see the *Channels > '<channel>' > '<topic>' > '<subtopic>' > '<resource>'* breadcrumb | ||
And I see the <resource> content | ||
And I see a *CC* button to control captions | ||
And I see a *Globe* button to control caption language | ||
|
||
Scenario: Engage with the captioned video content controls | ||
When I click the *CC* button | ||
Then I see an option for *Subtitles* | ||
And I see an option for *Transcript* | ||
When I click *Subtitles* | ||
Then I see captions overlaid as subtitles on the video | ||
When I click *Transcript* | ||
Then I see a transcript of the captions alongside the video | ||
When I click the *Globe* button | ||
Then I see the <language_option> option | ||
When I select <language_option> | ||
Then I see subtitles in <language> | ||
And I see the transcript in <language> | ||
|
||
Scenario: Engaging with the transcript | ||
When the transcript is enabled | ||
And the video is playing | ||
Then I see transcript cues highlighted within the transcript | ||
And I see the transcript automatically scroll to the next cue | ||
When I hover the mouse over the transcript | ||
Then the transcript stops scrolling automatically | ||
When I click a transcript cue | ||
Then the video seeks to the time of the transcript cue | ||
When I move the mouse outside of the transcript | ||
Then the transcript resumes automatically scrolling | ||
|
||
|
||
Examples: | ||
| channel | topic | subtopic | resource | language_option | language | | ||
| Touchable Earth (en) | India | Culture in India | Girl's Clothing | Français, langue française | French | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
kolibri/plugins/media_player/assets/src/mixins/videojsButtonMixin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import videojs from 'video.js'; | ||
|
||
/** | ||
* @param {String} videojsComponent A string of the videojs component to extend | ||
*/ | ||
export default function videojsButtonMixin(videojsComponent) { | ||
return class extends videojs.getComponent(videojsComponent) { | ||
/** | ||
* @param player | ||
* @param options | ||
* @param ready | ||
*/ | ||
constructor(player, options, ready) { | ||
super(player, options, ready); | ||
|
||
// Add missing hide handler since we're not using video.js hover CSS to show it | ||
this.on(this.menuButton_.el().parentElement, 'mouseleave', () => { | ||
this.menu.hide(); | ||
}); | ||
} | ||
|
||
/** | ||
* Should build and return an instance of a Video.js Menu | ||
* @return {Menu} | ||
*/ | ||
buildMenu() { | ||
throw new Error('Not implemented'); | ||
} | ||
|
||
/** | ||
* @override | ||
* @return {Menu} | ||
*/ | ||
createMenu() { | ||
if (this.items) { | ||
this.items.forEach(item => item.dispose()); | ||
this.items = []; | ||
} | ||
|
||
const menu = this.buildMenu(); | ||
this.items = this.createItems(); | ||
this.items.forEach(item => { | ||
menu.addItem(item); | ||
item.on('hide', () => this.unpressButton()); | ||
}); | ||
|
||
return menu; | ||
} | ||
|
||
/** | ||
* Removes class that adds specific functionality we don't want | ||
* | ||
* @param {String} classNames | ||
* @return {String} | ||
*/ | ||
removePopupClass(classNames) { | ||
return classNames.replace(/\bvjs-menu-button-popup\b/, ' '); | ||
} | ||
}; | ||
} |
39 changes: 39 additions & 0 deletions
39
kolibri/plugins/media_player/assets/src/mixins/videojsMenuItemVueMixin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import videojsVueMixin from './videojsVueMixin'; | ||
|
||
/** | ||
* @param {Object} vueComponent A compiled vue component object | ||
*/ | ||
export default function videojsMenuItemVueMixin(vueComponent) { | ||
return class extends videojsVueMixin('MenuItem', vueComponent) { | ||
createVueComponent(options = {}) { | ||
const component = super.createVueComponent(options); | ||
component.$on('hide', () => this.trigger('hide')); | ||
return component; | ||
} | ||
|
||
/** | ||
* Pass responsibility to focus down to Vue component | ||
*/ | ||
focus() { | ||
this.getVueComponent().focus(); | ||
} | ||
|
||
/** | ||
* @override | ||
*/ | ||
selected() { | ||
return this.getVueComponent().selected; | ||
} | ||
|
||
/** | ||
* We don't need to handle clicks | ||
* @override | ||
*/ | ||
handleClick() {} | ||
|
||
/** | ||
* Remove Video.js tap event handling so it doesn't mess with menu on mobile | ||
*/ | ||
emitTapEvents() {} | ||
}; | ||
} |
126 changes: 126 additions & 0 deletions
126
kolibri/plugins/media_player/assets/src/mixins/videojsMenuVueMixin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import videojsVueMixin from './videojsVueMixin'; | ||
|
||
/** | ||
* @param {Object} vueComponent A compiled vue component object | ||
*/ | ||
export default function videojsMenuVueMixin(vueComponent) { | ||
return class extends videojsVueMixin('Menu', vueComponent) { | ||
/** | ||
* @param player | ||
* @param options | ||
*/ | ||
constructor(player, options) { | ||
super(player, options); | ||
|
||
this.isLocked = false; | ||
this.focusedChild_ = 0; | ||
} | ||
|
||
/** | ||
* `contentEl` is used when `addItem` is called, so this allows the addition of the text track | ||
* options (the languages) in the right spot | ||
* | ||
* @override | ||
* @return {*|Element} | ||
*/ | ||
contentEl() { | ||
return this.getVueComponent().contentEl(); | ||
} | ||
|
||
/** | ||
* Override parent's method, which adds event handlers we don't want | ||
* | ||
* @override | ||
* @param {Component|String} item The name or instance of the item to add | ||
*/ | ||
addItem(item) { | ||
this.addChild(item); | ||
} | ||
|
||
/** | ||
* Triggered by mouseenter of button container | ||
* | ||
* @override | ||
*/ | ||
show() { | ||
this.doShow(); | ||
} | ||
|
||
/** | ||
* Triggered by mouseleave of button container | ||
* | ||
* @override | ||
*/ | ||
hide() { | ||
this.doHide(); | ||
} | ||
|
||
/** | ||
* Triggered on click in ancestor | ||
* | ||
* @override | ||
*/ | ||
lockShowing() { | ||
this.doShow(true); | ||
} | ||
|
||
/** | ||
* Triggered on blur in ancestor | ||
* | ||
* @override | ||
*/ | ||
unlockShowing() { | ||
this.doHide(true); | ||
} | ||
|
||
/** | ||
* @param {Boolean} lock Whether or not to lock it open | ||
*/ | ||
doShow(lock = false) { | ||
const component = this.getVueComponent(); | ||
this.isLocked = this.isLocked || lock; | ||
|
||
if (!component || component.showing()) { | ||
return; | ||
} | ||
|
||
component.show(); | ||
} | ||
|
||
/** | ||
* @param {Boolean} unlock Whether or not to unlock it if it's locked open | ||
*/ | ||
doHide(unlock = false) { | ||
const component = this.getVueComponent(); | ||
|
||
if (!component || !component.showing() || (!unlock && this.isLocked)) { | ||
return; | ||
} | ||
|
||
this.isLocked = false; | ||
component.hide(unlock); | ||
} | ||
|
||
/** | ||
* Called by Video.js key event handlers | ||
*/ | ||
focus(index) { | ||
const children = this.children(); | ||
|
||
if (!children) { | ||
return; | ||
} | ||
|
||
if (!index && index !== 0) { | ||
index = this.focusedChild_; | ||
} else if (index >= children.length) { | ||
index = 0; | ||
} else if (index < 0) { | ||
index = children.length - 1; | ||
} | ||
|
||
this.focusedChild_ = index; | ||
children[index].focus(); | ||
} | ||
}; | ||
} |
59 changes: 59 additions & 0 deletions
59
kolibri/plugins/media_player/assets/src/mixins/videojsVueMixin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Vue from 'kolibri.lib.vue'; | ||
import store from 'kolibri.coreVue.vuex.store'; | ||
import videojs from 'video.js'; | ||
|
||
/** | ||
* @param {String} videojsComponent A string of the videojs component to extend | ||
* @param {Object} vueComponent A compiled vue component object | ||
*/ | ||
export default function videojsVueMixin(videojsComponent, vueComponent) { | ||
const VideojsComponent = videojs.getComponent(videojsComponent); | ||
const VueComponent = Vue.extend(vueComponent); | ||
|
||
return class extends VideojsComponent { | ||
/** | ||
* This is called by video.js code that usually constructs an element, but here we'll leverage | ||
* vue by calling it manually. | ||
* | ||
* @return {Element} | ||
*/ | ||
createEl() { | ||
return this.createVueComponent().$el; | ||
} | ||
|
||
/** | ||
* @param {Object} [options] | ||
* @return {VueComponent} | ||
*/ | ||
createVueComponent(options) { | ||
this.clearVueComponent(); | ||
this._vueComponent = new VueComponent(Object.assign({ store }, options)).$mount(); | ||
return this.getVueComponent(); | ||
} | ||
|
||
/** | ||
* @return {VueComponent} | ||
*/ | ||
getVueComponent() { | ||
return this._vueComponent; | ||
} | ||
|
||
/** | ||
* Clears held Vue component instance, destroying it first | ||
*/ | ||
clearVueComponent() { | ||
if (this._vueComponent) { | ||
this._vueComponent.$destroy(); | ||
this._vueComponent = null; | ||
} | ||
} | ||
|
||
/** | ||
* video.js hook to dispose this video.js component, so be sure to `clearComponent` | ||
*/ | ||
dispose() { | ||
this.clearVueComponent(); | ||
super.dispose(); | ||
} | ||
}; | ||
} |
Oops, something went wrong.