Merge pull request #10706 from edx/clrux/ac-188-no-draggabilly
Video: Adding closed captioning to the video player (not draggable)
This commit is contained in:
@@ -283,7 +283,7 @@ require.config({
|
||||
"osda":{
|
||||
exports: "osda",
|
||||
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"]
|
||||
},
|
||||
}
|
||||
// end of annotation tool files
|
||||
}
|
||||
});
|
||||
|
||||
@@ -247,6 +247,22 @@ html:not('.afontgarde') .icon-fallback-img {
|
||||
}
|
||||
}
|
||||
|
||||
.closed-captions {
|
||||
position: absolute;
|
||||
width: 85%;
|
||||
left: 5%;
|
||||
top: 70%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.closed-captions.is-visible {
|
||||
max-height: ($baseline * 3);
|
||||
border-radius: ($baseline / 5);
|
||||
padding: 8px ($baseline / 2) 8px ($baseline * 1.5);
|
||||
background: rgba(0, 0, 0, .75);
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
@@ -701,39 +717,44 @@ html:not('.afontgarde') .icon-fallback-img {
|
||||
.subtitles {
|
||||
@include float(left);
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
max-height: 460px;
|
||||
width: flex-grid(3, 9);
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
visibility: visible;
|
||||
|
||||
li {
|
||||
@extend %ui-fake-link;
|
||||
margin-bottom: 8px;
|
||||
border: 0;
|
||||
.subtitles-menu {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #0074b5; // AA compliant
|
||||
line-height: lh();
|
||||
list-style: none;
|
||||
|
||||
&.current {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
li {
|
||||
@extend %ui-fake-link;
|
||||
margin-bottom: 8px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #0074b5; // AA compliant
|
||||
line-height: lh();
|
||||
|
||||
&.focused {
|
||||
outline: #000 dotted thin;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
&.current {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&.focused {
|
||||
outline: #000 dotted thin;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
margin-bottom: 0;
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<div class="closed-captions"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
|
||||
@@ -48,6 +48,12 @@
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the captioning control to the video player', function() {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.video')).toContain('.toggle-captions');
|
||||
expect($('.video')).toContain('.closed-captions');
|
||||
});
|
||||
|
||||
it('fetch the transcript in HTML5 mode', function () {
|
||||
runs(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
@@ -122,16 +128,16 @@
|
||||
|
||||
it('bind the mouse movement', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.subtitles')).toHandle('mouseover');
|
||||
expect($('.subtitles')).toHandle('mouseout');
|
||||
expect($('.subtitles')).toHandle('mousemove');
|
||||
expect($('.subtitles')).toHandle('mousewheel');
|
||||
expect($('.subtitles')).toHandle('DOMMouseScroll');
|
||||
expect($('.subtitles-menu')).toHandle('mouseover');
|
||||
expect($('.subtitles-menu')).toHandle('mouseout');
|
||||
expect($('.subtitles-menu')).toHandle('mousemove');
|
||||
expect($('.subtitles-menu')).toHandle('mousewheel');
|
||||
expect($('.subtitles-menu')).toHandle('DOMMouseScroll');
|
||||
});
|
||||
|
||||
it('bind the scroll', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.subtitles'))
|
||||
expect($('.subtitles-menu'))
|
||||
.toHandleWith('scroll', state.videoControl.showControls);
|
||||
});
|
||||
|
||||
@@ -158,14 +164,61 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderCaptions', function() {
|
||||
|
||||
describe('is rendered', function() {
|
||||
var KEY = $.ui.keyCode,
|
||||
|
||||
keyPressEvent = function(key) {
|
||||
return $.Event('keydown', { keyCode: key });
|
||||
};
|
||||
|
||||
it('toggles the captions on control click', function() {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
$('.toggle-captions').click();
|
||||
expect($('.toggle-captions')).toHaveClass('is-active');
|
||||
expect($('.closed-captions')).toHaveClass('is-visible');
|
||||
|
||||
$('.toggle-captions').click();
|
||||
expect($('.toggle-captions')).not.toHaveClass('is-active');
|
||||
expect($('.closed-captions')).not.toHaveClass('is-visible');
|
||||
});
|
||||
|
||||
it('toggles the captions on keypress ENTER', function() {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
|
||||
expect($('.toggle-captions')).toHaveClass('is-active');
|
||||
expect($('.closed-captions')).toHaveClass('is-visible');
|
||||
|
||||
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
|
||||
expect($('.toggle-captions')).not.toHaveClass('is-active');
|
||||
expect($('.closed-captions')).not.toHaveClass('is-visible');
|
||||
});
|
||||
|
||||
it('toggles the captions on keypress SPACE', function() {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
|
||||
expect($('.toggle-captions')).toHaveClass('is-active');
|
||||
expect($('.closed-captions')).toHaveClass('is-visible');
|
||||
|
||||
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
|
||||
expect($('.toggle-captions')).not.toHaveClass('is-active');
|
||||
expect($('.closed-captions')).not.toHaveClass('is-visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderLanguageMenu', function () {
|
||||
|
||||
describe('is rendered', function () {
|
||||
var KEY = $.ui.keyCode,
|
||||
|
||||
keyPressEvent = function(key) {
|
||||
return $.Event('keydown', { keyCode: key });
|
||||
};
|
||||
keyPressEvent = function(key) {
|
||||
return $.Event('keydown', { keyCode: key });
|
||||
};
|
||||
|
||||
it('if languages more than 1', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
@@ -364,9 +417,8 @@
|
||||
});
|
||||
|
||||
it('show explanation message', function () {
|
||||
expect($('.subtitles li')).toHaveHtml(
|
||||
'Caption will be displayed when you start playing ' +
|
||||
'the video.'
|
||||
expect($('.subtitles-menu li')).toHaveHtml(
|
||||
'Transcript will be displayed when you start playing the video.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -444,7 +496,7 @@
|
||||
runs(function () {
|
||||
$(window).trigger(jQuery.Event('mousemove'));
|
||||
jasmine.Clock.tick(state.config.captionsFreezeTime);
|
||||
$('.subtitles').trigger(jQuery.Event('mouseenter'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mouseenter'));
|
||||
jasmine.Clock.tick(state.config.captionsFreezeTime);
|
||||
});
|
||||
});
|
||||
@@ -459,7 +511,7 @@
|
||||
describe('when the cursor is moving', function () {
|
||||
it('reset the freezing timeout', function () {
|
||||
runs(function () {
|
||||
$('.subtitles').trigger(jQuery.Event('mousemove'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mousemove'));
|
||||
expect(window.clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -468,7 +520,7 @@
|
||||
describe('when the mouse is scrolling', function () {
|
||||
it('reset the freezing timeout', function () {
|
||||
runs(function () {
|
||||
$('.subtitles').trigger(jQuery.Event('mousewheel'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mousewheel'));
|
||||
expect(window.clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -486,7 +538,7 @@
|
||||
|
||||
describe('always', function () {
|
||||
beforeEach(function () {
|
||||
$('.subtitles').trigger(jQuery.Event('mouseout'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
|
||||
});
|
||||
|
||||
it('reset the freezing timeout', function () {
|
||||
@@ -501,9 +553,9 @@
|
||||
describe('when the player is playing', function () {
|
||||
beforeEach(function () {
|
||||
state.videoCaption.playing = true;
|
||||
$('.subtitles li[data-index]:first')
|
||||
$('.subtitles-menu li[data-index]:first')
|
||||
.addClass('current');
|
||||
$('.subtitles').trigger(jQuery.Event('mouseout'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
|
||||
});
|
||||
|
||||
it('scroll the transcript', function () {
|
||||
@@ -514,7 +566,7 @@
|
||||
describe('when the player is not playing', function () {
|
||||
beforeEach(function () {
|
||||
state.videoCaption.playing = false;
|
||||
$('.subtitles').trigger(jQuery.Event('mouseout'));
|
||||
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
|
||||
});
|
||||
|
||||
it('does not scroll the transcript', function () {
|
||||
|
||||
@@ -92,7 +92,8 @@ function (VideoPlayer) {
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1,
|
||||
html5: 1
|
||||
html5: 1,
|
||||
cc_load_policy: 0
|
||||
},
|
||||
videoId: 'cogebirgzzM',
|
||||
events: events
|
||||
@@ -118,7 +119,8 @@ function (VideoPlayer) {
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1
|
||||
modestbranding: 1,
|
||||
cc_load_policy: 0
|
||||
},
|
||||
videoId: 'abcdefghijkl',
|
||||
events: jasmine.any(Object)
|
||||
|
||||
@@ -139,7 +139,8 @@ function (HTML5Video, Resizer) {
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1
|
||||
modestbranding: 1,
|
||||
cc_load_policy: 0
|
||||
};
|
||||
|
||||
if (!state.isFlashMode()) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
(function (define) {
|
||||
// VideoCaption module.
|
||||
// VideoCaption module.
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
'video/09_video_caption.js',
|
||||
['video/00_sjson.js', 'video/00_async_process.js'],
|
||||
function (Sjson, AsyncProcess) {
|
||||
|
||||
/**
|
||||
* @desc VideoCaption module exports a function.
|
||||
*
|
||||
@@ -29,11 +30,14 @@
|
||||
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
|
||||
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
|
||||
'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
|
||||
'previousLanguageMenuItem', 'nextLanguageMenuItem'
|
||||
'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
|
||||
'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
|
||||
'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoCaption = this;
|
||||
this.renderElements();
|
||||
this.handleCaptioningCookie();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
@@ -41,6 +45,14 @@
|
||||
VideoCaption.prototype = {
|
||||
langTemplate: [
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text">',
|
||||
gettext('Turn on closed captioning'),
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
@@ -66,12 +78,13 @@
|
||||
].join(''),
|
||||
|
||||
template: [
|
||||
'<ol id="transcript-captions" class="subtitles" aria-label="',
|
||||
'<div class="subtitles" role="region" aria-label="',
|
||||
/* jshint maxlen:200 */
|
||||
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
|
||||
'">',
|
||||
'<li></li>',
|
||||
'</ol>'
|
||||
'<ol id="transcript-captions" class="subtitles-menu">',
|
||||
'</ol>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
@@ -106,7 +119,10 @@
|
||||
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = $(this.template);
|
||||
this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
|
||||
this.container = $(this.langTemplate);
|
||||
this.captionControlEl = this.container.find('.toggle-captions');
|
||||
this.captionDisplayEl = this.state.el.find('.closed-captions');
|
||||
this.transcriptControlEl = this.container.find('.toggle-transcript');
|
||||
this.languageChooserEl = this.container.find('.lang');
|
||||
this.menuChooserEl = this.languageChooserEl.parent();
|
||||
@@ -129,16 +145,26 @@
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
this.transcriptControlEl.on('click', this.toggle);
|
||||
this.subtitlesEl
|
||||
.on({
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave,
|
||||
mousemove: this.onMovement,
|
||||
mousewheel: this.onMovement,
|
||||
DOMMouseScroll: this.onMovement
|
||||
})
|
||||
.on(events, 'li[data-index]', this.onCaptionHandler);
|
||||
this.captionControlEl.on({
|
||||
click: this.toggleClosedCaptions,
|
||||
keydown: this.handleCaptionToggle
|
||||
});
|
||||
this.transcriptControlEl.on({
|
||||
click: this.toggle,
|
||||
keydown: this.handleTranscriptToggle
|
||||
});
|
||||
this.subtitlesMenuEl.on({
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave,
|
||||
mousemove: this.onMovement,
|
||||
mousewheel: this.onMovement,
|
||||
DOMMouseScroll: this.onMovement
|
||||
})
|
||||
.on(events, 'li[data-index]', this.onCaptionHandler);
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
|
||||
if (this.showLanguageMenu) {
|
||||
this.languageChooserEl.on({
|
||||
@@ -148,11 +174,6 @@
|
||||
this.languageChooserEl.on({
|
||||
keydown: this.handleKeypressLink
|
||||
}, '.control-lang');
|
||||
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
}
|
||||
|
||||
state.el
|
||||
@@ -168,7 +189,7 @@
|
||||
});
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
this.subtitlesEl.on('scroll', state.videoControl.showControls);
|
||||
this.subtitlesMenuEl.on('scroll', state.videoControl.showControls);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -176,6 +197,30 @@
|
||||
this.updatePlayTime(time);
|
||||
},
|
||||
|
||||
handleCaptionToggle: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch(keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggleClosedCaptions(event);
|
||||
}
|
||||
},
|
||||
|
||||
handleTranscriptToggle: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch(keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
},
|
||||
|
||||
handleKeypressLink: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode,
|
||||
@@ -188,7 +233,7 @@
|
||||
index = this.languageChooserEl.find('li').index(focused);
|
||||
total = this.languageChooserEl.find('li').size() - 1;
|
||||
|
||||
this.previousLanguageMenuItem(event, index, total);
|
||||
this.previousLanguageMenuItem(event, index);
|
||||
break;
|
||||
|
||||
case KEY.DOWN:
|
||||
@@ -241,13 +286,13 @@
|
||||
if (index === total) {
|
||||
this.languageChooserEl
|
||||
.find('.control-lang').first()
|
||||
.focus();
|
||||
.focus();
|
||||
} else {
|
||||
this.languageChooserEl
|
||||
.find('li:eq(' + index + ')')
|
||||
.next()
|
||||
.find('.control-lang')
|
||||
.focus();
|
||||
.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -256,11 +301,7 @@
|
||||
previousLanguageMenuItem: function(event, index) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (event.altKey || event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -287,11 +328,13 @@
|
||||
|
||||
|
||||
this.state.el.trigger('language_menu:show');
|
||||
|
||||
button
|
||||
.addClass('is-opened');
|
||||
|
||||
menu
|
||||
.find('.control-lang').last()
|
||||
.focus();
|
||||
.focus();
|
||||
},
|
||||
|
||||
closeLanguageMenu: function(event) {
|
||||
@@ -300,6 +343,7 @@
|
||||
var button = this.languageChooserEl;
|
||||
|
||||
this.state.el.trigger('language_menu:hide');
|
||||
|
||||
button
|
||||
.removeClass('is-opened')
|
||||
.find('.language-menu')
|
||||
@@ -473,7 +517,7 @@
|
||||
|
||||
state.el.removeClass('is-captions-rendered');
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
// occurred, then we hide the captions panel, and the "Transcript" button
|
||||
this.fetchXHR = $.ajaxWithPrefix({
|
||||
url: url,
|
||||
notifyOnError: false,
|
||||
@@ -491,10 +535,10 @@
|
||||
}
|
||||
} else {
|
||||
if (state.isTouch) {
|
||||
self.subtitlesEl.find('li').html(
|
||||
self.subtitlesEl.find('.subtitles-menu').html(
|
||||
gettext(
|
||||
'Caption will be displayed when ' +
|
||||
'you start playing the video.'
|
||||
'<li>Transcript will be displayed when ' +
|
||||
'you start playing the video.</li>'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
@@ -706,9 +750,9 @@
|
||||
};
|
||||
|
||||
this.rendered = false;
|
||||
this.subtitlesEl.empty();
|
||||
this.subtitlesMenuEl.empty();
|
||||
this.setSubtitlesHeight();
|
||||
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
|
||||
this.buildCaptions(this.subtitlesMenuEl, start, captions).done(onRender);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -718,7 +762,7 @@
|
||||
*/
|
||||
addPaddings: function () {
|
||||
|
||||
this.subtitlesEl
|
||||
this.subtitlesMenuEl
|
||||
.prepend(
|
||||
$('<li class="spacing">')
|
||||
.height(this.topSpacingHeight())
|
||||
@@ -936,6 +980,7 @@
|
||||
.addClass('current');
|
||||
|
||||
this.currentIndex = newIndex;
|
||||
this.captionDisplayEl.text(this.subtitlesEl.find("li[data-index='" + newIndex + "']").text());
|
||||
this.scrollCaption();
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1062,82 @@
|
||||
}
|
||||
},
|
||||
|
||||
handleCaptioningCookie: function() {
|
||||
if ($.cookie('show_closed_captions') === 'true') {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.showClosedCaptions();
|
||||
|
||||
// keep it going until turned off
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
this.hideClosedCaptions();
|
||||
}
|
||||
},
|
||||
|
||||
toggleClosedCaptions: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.el.hasClass('has-captions')) {
|
||||
this.state.showClosedCaptions = false;
|
||||
this.updateCaptioningCookie(false);
|
||||
this.hideClosedCaptions();
|
||||
} else {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.updateCaptioningCookie(true);
|
||||
this.showClosedCaptions();
|
||||
}
|
||||
},
|
||||
|
||||
showClosedCaptions: function() {
|
||||
this.state.el.addClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.show()
|
||||
.addClass('is-visible');
|
||||
|
||||
this.captionControlEl
|
||||
.addClass('is-active')
|
||||
.find('.control-text')
|
||||
.text(gettext('Hide closed captions'));
|
||||
|
||||
if (this.subtitlesEl.find('.current').text()) {
|
||||
this.captionDisplayEl
|
||||
.text(this.subtitlesEl.find('.current').text());
|
||||
} else {
|
||||
this.captionDisplayEl
|
||||
.text(gettext('(Caption will be displayed when you start playing the video.)'));
|
||||
}
|
||||
},
|
||||
|
||||
hideClosedCaptions: function() {
|
||||
this.state.el.removeClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.hide()
|
||||
.removeClass('is-visible');
|
||||
|
||||
this.captionControlEl
|
||||
.removeClass('is-active')
|
||||
.find('.control-text')
|
||||
.text(gettext('Turn on closed captioning'));
|
||||
},
|
||||
|
||||
updateCaptioningCookie: function(method) {
|
||||
if (method) {
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
$.cookie('show_closed_captions', null, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides captions and updates the cookie.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ log = logging.getLogger('VideoPage')
|
||||
VIDEO_BUTTONS = {
|
||||
'transcript': '.lang',
|
||||
'transcript_button': '.toggle-transcript',
|
||||
'cc_button': '.toggle-captions',
|
||||
'volume': '.volume',
|
||||
'play': '.video_control.play',
|
||||
'pause': '.video_control.pause',
|
||||
@@ -28,10 +29,12 @@ VIDEO_BUTTONS = {
|
||||
}
|
||||
|
||||
CSS_CLASS_NAMES = {
|
||||
'closed_captions': '.video.closed',
|
||||
'captions_closed': '.video.closed',
|
||||
'captions_rendered': '.video.is-captions-rendered',
|
||||
'captions': '.subtitles',
|
||||
'captions_text': '.subtitles > li',
|
||||
'captions_text': '.subtitles li',
|
||||
'captions_text_getter': '.subtitles li[role="link"][data-index="1"]',
|
||||
'closed_captions': '.closed-captions',
|
||||
'error_message': '.video .video-player h3',
|
||||
'video_container': '.video',
|
||||
'video_sources': '.video-player video source',
|
||||
@@ -293,6 +296,18 @@ class VideoPage(PageObject):
|
||||
"""
|
||||
self._captions_visibility(False)
|
||||
|
||||
def show_closed_captions(self):
|
||||
"""
|
||||
Make closed captions visible.
|
||||
"""
|
||||
self._closed_captions_visibility(True)
|
||||
|
||||
def hide_closed_captions(self):
|
||||
"""
|
||||
Make closed captions invisible.
|
||||
"""
|
||||
self._closed_captions_visibility(False)
|
||||
|
||||
def is_captions_visible(self):
|
||||
"""
|
||||
Get current visibility sate of captions.
|
||||
@@ -302,8 +317,20 @@ class VideoPage(PageObject):
|
||||
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
return not self.q(css=caption_state_selector).present
|
||||
caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['captions'])
|
||||
return self.q(css=caption_state_selector).visible
|
||||
|
||||
def is_closed_captions_visible(self):
|
||||
"""
|
||||
Get current visibility sate of closed captions.
|
||||
|
||||
Returns:
|
||||
bool: True means captions are visible, False means captions are not visible
|
||||
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
closed_caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
return self.q(css=closed_caption_state_selector).visible
|
||||
|
||||
@wait_for_js
|
||||
def _captions_visibility(self, captions_new_state):
|
||||
@@ -327,7 +354,24 @@ class VideoPage(PageObject):
|
||||
|
||||
# Verify that captions state is toggled/changed
|
||||
EmptyPromise(lambda: self.is_captions_visible() == captions_new_state,
|
||||
"Captions are {state}".format(state=state)).fulfill()
|
||||
"Transcripts are {state}".format(state=state)).fulfill()
|
||||
|
||||
@wait_for_js
|
||||
def _closed_captions_visibility(self, closed_captions_new_state):
|
||||
"""
|
||||
Set the video closed captioning visibility state.
|
||||
|
||||
Arguments:
|
||||
closed_captions_new_state (bool): True means show closed captioning
|
||||
"""
|
||||
states = {True: 'shown', False: 'hidden'}
|
||||
state = states[closed_captions_new_state]
|
||||
|
||||
self.click_player_button('cc_button')
|
||||
|
||||
# Make sure that the captions are visible
|
||||
EmptyPromise(lambda: self.is_closed_captions_visible() == closed_captions_new_state,
|
||||
"Closed captions are {state}".format(state=state)).fulfill()
|
||||
|
||||
@property
|
||||
def captions_text(self):
|
||||
@@ -345,6 +389,31 @@ class VideoPage(PageObject):
|
||||
|
||||
return ' '.join(subs)
|
||||
|
||||
@property
|
||||
def closed_captions_text(self):
|
||||
"""
|
||||
Extract closed captioning text.
|
||||
|
||||
Returns:
|
||||
str: closed captions Text.
|
||||
|
||||
"""
|
||||
self.wait_for_closed_captions()
|
||||
|
||||
closed_captions_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
subs = self.q(css=closed_captions_selector).html
|
||||
|
||||
return ' '.join(subs)
|
||||
|
||||
def click_first_line_in_transcript(self):
|
||||
"""
|
||||
Clicks a line in the transcript updating the current caption.
|
||||
"""
|
||||
|
||||
self.wait_for_captions()
|
||||
captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'])
|
||||
captions_selector.click()
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""
|
||||
@@ -494,6 +563,12 @@ class VideoPage(PageObject):
|
||||
response = requests.get(url, **kwargs)
|
||||
return response.status_code < 400, response.headers, response.content
|
||||
|
||||
def get_cookie(self, cookie_name):
|
||||
"""
|
||||
Searches for and returns `cookie_name`
|
||||
"""
|
||||
return self.browser.get_cookie(cookie_name)
|
||||
|
||||
def downloaded_transcript_contains_text(self, transcript_format, text_to_search):
|
||||
"""
|
||||
Download the transcript in format `transcript_format` and check that it contains the text `text_to_search`
|
||||
@@ -837,6 +912,20 @@ class VideoPage(PageObject):
|
||||
captions_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_rendered'])
|
||||
self.wait_for_element_presence(captions_rendered_selector, 'Captions Rendered')
|
||||
|
||||
def wait_for_closed_captions(self):
|
||||
"""
|
||||
Wait until closed captions are rendered completely.
|
||||
"""
|
||||
cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
self.wait_for_element_visibility(cc_rendered_selector, 'Closed captions rendered')
|
||||
|
||||
def wait_for_closed_captions_to_be_hidden(self):
|
||||
"""
|
||||
Waits for the closed captions to be turned off completely.
|
||||
"""
|
||||
cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
self.wait_for_element_invisibility(cc_rendered_selector, 'Closed captions hidden')
|
||||
|
||||
|
||||
def _parse_time_str(time_str):
|
||||
"""
|
||||
|
||||
@@ -13,11 +13,11 @@ from selenium.webdriver.common.keys import Keys
|
||||
|
||||
|
||||
CLASS_SELECTORS = {
|
||||
'video_container': 'div.video',
|
||||
'video_container': '.video',
|
||||
'video_init': '.is-initialized',
|
||||
'video_xmodule': '.xmodule_VideoModule',
|
||||
'video_spinner': '.video-wrapper .spinner',
|
||||
'video_controls': 'section.video-controls',
|
||||
'video_controls': '.video-controls',
|
||||
'attach_asset': '.upload-dialog > input[type="file"]',
|
||||
'upload_dialog': '.wrapper-modal-window-assetupload',
|
||||
'xblock': '.add-xblock-component',
|
||||
@@ -264,7 +264,7 @@ class VideoComponentPage(VideoPage):
|
||||
line_number (int): caption line number
|
||||
|
||||
"""
|
||||
caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
|
||||
caption_line_selector = ".subtitles li[data-index='{index}']".format(index=line_number - 1)
|
||||
self.q(css=caption_line_selector).results[0].send_keys(Keys.ENTER)
|
||||
|
||||
def is_caption_line_focused(self, line_number):
|
||||
@@ -275,7 +275,7 @@ class VideoComponentPage(VideoPage):
|
||||
line_number (int): caption line number
|
||||
|
||||
"""
|
||||
caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
|
||||
caption_line_selector = ".subtitles li[data-index='{index}']".format(index=line_number - 1)
|
||||
attributes = self.q(css=caption_line_selector).attrs('class')
|
||||
|
||||
return 'focused' in attributes
|
||||
@@ -504,7 +504,7 @@ class VideoComponentPage(VideoPage):
|
||||
As all the captions lines are exactly same so only getting partial lines will work.
|
||||
"""
|
||||
self.wait_for_captions()
|
||||
selector = '.subtitles > li:nth-child({})'
|
||||
selector = '.subtitles li:nth-child({})'
|
||||
return ' '.join([self.q(css=selector.format(i)).text[0] for i in range(1, 6)])
|
||||
|
||||
def set_url_field(self, url, field_number):
|
||||
|
||||
@@ -142,7 +142,7 @@ class VideoEditorTest(CMSVideoBaseTest):
|
||||
self.open_advanced_tab()
|
||||
self.video.upload_translation('1mb_transcripts.srt', 'uk')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
self.video.wait_for(self.video.is_captions_visible, 'Captions are visible', timeout=10)
|
||||
unicode_text = "Привіт, edX вітає вас.".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_lines())
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@ class CMSVideoTest(CMSVideoBaseTest):
|
||||
Then when I view the video it does show the captions
|
||||
"""
|
||||
self._create_course_unit(subtitles=True)
|
||||
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
def test_captions_toggling(self):
|
||||
|
||||
@@ -212,9 +212,9 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
# Verify that video has rendered in "Youtube" mode
|
||||
self.assertTrue(self.video.is_video_rendered('youtube'))
|
||||
|
||||
def test_cc_button_wo_english_transcript(self):
|
||||
def test_transcript_button_wo_english_transcript(self):
|
||||
"""
|
||||
Scenario: CC button works correctly w/o english transcript in Youtube mode
|
||||
Scenario: Transcript button works correctly w/o english transcript in Youtube mode
|
||||
Given the course has a Video component in "Youtube" mode
|
||||
And I have defined a non-english transcript for the video
|
||||
And I have uploaded a non-english transcript file to assets
|
||||
@@ -226,13 +226,38 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.navigate_to_video()
|
||||
self.video.show_captions()
|
||||
|
||||
# Verify that we see "好 各位同学" text in the captions
|
||||
# Verify that we see "好 各位同学" text in the transcript
|
||||
unicode_text = "好 各位同学".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_text)
|
||||
|
||||
def test_cc_button_transcripts_and_sub_fields_empty(self):
|
||||
def test_cc_button(self):
|
||||
"""
|
||||
Scenario: CC button works correctly if transcripts and sub fields are empty,
|
||||
Scenario: CC button works correctly with transcript in YouTube mode
|
||||
Given the course has a video component in "Youtube" mode
|
||||
And I have defined a transcript for the video
|
||||
Then I see the closed captioning element over the video
|
||||
"""
|
||||
data = {'transcripts': {'zh': 'chinese_transcripts.srt'}}
|
||||
self.metadata = self.metadata_for_mode('youtube', data)
|
||||
self.assets.append('chinese_transcripts.srt')
|
||||
self.navigate_to_video()
|
||||
|
||||
# Show captions and make sure they're visible and cookie is set
|
||||
self.video.show_closed_captions()
|
||||
self.video.wait_for_closed_captions()
|
||||
self.assertTrue(self.video.is_closed_captions_visible)
|
||||
self.video.reload_page()
|
||||
self.assertTrue(self.video.is_closed_captions_visible)
|
||||
|
||||
# Hide captions and make sure they're hidden and cookie is unset
|
||||
self.video.hide_closed_captions()
|
||||
self.video.wait_for_closed_captions_to_be_hidden()
|
||||
self.video.reload_page()
|
||||
self.video.wait_for_closed_captions_to_be_hidden()
|
||||
|
||||
def test_transcript_button_transcripts_and_sub_fields_empty(self):
|
||||
"""
|
||||
Scenario: Transcript button works correctly if transcripts and sub fields are empty,
|
||||
but transcript file exists in assets (Youtube mode of Video component)
|
||||
Given the course has a Video component in "Youtube" mode
|
||||
And I have uploaded a .srt.sjson file to assets
|
||||
@@ -247,11 +272,11 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
# Verify that we see "Welcome to edX." text in the captions
|
||||
self.assertIn('Welcome to edX.', self.video.captions_text)
|
||||
|
||||
def test_cc_button_hidden_no_translations(self):
|
||||
def test_transcript_button_hidden_no_translations(self):
|
||||
"""
|
||||
Scenario: CC button is hidden if no translations
|
||||
Scenario: Transcript button is hidden if no translations
|
||||
Given the course has a Video component in "Youtube" mode
|
||||
Then the "CC" button is hidden
|
||||
Then the "Transcript" button is hidden
|
||||
"""
|
||||
self.navigate_to_video()
|
||||
self.assertFalse(self.video.is_button_shown('transcript_button'))
|
||||
@@ -522,6 +547,16 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
timeout=5
|
||||
)
|
||||
|
||||
def _verify_closed_caption_text(self, text):
|
||||
"""
|
||||
Scenario: returns True if the captions are visible, False is else
|
||||
"""
|
||||
self.video.wait_for(
|
||||
lambda: (text in self.video.closed_captions_text),
|
||||
u'Closed captions contain "{}" text'.format(text),
|
||||
timeout=5
|
||||
)
|
||||
|
||||
def test_video_language_menu_working(self):
|
||||
"""
|
||||
Scenario: Language menu works correctly in Video component
|
||||
@@ -554,6 +589,43 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.video.select_language('en')
|
||||
self._verify_caption_text('Welcome to edX.')
|
||||
|
||||
def test_video_language_menu_working_closed_captions(self):
|
||||
"""
|
||||
Scenario: Language menu works correctly in Video component, checks closed captions
|
||||
Given the course has a Video component in "Youtube" mode
|
||||
And I have defined multiple language transcripts for the videos
|
||||
And I make sure captions are closed
|
||||
And I see video menu "language" with correct items
|
||||
And I select language with code "en"
|
||||
Then I see "Welcome to edX." text in the closed captions
|
||||
And I select language with code "zh"
|
||||
Then I see "我们今天要讲的题目是" text in the closed captions
|
||||
"""
|
||||
self.assets.extend(['chinese_transcripts.srt', 'subs_3_yD_cEKoCk.srt.sjson'])
|
||||
data = {'transcripts': {"zh": "chinese_transcripts.srt"}, 'sub': '3_yD_cEKoCk'}
|
||||
self.metadata = self.metadata_for_mode('youtube', additional_data=data)
|
||||
|
||||
# go to video
|
||||
self.navigate_to_video()
|
||||
self.video.show_closed_captions()
|
||||
|
||||
correct_languages = {'en': 'English', 'zh': 'Chinese'}
|
||||
self.assertEqual(self.video.caption_languages, correct_languages)
|
||||
|
||||
# we start the video, then pause it to activate the transcript
|
||||
self.video.click_player_button('play')
|
||||
self.video.wait_for_position('0:01')
|
||||
self.video.click_player_button('pause')
|
||||
|
||||
self.video.select_language('en')
|
||||
self.video.click_first_line_in_transcript()
|
||||
self._verify_closed_caption_text('Welcome to edX.')
|
||||
|
||||
self.video.select_language('zh')
|
||||
unicode_text = "我们今天要讲的题目是".decode('utf-8')
|
||||
self.video.click_first_line_in_transcript()
|
||||
self._verify_closed_caption_text(unicode_text)
|
||||
|
||||
def test_multiple_videos_in_sequentials_load_and_work(self):
|
||||
"""
|
||||
Scenario: Multiple videos in sequentials all load and work, switching between sequentials
|
||||
|
||||
@@ -1257,7 +1257,7 @@ main_vendor_js = base_vendor_js + [
|
||||
'js/vendor/jquery.ba-bbq.min.js',
|
||||
'js/vendor/afontgarde/modernizr.fontface-generatedcontent.js',
|
||||
'js/vendor/afontgarde/afontgarde.js',
|
||||
'js/vendor/afontgarde/edx-icons.js',
|
||||
'js/vendor/afontgarde/edx-icons.js'
|
||||
]
|
||||
|
||||
# Common files used by both RequireJS code and non-RequireJS code
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<h3 class="hidden">${_('No playable video sources found.')}</h3>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<div class="closed-captions"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
|
||||
Reference in New Issue
Block a user