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.
......
......@@ -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', {
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', {
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,11 +663,22 @@
expect(state.el).not.toHaveClass('closed');
});
it('scroll the caption', function () {
// 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();
});
});
});
});
describe('caption accessibility', function () {
beforeEach(function () {
......
(function() {
describe('VideoPlayer', function() {
var state, videoPlayer, player, videoControl, videoCaption, videoProgressSlider, videoSpeedControl, videoVolumeControl, oldOTBD;
(function () {
describe('VideoPlayer', function () {
var state, videoPlayer, player, videoControl, videoCaption,
videoProgressSlider, videoSpeedControl, videoVolumeControl,
oldOTBD;
function initialize(fixture) {
if (typeof fixture === 'undefined') {
......@@ -22,16 +24,20 @@
state.resizer = (function () {
var methods = [
'align', 'alignByWidthOnly', 'alignByHeightOnly', 'setParams', 'setMode'
'align',
'alignByWidthOnly',
'alignByHeightOnly',
'setParams',
'setMode'
],
obj = {};
$.each(methods, function(index, method) {
$.each(methods, function (index, method) {
obj[method] = jasmine.createSpy(method).andReturn(obj);
});
return obj;
})();
}());
}
function initializeYouTube() {
......@@ -40,72 +46,85 @@
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
.andReturn(false);
});
afterEach(function() {
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
describe('always', function() {
beforeEach(function() {
describe('constructor', function () {
describe('always', function () {
beforeEach(function () {
initialize();
});
it('instanticate current time to zero', function() {
it('instanticate current time to zero', function () {
expect(videoPlayer.currentTime).toEqual(0);
});
it('set the element', function() {
it('set the element', function () {
expect(state.el).toHaveId('video_id');
});
it('create video control', function() {
it('create video control', function () {
expect(videoControl).toBeDefined();
expect(videoControl.el).toHaveClass('video-controls');
});
it('create video caption', function() {
it('create video caption', function () {
expect(videoCaption).toBeDefined();
expect(state.youtubeId()).toEqual('Z5KLxerq05Y');
expect(state.speed).toEqual('1.0');
expect(state.config.caption_asset_path).toEqual('/static/subs/');
expect(state.config.caption_asset_path)
.toEqual('/static/subs/');
});
it('create video speed control', function() {
it('create video speed control', function () {
expect(videoSpeedControl).toBeDefined();
expect(videoSpeedControl.el).toHaveClass('speeds');
expect(videoSpeedControl.speeds).toEqual([ '0.75', '1.0', '1.25', '1.50' ]);
expect(videoSpeedControl.speeds)
.toEqual([ '0.75', '1.0', '1.25', '1.50' ]);
expect(state.speed).toEqual('1.0');
});
it('create video progress slider', function() {
it('create video progress slider', function () {
expect(videoProgressSlider).toBeDefined();
expect(videoProgressSlider.el).toHaveClass('slider');
});
// All the toHandleWith() expect tests are not necessary for this version of Video.
// jQuery event system is not used to trigger and invoke methods. This is an artifact from
// All the toHandleWith() expect tests are not necessary for
// this version of Video. jQuery event system is not used to
// trigger and invoke methods. This is an artifact from
// previous version of Video.
});
it('create Youtube player', function() {
var oldYT = window.YT;
it('create Youtube player', function () {
var oldYT = window.YT, events;
jasmine.stubRequests();
window.YT = {
Player: function () { },
PlayerState: oldYT.PlayerState,
ready: function(f){f();}
ready: function (f) {
f();
}
};
spyOn(window.YT, 'Player');
initializeYouTube();
events = {
onReady: videoPlayer.onReady,
onStateChange: videoPlayer.onStateChange,
onPlaybackQualityChange: videoPlayer
.onPlaybackQualityChange
};
expect(YT.Player).toHaveBeenCalledWith('id', {
playerVars: {
controls: 0,
......@@ -114,51 +133,46 @@
showinfo: 0,
enablejsapi: 1,
modestbranding: 1,
html5: 1,
start: 0,
end: null
html5: 1
},
videoId: 'cogebirgzzM',
events: {
onReady: videoPlayer.onReady,
onStateChange: videoPlayer.onStateChange,
onPlaybackQualityChange: videoPlayer.onPlaybackQualityChange
}
events: events
});
window.YT = oldYT;
});
// We can't test the invocation of HTML5Video because it is not available
// globally. It is defined within the scope of Require JS.
// We can't test the invocation of HTML5Video because it is not
// available globally. It is defined within the scope of Require
// JS.
describe('when not on a touch based device', function() {
beforeEach(function() {
describe('when not on a touch based device', function () {
beforeEach(function () {
window.onTouchBasedDevice.andReturn(true);
initialize();
});
it('create video volume control', function() {
it('create video volume control', function () {
expect(videoVolumeControl).toBeDefined();
expect(videoVolumeControl.el).toHaveClass('volume');
});
});
describe('when on a touch based device', function() {
describe('when on a touch based device', function () {
var oldOTBD;
beforeEach(function() {
beforeEach(function () {
initialize();
});
it('controls are in paused state', function() {
it('controls are in paused state', function () {
expect(videoControl.isPlaying).toBe(false);
});
});
});
describe('onReady', function() {
beforeEach(function() {
describe('onReady', function () {
beforeEach(function () {
initialize();
spyOn(videoPlayer, 'log').andCallThrough();
......@@ -166,18 +180,18 @@
videoPlayer.onReady();
});
it('log the load_video event', function() {
it('log the load_video event', function () {
expect(videoPlayer.log).toHaveBeenCalledWith('load_video');
});
it('autoplay the first video', function() {
it('autoplay the first video', function () {
expect(videoPlayer.play).not.toHaveBeenCalled();
});
});
describe('onStateChange', function() {
describe('when the video is unstarted', function() {
beforeEach(function() {
describe('onStateChange', function () {
describe('when the video is unstarted', function () {
beforeEach(function () {
initialize();
spyOn(videoControl, 'pause').andCallThrough();
......@@ -188,19 +202,19 @@
});
});
it('pause the video control', function() {
it('pause the video control', function () {
expect(videoControl.pause).toHaveBeenCalled();
});
it('pause the video caption', function() {
it('pause the video caption', function () {
expect(videoCaption.pause).toHaveBeenCalled();
});
});
describe('when the video is playing', function() {
describe('when the video is playing', function () {
var oldState;
beforeEach(function() {
beforeEach(function () {
// Create the first instance of the player.
initialize();
oldState = state;
......@@ -220,38 +234,39 @@
});
});
it('log the play_video event', function() {
expect(videoPlayer.log).toHaveBeenCalledWith('play_video', {
currentTime: 0
});
it('log the play_video event', function () {
expect(videoPlayer.log).toHaveBeenCalledWith(
'play_video', { currentTime: 0 }
);
});
it('pause other video player', function() {
it('pause other video player', function () {
expect(oldState.videoPlayer.onPause).toHaveBeenCalled();
});
it('set update interval', function() {
expect(window.setInterval).toHaveBeenCalledWith(videoPlayer.update, 200);
it('set update interval', function () {
expect(window.setInterval).toHaveBeenCalledWith(
videoPlayer.update, 200
);
expect(videoPlayer.updateInterval).toEqual(100);
});
it('play the video control', function() {
it('play the video control', function () {
expect(videoControl.play).toHaveBeenCalled();
});
it('play the video caption', function() {
it('play the video caption', function () {
expect(videoCaption.play).toHaveBeenCalled();
});
});
describe('when the video is paused', function() {
describe('when the video is paused', function () {
var currentUpdateIntrval;
beforeEach(function() {
beforeEach(function () {
initialize();
spyOn(videoPlayer, 'log').andCallThrough();
spyOn(window, 'clearInterval').andCallThrough();
spyOn(videoControl, 'pause').andCallThrough();
spyOn(videoCaption, 'pause').andCallThrough();
......@@ -266,28 +281,27 @@
});
});
it('log the pause_video event', function() {
expect(videoPlayer.log).toHaveBeenCalledWith('pause_video', {
currentTime: 0
});
it('log the pause_video event', function () {
expect(videoPlayer.log).toHaveBeenCalledWith(
'pause_video', { currentTime: 0 }
);
});
it('clear update interval', function() {
expect(window.clearInterval).toHaveBeenCalledWith(currentUpdateIntrval);
it('clear update interval', function () {
expect(videoPlayer.updateInterval).toBeUndefined();
});
it('pause the video control', function() {
it('pause the video control', function () {
expect(videoControl.pause).toHaveBeenCalled();
});
it('pause the video caption', function() {
it('pause the video caption', function () {
expect(videoCaption.pause).toHaveBeenCalled();
});
});
describe('when the video is ended', function() {
beforeEach(function() {
describe('when the video is ended', function () {
beforeEach(function () {
initialize();
spyOn(videoControl, 'pause').andCallThrough();
......@@ -298,70 +312,103 @@
});
});
it('pause the video control', function() {
it('pause the video control', function () {
expect(videoControl.pause).toHaveBeenCalled();
});
it('pause the video caption', function() {
it('pause the video caption', function () {
expect(videoCaption.pause).toHaveBeenCalled();
});
});
});
describe('onSeek', function() {
beforeEach(function() {
spyOn(window, 'clearInterval').andCallThrough();
describe('onSeek', function () {
beforeEach(function () {
initialize();
videoPlayer.updateInterval = 100;
spyOn(videoPlayer, 'updatePlayTime');
spyOn(videoPlayer, 'log');
spyOn(videoPlayer.player, 'seekTo');
state.videoPlayer.play();
waitsFor(function () {
var duration = videoPlayer.duration(),
currentTime = videoPlayer.currentTime;
return (
isFinite(currentTime) &&
currentTime > 0 &&
isFinite(duration) &&
duration > 0
);
}, 'video begins playing', 10000);
});
it('Slider event causes log update', function () {
videoProgressSlider.onSlide(jQuery.Event('slide'), {value: 60});
runs(function () {
var currentTime = videoPlayer.currentTime;
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 2 }
);
expect(videoPlayer.log).toHaveBeenCalledWith(
'seek_video',
{
old_time: 0,
new_time: 60,
old_time: currentTime,
new_time: 2,
type: 'onSlideSeek'
}
);
});
it('seek the player', function() {
videoProgressSlider.onSlide(jQuery.Event('slide'), {value: 60});
expect(videoPlayer.player.seekTo).toHaveBeenCalledWith(60, true);
});
it('call updatePlayTime on player', function() {
videoProgressSlider.onSlide(jQuery.Event('slide'), {value: 60});
it('seek the player', function () {
runs(function () {
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 60 }
);
expect(videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
expect(videoPlayer.player.seekTo)
.toHaveBeenCalledWith(60, true);
});
});
it('when the player is playing: reset the update interval', function() {
videoProgressSlider.onSlide(jQuery.Event('slide'), {value: 60});
it('call updatePlayTime on player', function () {
runs(function () {
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 60 }
);
expect(window.clearInterval).toHaveBeenCalledWith(100);
expect(videoPlayer.updatePlayTime)
.toHaveBeenCalledWith(60);
});
});
it('when the player is not playing: set the current time', function() {
videoProgressSlider.onSlide(jQuery.Event('slide'), {value: 60});
// Disabled 10/25/13 due to flakiness in master
xit(
'when the player is not playing: set the current time',
function ()
{
runs(function () {
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 20 }
);
videoPlayer.pause();
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 10 }
);
expect(videoPlayer.currentTime).toEqual(60);
waitsFor(function () {
return Math.round(videoPlayer.currentTime) === 10;
}, 'currentTime got updated', 10000);
});
});
});
describe('onSpeedChange', function() {
beforeEach(function() {
describe('onSpeedChange', function () {
beforeEach(function () {
initialize();
videoPlayer.currentTime = 60;
......@@ -372,113 +419,122 @@
spyOn(videoPlayer.player, 'setPlaybackRate').andCallThrough();
});
describe('always', function() {
beforeEach(function() {
describe('always', function () {
beforeEach(function () {
videoPlayer.onSpeedChange('0.75', false);
});
it('check if speed_change_video is logged', function() {
expect(videoPlayer.log).toHaveBeenCalledWith('speed_change_video', {
it('check if speed_change_video is logged', function () {
expect(videoPlayer.log).toHaveBeenCalledWith(
'speed_change_video',
{
current_time: videoPlayer.currentTime,
old_speed: '1.0',
new_speed: '0.75'
});
}
);
});
it('convert the current time to the new speed', function() {
it('convert the current time to the new speed', function () {
expect(videoPlayer.currentTime).toEqual(60);
});
it('set video speed to the new speed', function() {
it('set video speed to the new speed', function () {
expect(state.setSpeed).toHaveBeenCalledWith('0.75', false);
});
// Not relevant any more:
//
// expect( "tell video caption that the speed has changed" ) ...
});
describe('when the video is playing', function() {
beforeEach(function() {
describe('when the video is playing', function () {
beforeEach(function () {
videoPlayer.play();
videoPlayer.onSpeedChange('0.75', false);
});
it('trigger updatePlayTime event', function() {
expect(videoPlayer.player.setPlaybackRate).toHaveBeenCalledWith('0.75');
it('trigger updatePlayTime event', function () {
expect(videoPlayer.player.setPlaybackRate)
.toHaveBeenCalledWith('0.75');
});
});
describe('when the video is not playing', function() {
beforeEach(function() {
describe('when the video is not playing', function () {
beforeEach(function () {
videoPlayer.pause();
videoPlayer.onSpeedChange('0.75', false);
});
it('trigger updatePlayTime event', function() {
expect(videoPlayer.player.setPlaybackRate).toHaveBeenCalledWith('0.75');
it('trigger updatePlayTime event', function () {
expect(videoPlayer.player.setPlaybackRate)
.toHaveBeenCalledWith('0.75');
});
});
});
describe('onVolumeChange', function() {
beforeEach(function() {
describe('onVolumeChange', function () {
beforeEach(function () {
initialize();
spyOn(videoPlayer.player, 'setVolume');
videoPlayer.onVolumeChange(60);
});
it('set the volume on player', function() {
it('set the volume on player', function () {
expect(videoPlayer.player.setVolume).toHaveBeenCalledWith(60);
});
});
describe('update', function() {
beforeEach(function() {
describe('update', function () {
beforeEach(function () {
initialize();
spyOn(videoPlayer, 'updatePlayTime').andCallThrough();
});
describe('when the current time is unavailable from the player', function() {
beforeEach(function() {
describe(
'when the current time is unavailable from the player',
function ()
{
beforeEach(function () {
videoPlayer.player.getCurrentTime = function () {
return NaN;
};
videoPlayer.update();
});
it('does not trigger updatePlayTime event', function() {
it('does not trigger updatePlayTime event', function () {
expect(videoPlayer.updatePlayTime).not.toHaveBeenCalled();
});
});
describe('when the current time is available from the player', function() {
beforeEach(function() {
describe(
'when the current time is available from the player',
function ()
{
beforeEach(function () {
videoPlayer.player.getCurrentTime = function () {
return 60;
};
videoPlayer.update();
});
it('trigger updatePlayTime event', function() {
expect(videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
it('trigger updatePlayTime event', function () {
expect(videoPlayer.updatePlayTime)
.toHaveBeenCalledWith(60);
});
});
});
describe('updatePlayTime', function() {
beforeEach(function() {
// Disabled 10/24/13 due to flakiness in master
xdescribe('updatePlayTime', function () {
beforeEach(function () {
initialize();
spyOn(videoCaption, 'updatePlayTime').andCallThrough();
spyOn(videoProgressSlider, 'updatePlayTime').andCallThrough();
});
it('update the video playback time', function() {
it('update the video playback time', function () {
var duration = 0;
waitsFor(function () {
......@@ -500,7 +556,10 @@
// We resort to this trickery because Firefox and Chrome
// round the total time a bit differently.
if (htmlStr.match('1:00 / 1:01') || htmlStr.match('1:00 / 1:00')) {
if (
htmlStr.match('1:00 / 1:01') ||
htmlStr.match('1:00 / 1:00')
) {
expect(true).toBe(true);
} else {
expect(true).toBe(false);
......@@ -512,43 +571,33 @@
});
});
it('update the playback time on caption', function() {
var duration = 0;
it('update the playback time on caption', function () {
waitsFor(function () {
duration = videoPlayer.duration();
if (duration > 0) {
return true;
}
return false;
return videoPlayer.duration() > 0;
}, 'Video is fully loaded.', 1000);
runs(function () {
videoPlayer.updatePlayTime(60);
expect(videoCaption.updatePlayTime).toHaveBeenCalledWith(60);
expect(videoCaption.updatePlayTime)
.toHaveBeenCalledWith(60);
});
});
it('update the playback time on progress slider', function() {
it('update the playback time on progress slider', function () {
var duration = 0;
waitsFor(function () {
duration = videoPlayer.duration();
if (duration > 0) {
return true;
}
return false;
return duration > 0;
}, 'Video is fully loaded.', 1000);
runs(function () {
videoPlayer.updatePlayTime(60);
expect(videoProgressSlider.updatePlayTime).toHaveBeenCalledWith({
expect(videoProgressSlider.updatePlayTime)
.toHaveBeenCalledWith({
time: 60,
duration: duration
});
......@@ -556,30 +605,31 @@
});
});
describe('toggleFullScreen', function() {
describe('when the video player is not full screen', function() {
beforeEach(function() {
describe('toggleFullScreen', function () {
describe('when the video player is not full screen', function () {
beforeEach(function () {
initialize();
spyOn(videoCaption, 'resize').andCallThrough();
videoControl.toggleFullScreen(jQuery.Event("click"));
videoControl.toggleFullScreen(jQuery.Event('click'));
});
it('replace the full screen button tooltip', function() {
expect($('.add-fullscreen')).toHaveAttr('title', 'Exit full browser');
it('replace the full screen button tooltip', function () {
expect($('.add-fullscreen'))
.toHaveAttr('title', 'Exit full browser');
});
it('add the video-fullscreen class', function() {
it('add the video-fullscreen class', function () {
expect(state.el).toHaveClass('video-fullscreen');
});
it('tell VideoCaption to resize', function() {
it('tell VideoCaption to resize', function () {
expect(videoCaption.resize).toHaveBeenCalled();
expect(state.resizer.setMode).toHaveBeenCalled();
});
});
describe('when the video player already full screen', function() {
beforeEach(function() {
describe('when the video player already full screen', function () {
beforeEach(function () {
initialize();
spyOn(videoCaption, 'resize').andCallThrough();
......@@ -588,121 +638,124 @@
isFullScreen = true;
videoControl.fullScreenEl.attr('title', 'Exit-fullscreen');
videoControl.toggleFullScreen(jQuery.Event("click"));
videoControl.toggleFullScreen(jQuery.Event('click'));
});
it('replace the full screen button tooltip', function() {
expect($('.add-fullscreen')).toHaveAttr('title', 'Fill browser');
it('replace the full screen button tooltip', function () {
expect($('.add-fullscreen'))
.toHaveAttr('title', 'Fill browser');
});
it('remove the video-fullscreen class', function() {
it('remove the video-fullscreen class', function () {
expect(state.el).not.toHaveClass('video-fullscreen');
});
it('tell VideoCaption to resize', function() {
it('tell VideoCaption to resize', function () {
expect(videoCaption.resize).toHaveBeenCalled();
expect(state.resizer.setMode).toHaveBeenCalledWith('width');
expect(state.resizer.setMode)
.toHaveBeenCalledWith('width');
});
});
});
describe('play', function() {
beforeEach(function() {
describe('play', function () {
beforeEach(function () {
initialize();
spyOn(player, 'playVideo').andCallThrough();
});
describe('when the player is not ready', function() {
beforeEach(function() {
describe('when the player is not ready', function () {
beforeEach(function () {
player.playVideo = void 0;
videoPlayer.play();
});
it('does nothing', function() {
it('does nothing', function () {
expect(player.playVideo).toBeUndefined();
});
});
describe('when the player is ready', function() {
beforeEach(function() {
describe('when the player is ready', function () {
beforeEach(function () {
player.playVideo.andReturn(true);
videoPlayer.play();
});
it('delegate to the player', function() {
it('delegate to the player', function () {
expect(player.playVideo).toHaveBeenCalled();
});
});
});
describe('isPlaying', function() {
beforeEach(function() {
describe('isPlaying', function () {
beforeEach(function () {
initialize();
spyOn(player, 'getPlayerState').andCallThrough();
});
describe('when the video is playing', function() {
beforeEach(function() {
describe('when the video is playing', function () {
beforeEach(function () {
player.getPlayerState.andReturn(YT.PlayerState.PLAYING);
});
it('return true', function() {
it('return true', function () {
expect(videoPlayer.isPlaying()).toBeTruthy();
});
});
describe('when the video is not playing', function() {
beforeEach(function() {
describe('when the video is not playing', function () {
beforeEach(function () {
player.getPlayerState.andReturn(YT.PlayerState.PAUSED);
});
it('return false', function() {
it('return false', function () {
expect(videoPlayer.isPlaying()).toBeFalsy();
});
});
});
describe('pause', function() {
beforeEach(function() {
describe('pause', function () {
beforeEach(function () {
initialize();
spyOn(player, 'pauseVideo').andCallThrough();
videoPlayer.pause();
});
it('delegate to the player', function() {
it('delegate to the player', function () {
expect(player.pauseVideo).toHaveBeenCalled();
});
});
describe('duration', function() {
beforeEach(function() {
describe('duration', function () {
beforeEach(function () {
initialize();
spyOn(player, 'getDuration').andCallThrough();
videoPlayer.duration();
});
it('delegate to the player', function() {
it('delegate to the player', function () {
expect(player.getDuration).toHaveBeenCalled();
});
});
describe('playback rate', function() {
beforeEach(function() {
describe('playback rate', function () {
beforeEach(function () {
initialize();
player.setPlaybackRate(1.5);
});
it('set the player playback rate', function() {
it('set the player playback rate', function () {
expect(player.video.playbackRate).toEqual(1.5);
});
});
describe('volume', function() {
beforeEach(function() {
describe('volume', function () {
beforeEach(function () {
initialize();
spyOn(player, 'getVolume').andCallThrough();
});
it('set the player volume', function() {
it('set the player volume', function () {
var expectedValue = 60,
realValue;
......
......@@ -11,10 +11,10 @@
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
.andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
......@@ -38,7 +38,8 @@
});
it('build the seek handle', function() {
expect(videoProgressSlider.handle).toBe('.slider .ui-slider-handle');
expect(videoProgressSlider.handle)
.toBe('.slider .ui-slider-handle');
});
});
......@@ -101,15 +102,22 @@
beforeEach(function() {
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.frozen = false;
videoProgressSlider.updatePlayTime({time:20, duration:120});
videoProgressSlider.updatePlayTime({
time: 20,
duration: 120
});
});
it('update the max value of the slider', function() {
expect($.fn.slider).toHaveBeenCalledWith('option', 'max', 120);
expect($.fn.slider).toHaveBeenCalledWith(
'option', 'max', 120
);
});
it('update current value of the slider', function() {
expect($.fn.slider).toHaveBeenCalledWith('option', 'value', 20);
expect($.fn.slider).toHaveBeenCalledWith(
'option', 'value', 20
);
});
});
});
......@@ -119,27 +127,44 @@
initialize();
spyOn($.fn, 'slider').andCallThrough();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onSlide({}, {
value: 20
});
state.videoPlayer.play();
waitsFor(function () {
var duration = videoPlayer.duration(),
currentTime = videoPlayer.currentTime;
return (
isFinite(currentTime) &&
currentTime > 0 &&
isFinite(duration) &&
duration > 0
);
}, 'video begins playing', 10000);
});
it('freeze the slider', function() {
runs(function () {
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 20 }
);
expect(videoProgressSlider.frozen).toBeTruthy();
});
});
// Turned off test due to flakiness (30.10.2013).
xit('trigger seek event', function() {
runs(function () {
videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 20 }
);
it('trigger seek event', function() {
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
expect(videoPlayer.currentTime).toEqual(20);
});
});
describe('onChange', function() {
beforeEach(function() {
initialize();
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.onChange({}, {
value: 20
waitsFor(function () {
return Math.round(videoPlayer.currentTime) === 20;
}, 'currentTime got updated', 10000);
});
});
});
......@@ -149,44 +174,80 @@
var oldSetTimeout = null;
beforeEach(function() {
// Store original window.setTimeout() function. If we do not do this, then
// all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
// Store original window.setTimeout() function. If we do not do
// this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
oldSetTimeout = window.setTimeout;
// Redefine window.setTimeout() function as a spy.
window.setTimeout = jasmine.createSpy().andCallFake(function(callback, timeout) { return 5; });
window.setTimeout = jasmine.createSpy()
.andCallFake(function (callback, timeout) {
return 5;
});
window.setTimeout.andReturn(100);
initialize();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onStop({}, {
value: 20
});
videoPlayer.play();
waitsFor(function () {
var duration = videoPlayer.duration(),
currentTime = videoPlayer.currentTime;
return (
isFinite(currentTime) &&
currentTime > 0 &&
isFinite(duration) &&
duration > 0
);
}, 'video begins playing', 10000);
});
afterEach(function () {
// Reset the default window.setTimeout() function. If we do not do this,
// then all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
// Reset the default window.setTimeout() function. If we do not
// do this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
window.setTimeout = oldSetTimeout;
});
it('freeze the slider', function() {
runs(function () {
videoProgressSlider.onStop(
jQuery.Event('stop'), { value: 20 }
);
expect(videoProgressSlider.frozen).toBeTruthy();
});
});
// Turned off test due to flakiness (30.10.2013).
xit('trigger seek event', function() {
runs(function () {
videoProgressSlider.onStop(
jQuery.Event('stop'), { value: 20 }
);
it('trigger seek event', function() {
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
expect(videoPlayer.currentTime).toEqual(20);
waitsFor(function () {
return Math.round(videoPlayer.currentTime) === 20;
}, 'currentTime got updated', 10000);
});
});
it('set timeout to unfreeze the slider', function() {
runs(function () {
videoProgressSlider.onStop(
jQuery.Event('stop'), { value: 20 }
);
// Disabled 10/9/13 after failing in master
xit('set timeout to unfreeze the slider', function() {
expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 200);
expect(window.setTimeout).toHaveBeenCalledWith(
jasmine.any(Function), 200
);
window.setTimeout.mostRecentCall.args[0]();
expect(videoProgressSlider.frozen).toBeFalsy();
});
});
});
});
}).call(this);
......@@ -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);
}
......
/**
* @file HTML5 video player module. Provides methods to control the in-browser HTML5 video player.
* @file HTML5 video player module. Provides methods to control the in-browser
* HTML5 video player.
*
* The goal was to write this module so that it closely resembles the YouTube API. The main reason
* for this is because initially the edX video player supported only YouTube videos. When HTML5
* support was added, for greater compatibility, and to reduce the amount of code that needed to
* be modified, it was decided to write a similar API as the one provided by YouTube.
* The goal was to write this module so that it closely resembles the YouTube
* API. The main reason for this is because initially the edX video player
* supported only YouTube videos. When HTML5 support was added, for greater
* compatibility, and to reduce the amount of code that needed to be modified,
* it was decided to write a similar API as the one provided by YouTube.
*
* @external RequireJS
*
......@@ -33,16 +35,17 @@ function () {
};
Player.prototype.seekTo = function (value) {
if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) {
this.start = 0;
this.end = this.video.duration;
if (
typeof value === 'number' &&
value <= this.video.duration &&
value >= 0
) {
this.video.currentTime = value;
}
};
Player.prototype.setVolume = function (value) {
if ((typeof value === 'number') && (value <= 100) && (value >= 0)) {
if (typeof value === 'number' && value <= 100 && value >= 0) {
this.video.volume = value * 0.01;
}
};
......@@ -92,34 +95,32 @@ function () {
/*
* Constructor function for HTML5 Video player.
*
* @param {String|Object} el A DOM element where the HTML5 player will be inserted (as returned by jQuery(selector) function),
* or a selector string which will be used to select an element. This is a required parameter.
* @param {String|Object} el A DOM element where the HTML5 player will
* be inserted (as returned by jQuery(selector) function), or a
* selector string which will be used to select an element. This is a
* required parameter.
*
* @param config - An object whose properties will be used as configuration options for the HTML5 video
* player. This is an optional parameter. In the case if this parameter is missing, or some of the config
* object's properties are missing, defaults will be used. The available options (and their defaults) are as
* @param config - An object whose properties will be used as
* configuration options for the HTML5 video player. This is an
* optional parameter. In the case if this parameter is missing, or
* some of the config object's properties are missing, defaults will be
* used. The available options (and their defaults) are as
* follows:
*
* config = {
*
* videoSources: {}, // An object with properties being video sources. The property name is the
* // video format of the source. Supported video formats are: 'mp4', 'webm', and
* videoSources: {}, // An object with properties being video
* // sources. The property name is the
* // video format of the source. Supported
* // video formats are: 'mp4', 'webm', and
* // 'ogg'.
*
* playerVars: { // Object's properties identify player parameters.
* start: 0, // Possible values: positive integer. Position from which to start playing the
* // video. Measured in seconds. If value is non-numeric, or 'start' property is
* // not specified, the video will start playing from the beginning.
*
* end: null // Possible values: positive integer. Position when to stop playing the
* // video. Measured in seconds. If value is null, or 'end' property is not
* // specified, the video will end playing at the end.
*
* },
*
* events: { // Object's properties identify the events that the API fires, and the
* // functions (event listeners) that the API will call when those events occur.
* // If value is null, or property is not specified, then no callback will be
* events: { // Object's properties identify the
* // events that the API fires, and the
* // functions (event listeners) that the
* // API will call when those events occur.
* // If value is null, or property is not
* // specified, then no callback will be
* // called for that event.
*
* onReady: null,
......@@ -130,16 +131,19 @@ function () {
function Player(el, config) {
var sourceStr, _this, errorMessage;
// Initially we assume that el is a DOM element. If jQuery selector fails to select something, we
// assume that el is an ID of a DOM element. We try to select by ID. If jQuery fails this time,
// we return. Nothing breaks because the player 'onReady' event will never be fired.
// Initially we assume that el is a DOM element. If jQuery selector
// fails to select something, we assume that el is an ID of a DOM
// element. We try to select by ID. If jQuery fails this time, we
// return. Nothing breaks because the player 'onReady' event will
// never be fired.
this.el = $(el);
if (this.el.length === 0) {
this.el = $('#' + el);
if (this.el.length === 0) {
errorMessage = 'VideoPlayer: Element corresponding to the given selector does not found.';
errorMessage = 'VideoPlayer: Element corresponding to ' +
'the given selector does not found.';
if (window.console && console.log) {
console.log(errorMessage);
} else {
......@@ -156,12 +160,14 @@ function () {
return;
}
// We should have at least one video source. Otherwise there is no point to continue.
// We should have at least one video source. Otherwise there is no
// point to continue.
if (!config.videoSources) {
return;
}
// From the start, all sources are empty. We will populate this object below.
// From the start, all sources are empty. We will populate this
// object below.
sourceStr = {
mp4: ' ',
webm: ' ',
......@@ -171,7 +177,8 @@ function () {
// Will be used in inner functions to point to the current object.
_this = this;
// Create HTML markup for individual sources of the HTML5 <video> element.
// Create HTML markup for individual sources of the HTML5 <video>
// element.
$.each(sourceStr, function (videoType, videoSource) {
if (
(_this.config.videoSources[videoType]) &&
......@@ -179,58 +186,60 @@ function () {
) {
sourceStr[videoType] =
'<source ' +
'src="' + _this.config.videoSources[videoType] + '" ' +
'type="video/' + videoType + '" ' +
'src="' + _this.config.videoSources[videoType] +
'" ' + 'type="video/' + videoType + '" ' +
'/> ';
}
});
// We should have at least one video source. Otherwise there is no point to continue.
if ((sourceStr.mp4 === ' ') && (sourceStr.webm === ' ') && (sourceStr.ogg === ' ')) {
// We should have at least one video source. Otherwise there is no
// point to continue.
if (
sourceStr.mp4 === ' ' &&
sourceStr.webm === ' ' &&
sourceStr.ogg === ' '
) {
return;
}
// Determine the starting and ending time for the video.
this.start = config.playerVars.start;
this.end = config.playerVars.end;
// Create HTML markup for the <video> element, populating it with sources from previous step.
// Because of problems with creating video element via jquery
// (http://bugs.jquery.com/ticket/9174) we create it using native JS.
// Create HTML markup for the <video> element, populating it with
// sources from previous step. Because of problems with creating
// video element via jquery (http://bugs.jquery.com/ticket/9174) we
// create it using native JS.
this.video = document.createElement('video');
this.video.innerHTML = _.values(sourceStr).join('');
// Get the jQuery object, and set the player state to UNSTARTED.
// The player state is used by other parts of the VideoPlayer to detrermine what the video is
// currently doing.
// The player state is used by other parts of the VideoPlayer to
// determine what the video is currently doing.
this.videoEl = $(this.video);
this.playerState = HTML5Video.PlayerState.UNSTARTED;
// Attach a 'click' event on the <video> element. It will cause the video to pause/play.
// Attach a 'click' event on the <video> element. It will cause the
// video to pause/play.
this.videoEl.on('click', function (event) {
if (_this.playerState === HTML5Video.PlayerState.PAUSED) {
_this.playVideo();
_this.playerState = HTML5Video.PlayerState.PLAYING;
_this.callStateChangeCallback();
} else if (_this.playerState === HTML5Video.PlayerState.PLAYING) {
} else if (
_this.playerState === HTML5Video.PlayerState.PLAYING
) {
_this.pauseVideo();
_this.playerState = HTML5Video.PlayerState.PAUSED;
_this.callStateChangeCallback();
}
});
// When the <video> tag has been processed by the browser, and it is ready for playback,
// notify other parts of the VideoPlayer, and initially pause the video.
//
// Also, at this time we can get the real duration of the video. Update the starting end ending
// points of the video. Note that first time, the video will start playing at the specified start time,
// and end playing at the specified end time. After it was paused, or when a seek operation happeded,
// the starting time and ending time will reset to the beginning and the end of the video respectively.
// When the <video> tag has been processed by the browser, and it
// is ready for playback, notify other parts of the VideoPlayer,
// and initially pause the video.
this.video.addEventListener('canplay', function () {
// Because firefox triggers 'canplay' event every time when 'currentTime' property
// changes, we must make sure that this block of code runs only once. Otherwise,
// this will be an endless loop ('currentTime' property is changed below).
// Because Firefox triggers 'canplay' event every time when
// 'currentTime' property changes, we must make sure that this
// block of code runs only once. Otherwise, this will be an
// endless loop ('currentTime' property is changed below).
//
// Chrome is immune to this behavior.
if (_this.playerState !== HTML5Video.PlayerState.UNSTARTED) {
......@@ -239,14 +248,6 @@ function () {
_this.playerState = HTML5Video.PlayerState.PAUSED;
if (_this.start > _this.video.duration) {
_this.start = 0;
}
if ((_this.end === null) || (_this.end > _this.video.duration)) {
_this.end = _this.video.duration;
}
_this.video.currentTime = _this.start;
if ($.isFunction(_this.config.events.onReady)) {
_this.config.events.onReady(null);
}
......@@ -275,9 +276,10 @@ function () {
}
}());
// The YouTube API presents several constants which describe the player's state at a given moment.
// HTML5Video API will copy these constats so that code which uses both the YouTube API and this API
// doesn't have to change.
// The YouTube API presents several constants which describe the player's
// state at a given moment. HTML5Video API will copy these constants so
// that code which uses both the YouTube API and this API doesn't have to
// change.
HTML5Video.PlayerState = {
UNSTARTED: -1,
ENDED: 0,
......
......@@ -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();
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.updatePlayTime',
'videoProgressSlider.updateStartEndTimeRegion',
{
time: time,
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;
end = this.videoPlayer.endTime || params.duration;
left = (100 * (start / params.duration)).toFixed(1);
width = (100 * ((end - start) / params.duration)).toFixed(1);
// 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