Commit 08538e89 by Anton Stupak

Merge pull request #3128 from edx/anton/transcript-generation

Update captions logic on front-end.
parents b1a0cc9d 92cd4627
......@@ -201,7 +201,9 @@ def upload_file(_step, file_name):
@step('I see "([^"]*)" text in the captions')
def check_text_in_the_captions(_step, text):
assert world.browser.is_text_present(text.strip(), 5)
world.wait_for(lambda _: world.css_text('.subtitles'))
actual_text = world.css_text('.subtitles')
assert (text in actual_text)
@step('I see value "([^"]*)" in the field "([^"]*)"$')
......
(function (require) {
require(
['video/00_async_process.js'],
function (AsyncProcess) {
var getArrayNthLength = function (n, multiplier) {
var result = [],
mul = multiplier || 1;
for (var i = 0; i < n; i++) {
result[i] = i * mul;
}
return result;
},
items = getArrayNthLength(1000);
describe('AsyncProcess', function () {
it ('Array is processed successfully', function () {
var processedArray,
expectedArray = getArrayNthLength(1000, 2),
process = function (item) {
return 2 * item;
};
runs(function () {
AsyncProcess.array(items, process).done(function (result) {
processedArray = result;
});
});
waitsFor(function () {
return processedArray;
}, 'Array processing takes too much time', WAIT_TIMEOUT);
runs(function () {
expect(processedArray).toEqual(expectedArray);
});
});
it ('If non-array is passed, error callback is called', function () {
var isError,
process = function () {};
runs(function () {
AsyncProcess.array('string', process).fail(function () {
isError = true;
});
});
waitsFor(function () {
return isError;
}, 'Error callback wasn\'t called', WAIT_TIMEOUT);
runs(function () {
expect(isError).toBeTruthy();
});
});
it ('If an empty array is passed, returns initial array', function () {
var processedArray,
process = function () {};
runs(function () {
AsyncProcess.array([], process).done(function (result) {
processedArray = result;
});
});
waitsFor(function () {
return processedArray;
}, 'Array processing takes too much time', WAIT_TIMEOUT);
runs(function () {
expect(processedArray).toEqual([]);
});
});
it ('If no process function passed, returns initial array', function () {
var processedArray;
runs(function () {
AsyncProcess.array(items).done(function (result) {
processedArray = result;
});
});
waitsFor(function () {
return processedArray;
}, 'Array processing takes too much time', WAIT_TIMEOUT);
runs(function () {
expect(processedArray).toEqual(items);
});
});
});
});
}(RequireJS.require));
(function (require) {
require(
['video/00_sjson.js'],
function (Sjson) {
describe('Sjson', function () {
var data = jasmine.stubbedCaption,
sjson;
beforeEach(function() {
sjson = new Sjson(data);
});
it ('returns captions', function () {
expect(sjson.getCaptions()).toEqual(data.text);
});
it ('returns start times', function () {
expect(sjson.getStartTimes()).toEqual(data.start);
});
it ('returns correct length', function () {
expect(sjson.getSize()).toEqual(data.text.length);
});
it('search returns a correct caption index', function () {
expect(sjson.search(0)).toEqual(-1);
expect(sjson.search(3120)).toEqual(1);
expect(sjson.search(6270)).toEqual(2);
expect(sjson.search(8490)).toEqual(2);
expect(sjson.search(21620)).toEqual(4);
expect(sjson.search(24920)).toEqual(5);
});
});
});
}(RequireJS.require));
......@@ -92,7 +92,7 @@
});
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({
videoId: 'abcdefghijkl'
videoId: 'cogebirgzzM'
});
});
});
......@@ -237,10 +237,17 @@
describe('when on a non touch-based device', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
it('render the caption', function () {
runs(function () {
var captionsData;
captionsData = jasmine.stubbedCaption;
......@@ -255,16 +262,20 @@
expect($(link)).toHaveText(captionsData.text[index]);
});
});
});
it('add a padding element to caption', function () {
runs(function () {
expect($('.subtitles li:first').hasClass('spacing'))
.toBe(true);
expect($('.subtitles li:last').hasClass('spacing'))
.toBe(true);
});
});
it('bind all the caption link', function () {
runs(function () {
var handlerList = ['captionMouseOverOut', 'captionClick',
'captionMouseDown', 'captionFocus', 'captionBlur',
'captionKeyDown'
......@@ -300,12 +311,22 @@
expect(state.videoCaption.captionKeyDown).toHaveBeenCalled();
});
});
});
it('set rendered to true', function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
runs(function () {
expect(state.videoCaption.rendered).toBeTruthy();
});
});
});
describe('when on a touch-based device', function () {
beforeEach(function () {
......@@ -345,52 +366,61 @@
beforeEach(function () {
jasmine.Clock.useMock();
spyOn(window, 'clearTimeout');
runs(function () {
state = jasmine.initializePlayer();
jasmine.Clock.tick(50);
});
describe('when cursor is outside of the caption box', function () {
beforeEach(function () {
$(window).trigger(jQuery.Event('mousemove'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
describe('when cursor is outside of the caption box', function () {
it('does not set freezing timeout', function () {
runs(function () {
expect(state.videoCaption.frozen).toBeFalsy();
});
});
});
describe('when cursor is in the caption box', function () {
beforeEach(function () {
spyOn(state.videoCaption, 'onMouseLeave');
runs(function () {
$(window).trigger(jQuery.Event('mousemove'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
$('.subtitles').trigger(jQuery.Event('mouseenter'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
});
});
it('set the freezing timeout', function () {
runs(function () {
expect(state.videoCaption.frozen).not.toBeFalsy();
expect(state.videoCaption.onMouseLeave).toHaveBeenCalled();
});
describe('when the cursor is moving', function () {
beforeEach(function () {
$('.subtitles').trigger(jQuery.Event('mousemove'));
});
describe('when the cursor is moving', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousemove'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
describe('when the mouse is scrolling', function () {
beforeEach(function () {
$('.subtitles').trigger(jQuery.Event('mousewheel'));
});
describe('when the mouse is scrolling', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousewheel'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
});
});
describe(
'when cursor is moving out of the caption box',
......@@ -441,25 +471,6 @@
});
});
it('reRenderCaption', function () {
state = jasmine.initializePlayer();
var Caption = state.videoCaption,
li;
Caption.captions = ['test'];
Caption.start = [500];
spyOn(Caption, 'addPaddings');
Caption.reRenderCaption();
li = $('ol.subtitles li');
expect(Caption.addPaddings).toHaveBeenCalled();
expect(li.length).toBe(1);
expect(li).toHaveData('start', '500');
});
describe('fetchCaption', function () {
var Caption, msg;
......@@ -467,7 +478,6 @@
state = jasmine.initializePlayer();
Caption = state.videoCaption;
spyOn($, 'ajaxWithPrefix').andCallThrough();
spyOn(Caption, 'reRenderCaption');
spyOn(Caption, 'renderCaption');
spyOn(Caption, 'bindHandlers');
spyOn(Caption, 'updatePlayTime');
......@@ -511,7 +521,6 @@
expect(Caption.bindHandlers).toHaveBeenCalled();
expect(Caption.renderCaption).not.toHaveBeenCalled();
expect(Caption.updatePlayTime).not.toHaveBeenCalled();
expect(Caption.reRenderCaption).not.toHaveBeenCalled();
expect(Caption.loaded).toBeTruthy();
});
......@@ -527,7 +536,6 @@
expect(Caption.bindHandlers).not.toHaveBeenCalled();
expect(Caption.renderCaption).not.toHaveBeenCalled();
expect(Caption.updatePlayTime).not.toHaveBeenCalled();
expect(Caption.reRenderCaption).not.toHaveBeenCalled();
expect(Caption.loaded).toBeTruthy();
});
......@@ -539,9 +547,8 @@
expect($.ajaxWithPrefix).toHaveBeenCalled();
expect(Caption.bindHandlers).not.toHaveBeenCalled();
expect(Caption.renderCaption).not.toHaveBeenCalled();
expect(Caption.renderCaption).toHaveBeenCalled();
expect(Caption.updatePlayTime).toHaveBeenCalled();
expect(Caption.reRenderCaption).toHaveBeenCalled();
expect(Caption.loaded).toBeTruthy();
});
......@@ -553,19 +560,18 @@
expect(Caption.bindHandlers).toHaveBeenCalled();
expect(Caption.renderCaption).toHaveBeenCalled();
expect(Caption.updatePlayTime).not.toHaveBeenCalled();
expect(Caption.reRenderCaption).not.toHaveBeenCalled();
expect(Caption.loaded).toBeTruthy();
});
it('on success: re-rendered correct', function () {
Caption.loaded = true;
Caption.rendered = true;
Caption.fetchCaption();
expect($.ajaxWithPrefix).toHaveBeenCalled();
expect(Caption.bindHandlers).not.toHaveBeenCalled();
expect(Caption.renderCaption).not.toHaveBeenCalled();
expect(Caption.renderCaption).toHaveBeenCalled();
expect(Caption.updatePlayTime).toHaveBeenCalled();
expect(Caption.reRenderCaption).toHaveBeenCalled();
expect(Caption.loaded).toBeTruthy();
});
......@@ -680,27 +686,23 @@
});
});
describe('search', function () {
it('return a correct caption index', function () {
state = jasmine.initializePlayer();
expect(state.videoCaption.search(0)).toEqual(-1);
expect(state.videoCaption.search(3120)).toEqual(1);
expect(state.videoCaption.search(6270)).toEqual(2);
expect(state.videoCaption.search(8490)).toEqual(2);
expect(state.videoCaption.search(21620)).toEqual(4);
expect(state.videoCaption.search(24920)).toEqual(5);
});
});
describe('play', function () {
describe('when the caption was not rendered', function () {
beforeEach(function () {
window.onTouchBasedDevice.andReturn(['iPad']);
runs(function () {
state = jasmine.initializePlayer();
state.videoCaption.play();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
it('render the caption', function () {
runs(function () {
var captionsData;
captionsData = jasmine.stubbedCaption;
......@@ -716,20 +718,28 @@
});
});
});
it('add a padding element to caption', function () {
runs(function () {
expect($('.subtitles li:first')).toBe('.spacing');
expect($('.subtitles li:last')).toBe('.spacing');
});
});
it('set rendered to true', function () {
runs(function () {
expect(state.videoCaption.rendered).toBeTruthy();
});
});
it('set playing to true', function () {
runs(function () {
expect(state.videoCaption.playing).toBeTruthy();
});
});
});
});
describe('pause', function () {
beforeEach(function () {
......@@ -745,82 +755,117 @@
describe('updatePlayTime', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
describe('when the video speed is 1.0x', function () {
beforeEach(function () {
state.videoSpeedControl.currentSpeed = '1.0';
state.videoCaption.updatePlayTime(25.000);
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
describe('when the video speed is 1.0x', function () {
it('search the caption based on time', function () {
runs(function () {
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(5);
});
});
describe('when the video speed is not 1.0x', function () {
beforeEach(function () {
state.videoSpeedControl.currentSpeed = '0.75';
// Flash mode
spyOn(state, 'isFlashMode').andReturn(true);
state.speed = '1.0';
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(5);
});
});
});
describe('when the video speed is not 1.0x', function () {
it('search the caption based on 1.0x speed', function () {
runs(function () {
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(5);
// Flash mode
state.speed = '2.0';
spyOn(state, 'isFlashMode').andReturn(true);
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(9);
state.speed = '0.75';
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(3);
});
});
});
describe('when the index is not the same', function () {
beforeEach(function () {
runs(function () {
state.videoCaption.currentIndex = 1;
$('.subtitles li[data-index=5]').addClass('current');
state.videoCaption.updatePlayTime(25.000);
});
});
it('deactivate the previous caption', function () {
runs(function () {
expect($('.subtitles li[data-index=1]'))
.not.toHaveClass('current');
});
});
it('activate new caption', function () {
runs(function () {
expect($('.subtitles li[data-index=5]'))
.toHaveClass('current');
});
});
it('save new index', function () {
runs(function () {
expect(state.videoCaption.currentIndex).toEqual(5);
});
});
// Disabled 11/25/13 due to flakiness in master
xit('scroll caption to new position', function () {
it('scroll caption to new position', function () {
runs(function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
});
describe('when the index is the same', function () {
beforeEach(function () {
it('does not change current subtitle', function () {
runs(function () {
state.videoCaption.currentIndex = 1;
$('.subtitles li[data-index=3]').addClass('current');
state.videoCaption.updatePlayTime(15.000);
});
it('does not change current subtitle', function () {
expect($('.subtitles li[data-index=3]'))
.toHaveClass('current');
});
});
});
});
describe('resize', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
runs(function () {
videoControl = state.videoControl;
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.resize();
});
});
describe('set the height of caption container', function () {
it('when CC button is enabled', function () {
runs(function () {
var realHeight = parseInt(
$('.subtitles').css('maxHeight'), 10
),
......@@ -830,8 +875,10 @@
// environments: Linux * Mac * FF * Chrome
expect(realHeight).toBeCloseTo(shouldBeHeight, 2);
});
});
it('when CC button is disabled ', function () {
runs(function () {
var realHeight, videoWrapperHeight, progressSliderHeight,
controlHeight, shouldBeHeight;
......@@ -851,8 +898,10 @@
expect(realHeight).toBe(shouldBeHeight);
});
});
});
it('set the height of caption spacing', function () {
runs(function () {
var firstSpacing, lastSpacing;
firstSpacing = Math.abs(parseInt(
......@@ -867,100 +916,111 @@
expect(lastSpacing - state.videoCaption.bottomSpacingHeight())
.toBeLessThan(1);
});
});
it('scroll caption to new position', function () {
runs(function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
});
// Disabled 11/25/13 due to flakiness in master
xdescribe('scrollCaption', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
describe('when frozen', function () {
beforeEach(function () {
it('does not scroll the caption', function () {
runs(function () {
state.videoCaption.frozen = true;
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.scrollCaption();
});
it('does not scroll the caption', function () {
expect($.fn.scrollTo).not.toHaveBeenCalled();
});
});
});
describe('when not frozen', function () {
beforeEach(function () {
runs(function () {
state.videoCaption.frozen = false;
});
describe('when there is no current caption', function () {
beforeEach(function () {
state.videoCaption.scrollCaption();
});
describe('when there is no current caption', function () {
it('does not scroll the caption', function () {
runs(function () {
state.videoCaption.scrollCaption();
expect($.fn.scrollTo).not.toHaveBeenCalled();
});
});
});
describe('when there is a current caption', function () {
beforeEach(function () {
it('scroll to current caption', function () {
runs(function () {
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.scrollCaption();
});
it('scroll to current caption', function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
});
});
});
// Disabled 10/9/13 due to flakiness in master
xdescribe('seekPlayer', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
describe('when the video speed is 1.0x', function () {
beforeEach(function () {
state.videoSpeedControl.currentSpeed = '1.0';
$('.subtitles li[data-start="14910"]').trigger('click');
waitsFor(function () {
var duration = state.videoPlayer.duration(),
isRendered = state.videoCaption.rendered;
return isRendered && duration;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
describe('when the video speed is 1.0x', function () {
it('trigger seek event with the correct time', function () {
runs(function () {
state.videoSpeedControl.currentSpeed = '1.0';
$('.subtitles li[data-start="14910"]').trigger('click');
expect(state.videoPlayer.currentTime).toEqual(14.91);
});
});
});
describe('when the video speed is not 1.0x', function () {
beforeEach(function () {
it('trigger seek event with the correct time', function () {
runs(function () {
state.videoSpeedControl.currentSpeed = '0.75';
$('.subtitles li[data-start="14910"]').trigger('click');
});
it('trigger seek event with the correct time', function () {
expect(state.videoPlayer.currentTime).toEqual(14.91);
});
});
});
describe('when the player type is Flash at speed 0.75x',
function () {
beforeEach(function () {
it('trigger seek event with the correct time', function () {
runs(function () {
state.videoSpeedControl.currentSpeed = '0.75';
state.currentPlayerMode = 'flash';
$('.subtitles li[data-start="14910"]').trigger('click');
});
it('trigger seek event with the correct time', function () {
expect(state.videoPlayer.currentTime).toEqual(15);
});
});
});
});
describe('toggle', function () {
beforeEach(function () {
......@@ -998,7 +1058,6 @@
beforeEach(function () {
state.el.addClass('closed');
state.videoCaption.toggle(jQuery.Event('click'));
jasmine.Clock.useMock();
});
......@@ -1039,43 +1098,61 @@
describe('caption accessibility', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
});
describe('when getting focus through TAB key', function () {
beforeEach(function () {
runs(function () {
state.videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(
jQuery.Event('focus')
);
});
});
it('shows an outline around the caption', function () {
runs(function () {
expect($('.subtitles li[data-index=0]'))
.toHaveClass('focused');
});
});
it('has automatic scrolling disabled', function () {
runs(function () {
expect(state.videoCaption.autoScrolling).toBe(false);
});
});
});
describe('when loosing focus through TAB key', function () {
beforeEach(function () {
runs(function () {
$('.subtitles li[data-index=0]').trigger(
jQuery.Event('blur')
);
});
});
it('does not show an outline around the caption', function () {
runs(function () {
expect($('.subtitles li[data-index=0]'))
.not.toHaveClass('focused');
});
});
it('has automatic scrolling enabled', function () {
runs(function () {
expect(state.videoCaption.autoScrolling).toBe(true);
});
});
});
describe(
'when same caption gets the focus through mouse after ' +
......@@ -1083,22 +1160,28 @@
function () {
beforeEach(function () {
runs(function () {
state.videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]')
.trigger(jQuery.Event('focus'));
$('.subtitles li[data-index=0]')
.trigger(jQuery.Event('mousedown'));
});
});
it('does not show an outline around it', function () {
runs(function () {
expect($('.subtitles li[data-index=0]'))
.not.toHaveClass('focused');
});
});
it('has automatic scrolling enabled', function () {
runs(function () {
expect(state.videoCaption.autoScrolling).toBe(true);
});
});
});
describe(
'when a second caption gets focus through mouse after ' +
......@@ -1108,6 +1191,7 @@
var subDataLiIdx__0, subDataLiIdx__1;
beforeEach(function () {
runs(function () {
subDataLiIdx__0 = $('.subtitles li[data-index=0]');
subDataLiIdx__1 = $('.subtitles li[data-index=1]');
......@@ -1120,20 +1204,27 @@
subDataLiIdx__1.trigger(jQuery.Event('mousedown'));
});
});
it('does not show an outline around the first', function () {
runs(function () {
expect(subDataLiIdx__0).not.toHaveClass('focused');
});
});
it('does not show an outline around the second', function () {
runs(function () {
expect(subDataLiIdx__1).not.toHaveClass('focused');
});
});
it('has automatic scrolling enabled', function () {
runs(function () {
expect(state.videoCaption.autoScrolling).toBe(true);
});
});
});
});
});
}).call(this);
(function (define) {
define(
'video/00_async_process.js',
[],
function() {
"use strict";
/**
* Provides convenient way to process big amount of data without UI blocking.
*
* @param {array} list Array to process.
* @param {function} process Calls this function on each item in the list.
* @return {array} Returns a Promise object to observe when all actions of a
certain type bound to the collection, queued or not, have finished.
*/
var AsyncProcess = {
array: function (list, process) {
if (!_.isArray(list)) {
return $.Deferred().reject().promise();
}
if (!_.isFunction(process) || !list.length) {
return $.Deferred().resolve(list).promise();
}
var MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously
dfd = $.Deferred(),
result = [],
index = 0,
len = list.length;
var getCurrentTime = function () {
return (new Date()).getTime();
};
var handler = function () {
var start = getCurrentTime();
do {
result[index] = process(list[index], index);
index++;
} while (index < len && getCurrentTime() - start < MAX_DELAY);
if (index < len) {
setTimeout(handler, 25);
} else {
dfd.resolve(result);
}
};
setTimeout(handler, 25);
return dfd.promise();
}
};
return AsyncProcess;
});
}(RequireJS.define));
(function (define) {
define(
'video/00_sjson.js',
[],
function() {
"use strict";
var Sjson = function (data) {
var sjson = {
start: data.start.concat(),
text: data.text.concat()
},
module = {};
var getter = function (propertyName) {
return function () {
return sjson[propertyName];
};
};
var getStartTimes = getter('start');
var getCaptions = getter('text');
var size = function () {
return sjson.text.length;
};
var search = function (time) {
var start = getStartTimes(),
max = size() - 1,
min = 0,
index;
if (time < start[min]) {
return -1;
}
while (min < max) {
index = Math.ceil((max + min) / 2);
if (time < start[index]) {
max = index - 1;
}
if (time >= start[index]) {
min = index;
}
}
return min;
};
return {
getCaptions: getCaptions,
getStartTimes: getStartTimes,
getSize: size,
search: search
};
};
return Sjson;
});
}(RequireJS.define));
(function (requirejs, require, define) {
(function (define) {
// VideoCaption module.
define(
'video/09_video_caption.js',
[],
function () {
['video/00_sjson.js', 'video/00_async_process.js'],
function (Sjson, AsyncProcess) {
/**
* @desc VideoCaption module exports a function.
......@@ -21,16 +21,11 @@ function () {
* @returns {undefined}
*/
return function (state) {
var dfd = $.Deferred();
state.videoCaption = {};
_makeFunctionsPublic(state);
state.videoCaption.renderElements();
dfd.resolve();
return dfd.promise();
return $.Deferred().resolve().promise();
};
// ***************************************************************
......@@ -65,10 +60,8 @@ function () {
renderCaption: renderCaption,
renderElements: renderElements,
renderLanguageMenu: renderLanguageMenu,
reRenderCaption: reRenderCaption,
resize: resize,
scrollCaption: scrollCaption,
search: search,
seekPlayer: seekPlayer,
setSubtitlesHeight: setSubtitlesHeight,
toggle: toggle,
......@@ -133,18 +126,46 @@ function () {
// mousemove, etc.).
function bindHandlers() {
var self = this,
Caption = this.videoCaption;
Caption = this.videoCaption,
events = [
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
'keydown'
].join(' ');
Caption.hideSubtitlesEl.on({
'click': Caption.toggle
});
Caption.subtitlesEl.on({
Caption.subtitlesEl
.on({
mouseenter: Caption.onMouseEnter,
mouseleave: Caption.onMouseLeave,
mousemove: Caption.onMovement,
mousewheel: Caption.onMovement,
DOMMouseScroll: Caption.onMovement
})
.on(events, 'li[data-index]', function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
Caption.captionMouseOverOut(event);
break;
case 'mousedown':
Caption.captionMouseDown(event);
break;
case 'click':
Caption.captionClick(event);
break;
case 'focusin':
Caption.captionFocus(event);
break;
case 'focusout':
Caption.captionBlur(event);
break;
case 'keydown':
Caption.captionKeyDown(event);
break;
}
});
if (Caption.showLanguageMenu) {
......@@ -154,12 +175,6 @@ function () {
});
}
this.el.on('speedchange', function () {
if (self.isFlashMode()) {
Caption.fetchCaption();
}
});
if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
Caption.subtitlesEl.on('scroll', this.videoControl.showControls);
}
......@@ -241,7 +256,7 @@ function () {
if (this.videoType === 'youtube') {
data = {
videoId: this.youtubeId()
videoId: this.youtubeId('1.0')
};
}
......@@ -251,13 +266,15 @@ function () {
url: self.config.transcriptTranslationUrl + '/' + language,
notifyOnError: false,
data: data,
success: function (captions) {
Caption.captions = captions.text;
Caption.start = captions.start;
success: function (response) {
Caption.sjson = new Sjson(response);
var start = Caption.sjson.getStartTimes(),
captions = Caption.sjson.getCaptions();
if (Caption.loaded) {
if (Caption.rendered) {
Caption.reRenderCaption();
Caption.renderCaption(start, captions);
Caption.updatePlayTime(self.videoPlayer.currentTime);
}
} else {
......@@ -269,7 +286,7 @@ function () {
)
);
} else {
Caption.renderCaption();
Caption.renderCaption(start, captions);
}
Caption.bindHandlers();
......@@ -334,7 +351,6 @@ function () {
.height(this.videoCaption.bottomSpacingHeight());
this.videoCaption.scrollCaption();
this.videoCaption.setSubtitlesHeight();
}
......@@ -380,90 +396,53 @@ function () {
});
}
function buildCaptions (container, captions, start) {
var fragment = document.createDocumentFragment();
$.each(captions, function(index, text) {
var liEl = $('<li>');
liEl.html(text);
liEl.attr({
function buildCaptions (container, start, captions) {
var process = function(text, index) {
var liEl = $('<li>', {
'data-index': index,
'data-start': start[index],
'tabindex': 0
});
}).html(text);
fragment.appendChild(liEl[0]);
});
return liEl[0];
};
container.append([fragment]);
return AsyncProcess.array(captions, process).done(function (list) {
container.append(list);
});
}
function renderCaption() {
var Caption = this.videoCaption,
events = ['mouseover', 'mouseout', 'mousedown', 'click', 'focus',
'blur', 'keydown'].join(' ');
Caption.setSubtitlesHeight();
buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
Caption.subtitlesEl.on(events, 'li[data-index]', function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
Caption.captionMouseOverOut(event);
break;
case 'mousedown':
Caption.captionMouseDown(event);
break;
case 'click':
Caption.captionClick(event);
break;
case 'focusin':
Caption.captionFocus(event);
break;
case 'focusout':
Caption.captionBlur(event);
break;
case 'keydown':
Caption.captionKeyDown(event);
break;
}
});
function renderCaption(start, captions) {
var Caption = this.videoCaption;
var onRender = function () {
Caption.addPaddings();
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have this
// flag enabled as we assume mouse use. Then, if the first caption
// (through forward tabbing) or the last caption (through backwards
// tabbing) gets the focus, disable that feature. Re-enable it if tabbing
// then cycles out of the the captions.
// through them as it interferes with that action. Initially, have
// this flag enabled as we assume mouse use. Then, if the first
// caption (through forward tabbing) or the last caption (through
// backwards tabbing) gets the focus, disable that feature.
// Re-enable it if tabbing then cycles out of the the captions.
Caption.autoScrolling = true;
// Keeps track of where the focus is situated in the array of captions.
// Used to implement the automatic scrolling behavior and decide if the
// outline around a caption has to be hidden or shown on a mouseenter
// or mouseleave. Initially, no caption has the focus, set the
// index to -1.
// Keeps track of where the focus is situated in the array of
// captions. Used to implement the automatic scrolling behavior and
// decide if the outline around a caption has to be hidden or shown
// on a mouseenter or mouseleave. Initially, no caption has the
// focus, set the index to -1.
Caption.currentCaptionIndex = -1;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
Caption.isMouseFocus = false;
Caption.addPaddings();
Caption.rendered = true;
}
};
function reRenderCaption() {
var Caption = this.videoCaption;
Caption.currentIndex = null;
Caption.rendered = false;
Caption.subtitlesEl.empty();
buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
Caption.addPaddings();
Caption.rendered = true;
Caption.setSubtitlesHeight();
buildCaptions(Caption.subtitlesEl, start, captions).done(onRender);
}
function addPaddings() {
......@@ -529,7 +508,7 @@ function () {
// off again as it may have been enabled in captionBlur.
if (
captionIndex <= 1 ||
captionIndex >= this.videoCaption.captions.length - 2
captionIndex >= this.videoCaption.sjson.getSize() - 2
) {
this.videoCaption.autoScrolling = false;
}
......@@ -547,7 +526,7 @@ function () {
// tabbing back out of the captions or on the last element and tabbing
// forward out of the captions.
if (captionIndex === 0 ||
captionIndex === this.videoCaption.captions.length - 1) {
captionIndex === this.videoCaption.sjson.getSize() - 1) {
this.videoCaption.autoScrolling = true;
}
......@@ -579,38 +558,13 @@ function () {
}
}
function search(time) {
var index, max, min;
if (this.videoCaption.loaded) {
min = 0;
max = this.videoCaption.start.length - 1;
if (time < this.videoCaption.start[min]) {
return -1;
}
while (min < max) {
index = Math.ceil((max + min) / 2);
if (time < this.videoCaption.start[index]) {
max = index - 1;
}
if (time >= this.videoCaption.start[index]) {
min = index;
}
}
return min;
}
return undefined;
}
function play() {
if (this.videoCaption.loaded) {
if (!this.videoCaption.rendered) {
this.videoCaption.renderCaption();
var start = this.videoCaption.sjson.getStartTimes(),
captions = this.videoCaption.sjson.getCaptions();
this.videoCaption.renderCaption(start, captions);
}
this.videoCaption.playing = true;
......@@ -627,20 +581,12 @@ function () {
var newIndex;
if (this.videoCaption.loaded) {
// Current mode === 'flash' can only be for YouTube videos. So, we
// don't have to also check for videoType === 'youtube'.
if (this.isFlashMode()) {
// Total play time changes with speed change. Also there is
// a 250 ms delay we have to take into account.
time = Math.round(
Time.convert(time, this.speed, '1.0') * 1000 + 100
);
} else {
// Total play time remains constant when speed changes.
time = Math.round(time * 1000 + 100);
time = Time.convert(time, this.speed, '1.0');
}
newIndex = this.videoCaption.search(time);
time = Math.round(time * 1000 + 100);
newIndex = this.videoCaption.sjson.search(time);
if (
typeof newIndex !== 'undefined' &&
......@@ -658,39 +604,27 @@ function () {
.addClass('current');
this.videoCaption.currentIndex = newIndex;
this.videoCaption.scrollCaption();
}
}
}
function seekPlayer(event) {
var time;
var time = parseInt($(event.target).data('start'), 10);
event.preventDefault();
// Current mode === 'flash' can only be for YouTube videos. So, we
// don't have to also check for videoType === 'youtube'.
if (this.isFlashMode()) {
// Total play time changes with speed change. Also there is
// a 250 ms delay we have to take into account.
time = Math.round(
Time.convert(
$(event.target).data('start'), '1.0', this.speed
) / 1000
);
} else {
// Total play time remains constant when speed changes.
time = parseInt($(event.target).data('start'), 10)/1000;
time = Math.round(Time.convert(time, '1.0', this.speed));
}
this.trigger(
'videoPlayer.onCaptionSeek',
{
'type': 'onCaptionSeek',
'time': time
'time': time/1000
}
);
event.preventDefault();
}
function calculateOffset(element) {
......@@ -801,4 +735,4 @@ function () {
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
}(RequireJS.define));
......@@ -72,6 +72,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
'js': [
resource_string(module, 'js/src/video/00_video_storage.js'),
resource_string(module, 'js/src/video/00_resizer.js'),
resource_string(module, 'js/src/video/00_async_process.js'),
resource_string(module, 'js/src/video/00_sjson.js'),
resource_string(module, 'js/src/video/01_initialize.js'),
resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'),
......
......@@ -243,6 +243,7 @@ Feature: LMS Video component
And it has a video in "Youtube" mode:
| transcripts | sub | download_track |
| {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true |
And I see "Hi, welcome to Edx." text in the captions
Then I can download transcript in "srt" format that has text "Hi, welcome to Edx."
And I select language with code "zh"
And I see "好 各位同学" text in the captions
......@@ -256,6 +257,7 @@ Feature: LMS Video component
And it has a video in "HTML5" mode:
| transcripts | sub | download_track |
| {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true |
And I see "Hi, welcome to Edx." text in the captions
Then I can download transcript in "srt" format that has text "Hi, welcome to Edx."
And I select language with code "zh"
And I see "好 各位同学" text in the captions
......@@ -298,8 +300,10 @@ Feature: LMS Video component
And a video "D" in "Youtube_HTML5" mode in position "3" of sequential
And I open the section with videos
Then videos have rendered in "HTML5" mode
And I see "Hi, welcome to Edx." text in the captions
And I see "Equal transcripts" text in the captions
And I see text in the captions:
| text |
| Hi, welcome to Edx. |
| Equal transcripts |
When I open video "C"
Then the video has rendered in "HTML5" mode
And I make sure captions are opened
......
......@@ -419,7 +419,15 @@ def i_see_menu(_step, menu):
@step('I see "([^"]*)" text in the captions$')
def check_text_in_the_captions(_step, text):
assert world.browser.is_text_present(text.strip())
world.wait_for(lambda _: world.css_text('.subtitles'))
actual_text = world.css_text('.subtitles')
assert (text in actual_text)
@step('I see text in the captions:')
def check_captions(_step):
for index, video in enumerate(_step.hashes):
assert (video.get('text') in world.css_text('.subtitles', index=index))
@step('I select language with code "([^"]*)"$')
......@@ -441,14 +449,14 @@ def select_language(_step, code):
# Make sure that all ajax requests that affects the display of captions are finished.
# For example, request to get new translation etc.
world.wait_for_ajax_complete()
assert world.css_visible('.subtitles')
world.wait_for_visible('.subtitles')
@step('I click video button "([^"]*)"$')
def click_button(_step, button):
world.css_click(VIDEO_BUTTONS[button])
@step('I see video starts playing from "([^"]*)" position$')
def start_playing_video_from_n_seconds(_step, position):
world.wait_for(
......@@ -504,9 +512,7 @@ def video_alignment(_step, transcript_visibility):
height = abs(expected['height'] - real['height']) <= 5
# Restore initial window size
set_window_dimensions(
initial['width'], initial['height']
)
set_window_dimensions(initial['width'], initial['height'])
assert all([width, height])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment