Commit 9b3bc84c by Valera Rozuvan

Merge pull request #1480 from edx/valera/bugfix_start_end_time_correct_navigation

Bug fix: video end time proper seek beyond.
parents 2628e4f1 5cd4bae3
......@@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Video start and end times now function the same for both YouTube and
HTML5 videos. If end time is set, the video can still play until the end, after
it pauses on the end time.
Blades: Disallow users to enter video url's in http.
Blades: Fix bug when the speed can only be changed when the video is playing.
......@@ -48,7 +52,7 @@ on the request instead of overwriting the POST attr
---------- split mongo backend refactoring changelog section ------------
Studio: course catalog, assets, checklists, course outline pages now use course
Studio: course catalog, assets, checklists, course outline pages now use course
id syntax w/ restful api style
Common:
......@@ -57,7 +61,7 @@ Common:
Common: location mapper: % encode periods and dollar signs when used as key in the mapping dict
Common: location mapper: added a bunch of new helper functions for generating
Common: location mapper: added a bunch of new helper functions for generating
old location style info from a CourseLocator
Common: locators: allow - ~ and . in course, branch, and block ids.
......
......@@ -96,7 +96,10 @@
});
});
it('parse the videos if subtitles do not exist', function () {
it(
'parse the videos if subtitles do not exist',
function ()
{
var sub = '';
$('#example').find('.video').data('sub', '');
......@@ -117,16 +120,41 @@
ogg: null
}, v = document.createElement('video');
if (!!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''))) {
html5Sources['webm'] = 'xmodule/include/fixtures/test.webm';
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/webm; codecs="vp8, vorbis"'
).replace(/no/, '')
)
) {
html5Sources['webm'] =
'xmodule/include/fixtures/test.webm';
}
if (!!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''))) {
html5Sources['mp4'] = 'xmodule/include/fixtures/test.mp4';
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/mp4; codecs="avc1.42E01E, ' +
'mp4a.40.2"'
).replace(/no/, '')
)
) {
html5Sources['mp4'] =
'xmodule/include/fixtures/test.mp4';
}
if (!!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''))) {
html5Sources['ogg'] = 'xmodule/include/fixtures/test.ogv';
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/ogg; codecs="theora"'
).replace(/no/, '')
)
) {
html5Sources['ogg'] =
'xmodule/include/fixtures/test.ogv';
}
expect(state.html5Sources).toEqual(html5Sources);
......@@ -143,10 +171,10 @@
});
});
// Note that the loading of stand alone HTML5 player API is handled by
// Require JS. When state.videoPlayer is created, the stand alone HTML5
// player object is already loaded, so no further testing in that case
// is required.
// Note that the loading of stand alone HTML5 player API is
// handled by Require JS. When state.videoPlayer is created,
// the stand alone HTML5 player object is already loaded, so no
// further testing in that case is required.
describe('HTML5 API is available', function () {
beforeEach(function () {
state = new Video('#example');
......@@ -172,8 +200,10 @@
describe('with speed', function () {
it('return the video id for given speed', function () {
expect(state.youtubeId('0.75')).toEqual(this['7tqY6eQzVhE']);
expect(state.youtubeId('1.0')).toEqual(this['cogebirgzzM']);
expect(state.youtubeId('0.75'))
.toEqual(this['7tqY6eQzVhE']);
expect(state.youtubeId('1.0'))
.toEqual(this['cogebirgzzM']);
});
});
......@@ -210,7 +240,7 @@
itDescription: 'start time is greater than end time',
data: {start: 42, end: 24},
expectData: {start: 42, end: null}
},
}
];
beforeEach(function () {
......@@ -234,8 +264,8 @@
state = new Video('#example');
expect(state.config.start).toBe(expectData.start);
expect(state.config.end).toBe(expectData.end);
expect(state.config.startTime).toBe(expectData.start);
expect(state.config.endTime).toBe(expectData.end);
});
}
});
......@@ -263,7 +293,10 @@
state3 = new Video('#example3');
});
it('check for YT availability is performed only once', function () {
it(
'check for YT availability is performed only once',
function ()
{
var numAjaxCalls = 0;
// Total ajax calls made.
......@@ -307,10 +340,14 @@
});
it('save setting for new speed', function () {
expect($.cookie).toHaveBeenCalledWith('video_speed', '0.75', {
expires: 3650,
path: '/'
});
expect($.cookie).toHaveBeenCalledWith(
'video_speed',
'0.75',
{
expires: 3650,
path: '/'
}
);
});
});
......@@ -341,10 +378,14 @@
});
it('save setting for new speed', function () {
expect($.cookie).toHaveBeenCalledWith('video_speed', '0.75', {
expires: 3650,
path: '/'
});
expect($.cookie).toHaveBeenCalledWith(
'video_speed',
'0.75',
{
expires: 3650,
path: '/'
}
);
});
});
......
......@@ -10,7 +10,8 @@
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(false);
initialize();
player.config.events.onReady = jasmine.createSpy('onReady');
});
......@@ -46,17 +47,22 @@
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.getPlayerState()).toBe(STATUS.PLAYING);
expect(player.getPlayerState())
.toBe(STATUS.PLAYING);
});
});
it('callback was called', function () {
waitsFor(function () {
return state.videoPlayer.player.getPlayerState() !== STATUS.PAUSED;
var stateStatus = state.videoPlayer.player
.getPlayerState();
return stateStatus !== STATUS.PAUSED;
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.callStateChangeCallback).toHaveBeenCalled();
expect(player.callStateChangeCallback)
.toHaveBeenCalled();
});
});
});
......@@ -78,7 +84,8 @@
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
expect(player.getPlayerState())
.toBe(STATUS.PAUSED);
});
});
......@@ -88,7 +95,8 @@
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.callStateChangeCallback).toHaveBeenCalled();
expect(player.callStateChangeCallback)
.toHaveBeenCalled();
});
});
});
......@@ -121,7 +129,8 @@
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.callStateChangeCallback).toHaveBeenCalled();
expect(player.callStateChangeCallback)
.toHaveBeenCalled();
});
});
});
......@@ -156,39 +165,26 @@
return player.getPlayerState() !== STATUS.PLAYING;
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
expect(player.callStateChangeCallback).toHaveBeenCalled();
expect(player.callStateChangeCallback)
.toHaveBeenCalled();
});
});
});
describe('[canplay]', function () {
beforeEach(function () {
it(
'player state was changed, start/end was defined, ' +
'onReady called', function ()
{
waitsFor(function () {
return player.getPlayerState() !== STATUS.UNSTARTED;
}, 'Video cannot be played', WAIT_TIMEOUT);
});
it('player state was changed', function () {
runs(function () {
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
});
});
it('end property was defined', function () {
runs(function () {
expect(player.end).not.toBeNull();
});
});
it('start position was defined', function () {
runs(function () {
expect(player.video.currentTime).toBe(player.start);
});
});
it('onReady callback was called', function () {
runs(function () {
expect(player.config.events.onReady).toHaveBeenCalled();
expect(player.video.currentTime).toBe(0);
expect(player.config.events.onReady)
.toHaveBeenCalled();
});
});
});
......@@ -276,7 +272,8 @@
it('getCurrentTime', function () {
runs(function () {
player.video.currentTime = 3;
expect(player.getCurrentTime()).toBe(player.video.currentTime);
expect(player.getCurrentTime())
.toBe(player.video.currentTime);
});
});
......@@ -330,7 +327,8 @@
});
it('getAvailablePlaybackRates', function () {
expect(player.getAvailablePlaybackRates()).toEqual(playbackRates);
expect(player.getAvailablePlaybackRates())
.toEqual(playbackRates);
});
});
});
......
......@@ -6,13 +6,19 @@ function (Resizer) {
describe('Resizer', function () {
var html = [
'<div class="rszr-wrapper" style="width:200px; height: 200px;">',
'<div class="rszr-el" style="width:100px; height: 150px;">',
'<div ' +
'class="rszr-wrapper" ' +
'style="width:200px; height: 200px;"' +
'>',
'<div ' +
'class="rszr-el" ' +
'style="width:100px; height: 150px;"' +
'>',
'Content',
'</div>',
'</div>'
].join(''),
config, container, element;
config, container, element, originalConsoleLog;
beforeEach(function () {
setFixtures(html);
......@@ -23,12 +29,17 @@ function (Resizer) {
container: container,
element: element
};
originalConsoleLog = window.console.log;
spyOn(console, 'log');
});
afterEach(function () {
window.console.log = originalConsoleLog;
});
it('When Initialize without required parameters, log message is shown',
function () {
spyOn(console, 'log');
new Resizer({ });
expect(console.log).toHaveBeenCalled();
}
......
......@@ -22,7 +22,12 @@
afterEach(function () {
YT.Player = undefined;
$('.subtitles').remove();
// `source` tags should be removed to avoid memory leak bug that we
// had before. Removing of `source` tag, not `video` tag, stops
// loading video source and clears the memory.
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
......@@ -442,7 +447,8 @@
expect(videoCaption.currentIndex).toEqual(5);
});
it('scroll caption to new position', function () {
// Disabled 10/25/13 due to flakiness in master
xit('scroll caption to new position', function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
......@@ -640,6 +646,8 @@
beforeEach(function () {
state.el.addClass('closed');
videoCaption.toggle(jQuery.Event('click'));
jasmine.Clock.useMock();
});
it('log the show_transcript event', function () {
......@@ -655,8 +663,19 @@
expect(state.el).not.toHaveClass('closed');
});
it('scroll the caption', function () {
expect($.fn.scrollTo).toHaveBeenCalled();
// Test turned off due to flakiness (30.10.2013).
xit('scroll the caption', function () {
// After transcripts are shown, and the video plays for a
// bit.
jasmine.Clock.tick(1000);
// The transcripts should have advanced by at least one
// position. When they advance, the list scrolls. The
// current transcript position should be constantly
// visible.
runs(function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
});
});
......
......@@ -23,7 +23,9 @@ function () {
}
if (!config.element) {
console.log('Required parameter `element` is not passed.');
console.log(
'[Video info]: Required parameter `element` is not passed.'
);
}
return this;
......@@ -55,7 +57,7 @@ function () {
};
};
var align = function() {
var align = function () {
var data = getData();
switch (mode) {
......
......@@ -262,8 +262,8 @@ function (VideoPlayer) {
this.config = {
element: element,
start: data['start'],
end: data['end'],
startTime: data['start'],
endTime: data['end'],
caption_data_dir: data['captionDataDir'],
caption_asset_path: data['captionAssetPath'],
show_captions: regExp.test(data['showCaptions'].toString()),
......@@ -369,7 +369,7 @@ function (VideoPlayer) {
/*
* function checkStartEndTimes()
*
* Validate config.start and config.end times.
* Validate config.startTime and config.endTime times.
*
* We can check at this time if the times are proper integers, and if they
* make general sense. I.e. if start time is => 0 and <= end time.
......@@ -379,14 +379,18 @@ function (VideoPlayer) {
* if start time and/or end time are greater than the length of the video.
*/
function checkStartEndTimes() {
this.config.start = parseInt(this.config.start, 10);
if ((!isFinite(this.config.start)) || (this.config.start < 0)) {
this.config.start = 0;
this.config.startTime = parseInt(this.config.startTime, 10);
if (!isFinite(this.config.startTime) || this.config.startTime < 0) {
this.config.startTime = 0;
}
this.config.end = parseInt(this.config.end, 10);
if ((!isFinite(this.config.end)) || (this.config.end < this.config.start)) {
this.config.end = null;
this.config.endTime = parseInt(this.config.endTime, 10);
if (
!isFinite(this.config.endTime) ||
this.config.endTime < this.config.startTime ||
this.config.endTime === 0
) {
this.config.endTime = null;
}
}
......
......@@ -44,8 +44,12 @@ function () {
// Private functions.
function _makeFunctionsPublic(state) {
state.focusGrabber.enableFocusGrabber = _.bind(enableFocusGrabber, state);
state.focusGrabber.disableFocusGrabber = _.bind(disableFocusGrabber, state);
state.focusGrabber.enableFocusGrabber = _.bind(
enableFocusGrabber, state
);
state.focusGrabber.disableFocusGrabber = _.bind(
disableFocusGrabber, state
);
state.focusGrabber.onFocus = _.bind(onFocus, state);
}
......
......@@ -63,9 +63,9 @@ function (HTML5Video, Resizer) {
var youTubeId;
// The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's metadata
// is loaded, which normally happens just after the video starts playing.
// Just after that configurations can be applied.
// by student before video starts playing. Waits until the video's
// metadata is loaded, which normally happens just after the video
// starts playing. Just after that configurations can be applied.
state.videoPlayer.ready = _.once(function () {
state.videoPlayer.onSpeedChange(state.speed);
});
......@@ -79,6 +79,15 @@ function (HTML5Video, Resizer) {
state.videoPlayer.currentTime = 0;
state.videoPlayer.initialSeekToStartTime = true;
state.videoPlayer.oneTimePauseAtEndTime = true;
// The initial value of the variable `seekToStartTimeOldSpeed`
// should always differ from the value returned by the duration
// function.
state.videoPlayer.seekToStartTimeOldSpeed = 'void';
state.videoPlayer.playerVars = {
controls: 0,
wmode: 'transparent',
......@@ -92,9 +101,6 @@ function (HTML5Video, Resizer) {
state.videoPlayer.playerVars.html5 = 1;
}
state.videoPlayer.playerVars.start = state.config.start;
state.videoPlayer.playerVars.end = state.config.end;
// There is a bug which prevents YouTube API to correctly set the speed
// to 1.0 from another speed in Firefox when in HTML5 mode. There is a
// fix which basically reloads the video at speed 1.0 when this change
......@@ -196,6 +202,24 @@ function (HTML5Video, Resizer) {
if (isFinite(this.videoPlayer.currentTime)) {
this.videoPlayer.updatePlayTime(this.videoPlayer.currentTime);
// We need to pause the video is current time is smaller (or equal)
// than end time. Also, we must make sure that the end time is the
// one that was set in the configuration parameter. If it differs,
// this means that it was either reset to the end, or the duration
// changed it's value.
//
// In the case of YouTube Flash mode, we must remember that the
// start and end times are rescaled based on the current speed of
// the video.
if (
this.videoPlayer.endTime <= this.videoPlayer.currentTime &&
this.videoPlayer.oneTimePauseAtEndTime
) {
this.videoPlayer.oneTimePauseAtEndTime = false;
this.videoPlayer.pause();
this.videoPlayer.endTime = this.videoPlayer.duration();
}
}
}
......@@ -254,27 +278,43 @@ function (HTML5Video, Resizer) {
// It is created on a onPlay event. Cleared on a onPause event.
// Reinitialized on a onSeek event.
function onSeek(params) {
var duration = this.videoPlayer.duration(),
newTime = params.time;
if (
(typeof newTime !== 'number') ||
(newTime > duration) ||
(newTime < 0)
) {
return;
}
this.videoPlayer.log(
'seek_video',
{
old_time: this.videoPlayer.currentTime,
new_time: params.time,
new_time: newTime,
type: params.type
}
);
this.videoPlayer.player.seekTo(params.time, true);
this.videoPlayer.startTime = 0;
this.videoPlayer.endTime = duration;
this.videoPlayer.player.seekTo(newTime, true);
if (this.videoPlayer.isPlaying()) {
clearInterval(this.videoPlayer.updateInterval);
this.videoPlayer.updateInterval = setInterval(
this.videoPlayer.update, 200
);
setTimeout(this.videoPlayer.update, 0);
} else {
this.videoPlayer.currentTime = params.time;
this.videoPlayer.currentTime = newTime;
}
this.videoPlayer.updatePlayTime(params.time);
this.videoPlayer.updatePlayTime(newTime);
}
function onEnded() {
......@@ -469,21 +509,89 @@ function (HTML5Video, Resizer) {
}
function updatePlayTime(time) {
var duration;
var duration, durationChange;
duration = this.videoPlayer.duration();
this.trigger(
'videoProgressSlider.updatePlayTime',
{
time: time,
duration: duration
if (
duration > 0 &&
(
this.videoPlayer.seekToStartTimeOldSpeed !== this.speed ||
this.videoPlayer.initialSeekToStartTime
)
) {
if (
this.videoPlayer.seekToStartTimeOldSpeed !== this.speed &&
this.videoPlayer.initialSeekToStartTime === false
) {
durationChange = true;
} else {
durationChange = false;
}
);
this.videoPlayer.initialSeekToStartTime = false;
this.videoPlayer.seekToStartTimeOldSpeed = this.speed;
// We retrieve the original times. They could have been changed due
// to the fact of speed change (duration change). This happens when
// in YouTube Flash mode. There each speed is a different video,
// with a different length.
this.videoPlayer.startTime = this.config.startTime;
this.videoPlayer.endTime = this.config.endTime;
if (this.videoPlayer.startTime > duration) {
this.videoPlayer.startTime = 0;
} else {
if (this.currentPlayerMode === 'flash') {
this.videoPlayer.startTime /= Number(this.speed);
}
}
if (
this.videoPlayer.endTime === null ||
this.videoPlayer.endTime > duration
) {
this.videoPlayer.endTime = duration;
} else {
if (this.currentPlayerMode === 'flash') {
this.videoPlayer.endTime /= Number(this.speed);
}
}
// If this is not a duration change (if it is, we continue playing
// from current time), then we need to seek the video to the start
// time.
//
// We seek only if start time differs from zero.
if (durationChange === false && this.videoPlayer.startTime > 0) {
if (this.videoType === 'html5') {
this.videoPlayer.player.seekTo(this.videoPlayer.startTime);
} else {
this.videoPlayer.player.loadVideoById({
videoId: this.youtubeId(),
startSeconds: this.videoPlayer.startTime
});
}
}
// Rebuild the slider start-end range (if it doesn't take up the
// whole slider).
if (!(
this.videoPlayer.startTime === 0 &&
this.videoPlayer.endTime === duration
)) {
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
}
}
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
'videoProgressSlider.updatePlayTime',
{
time: time,
duration: duration
}
);
......@@ -506,10 +614,26 @@ function (HTML5Video, Resizer) {
return playerState === PLAYING;
}
/*
* Return the duration of the video in seconds.
*
* First, try to use the native player API call to get the duration.
* If the value returned by the native function is not valid, resort to
* the value stored in the metadata for the video. Note that the metadata
* is available only for YouTube videos.
*
* IMPORTANT! It has been observed that sometimes, after initial playback
* of the video, when operations "pause" and "play" are performed (in that
* sequence), the function will start returning a slightly different value.
*
* For example: While playing for the first time, the function returns 31.
* After pausing the video and then resuming once more, the function will
* start returning 31.950656.
*
* This instability is internal to the player API (or browser internals).
*/
function duration() {
var dur;
dur = this.videoPlayer.player.getDuration();
var dur = this.videoPlayer.player.getDuration();
if (!isFinite(dur)) {
dur = this.getDuration();
......
......@@ -99,7 +99,7 @@ function () {
}
function updateStartEndTimeRegion(params) {
var left, width, start, end;
var left, width, start, end, step;
// We must have a duration in order to determine the area of range.
// It also must be non-zero.
......@@ -108,19 +108,28 @@ function () {
}
// If the range spans the entire length of video, we don't do anything.
if (!this.config.start && !this.config.end) {
if (!this.videoPlayer.startTime && !this.videoPlayer.endTime) {
return;
}
start = this.config.start;
start = this.videoPlayer.startTime;
// If end is set to null, then we set it to the end of the video. We
// know that start is not a the beginning, therefore we must build a
// range.
end = this.config.end || params.duration;
left = (100 * (start / params.duration)).toFixed(1);
width = (100 * ((end - start) / params.duration)).toFixed(1);
end = this.videoPlayer.endTime || params.duration;
// Because JavaScript has weird rounding rules when a series of
// mathematical operations are performed in a single statement, we will
// split everything up into smaller statements.
//
// This will ensure that visually, the start-end range aligns nicely
// with actual starting and ending point of the video.
step = 100.0 / params.duration;
left = start * step;
width = end * step - left;
left = left.toFixed(1);
width = width.toFixed(1);
if (!this.videoProgressSlider.sliderRange) {
this.videoProgressSlider.sliderRange = $('<div />', {
......
......@@ -241,9 +241,10 @@ function () {
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log('ERROR while fetching captions.');
console.log('[Video info]: ERROR while fetching captions.');
console.log(
'STATUS:', textStatus + ', MESSAGE:', '' + errorThrown
'[Video info]: STATUS:', textStatus +
', MESSAGE:', '' + errorThrown
);
_this.videoCaption.hideCaptions(true, false);
......
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