Commit 1c910023 by David Baumgold

Merge pull request #7111 from ataki/ataki/upstream-pr-video-start-stop

hard start/stop times to video player
parents a91d90cb 2c622636
......@@ -5,6 +5,8 @@ function (Sjson) {
describe('Sjson', function () {
var data = jasmine.stubbedCaption,
sjson;
var videoStops = [0, 3120, 6270, 8490, 21620, 24920];
var OUT_OF_BOUNDS_STOP = 10024920;
beforeEach(function() {
sjson = new Sjson(data);
......@@ -23,12 +25,42 @@ function (Sjson) {
});
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);
expect(sjson.search(videoStops[0])).toEqual(0);
expect(sjson.search(videoStops[1])).toEqual(1);
expect(sjson.search(videoStops[2])).toEqual(2);
expect(sjson.search(videoStops[3])).toEqual(2);
expect(sjson.search(videoStops[4])).toEqual(4);
expect(sjson.search(videoStops[5])).toEqual(5);
});
it('search returns the last entry for a value outside the bounds of the array', function() {
expect(sjson.search(OUT_OF_BOUNDS_STOP)).toEqual(sjson.getCaptions().length - 1);
});
it('search returns the first entry for a negative index in the array', function() {
expect(sjson.search(-1)).toEqual(0);
});
it('search only searches through a subrange of times if start / end times are specified', function () {
var start = videoStops[2] - 100;
var end = videoStops[5] - 100;
var results = sjson.filter(start, end);
var expectedLength = results.captions.length - 1;
expect(sjson.search(videoStops[0], start, end)).toEqual(0);
expect(sjson.search(videoStops[1], start, end)).toEqual(0);
expect(sjson.search(videoStops[2], start, end)).toEqual(0);
expect(sjson.search(videoStops[3], start, end)).toEqual(0);
expect(sjson.search(OUT_OF_BOUNDS_STOP, start, end)).toEqual(expectedLength);
});
it('filters results correctly given a start and end time', function () {
var start = videoStops[1] - 100;
var end = videoStops[4] - 100;
var results = sjson.filter(start, end);
expect(results.start.length).toEqual(3);
expect(results.captions.length).toEqual(3);
});
});
});
......
......@@ -799,6 +799,10 @@
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(5);
// To test speed, don't use start / end times.
state.config.startTime = 0;
state.config.endTime = null;
// Flash mode
state.speed = '2.0';
spyOn(state, 'isFlashMode').andReturn(true);
......
......@@ -278,7 +278,8 @@
describe('constructor with end-time', function () {
it(
'saved position is 0, timer slider and VCR set to 0:00',
'saved position is 0, timer slider and VCR set to 0:00 ' +
'and ending at specified end-time',
function ()
{
var duration, sliderEl, expectedValue;
......@@ -301,7 +302,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:00 / 1:00');
expect(expectedValue).toHaveText('0:00 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(0);
......@@ -335,7 +336,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:15 / 1:00');
expect(expectedValue).toHaveText('0:15 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(15);
......@@ -369,7 +370,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:00 / 1:00');
expect(expectedValue).toHaveText('0:00 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(0);
......@@ -403,7 +404,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:00 / 1:00');
expect(expectedValue).toHaveText('0:00 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(0);
......@@ -438,7 +439,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:00 / 1:00');
expect(expectedValue).toHaveText('0:00 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(0);
......@@ -450,7 +451,7 @@
describe('constructor with start-time and end-time', function () {
it(
'saved position is 0, timer slider and VCR set to start-time',
'saved position is 0, timer slider and VCR set to appropriate start and end times',
function ()
{
var duration, sliderEl, expectedValue;
......@@ -474,7 +475,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:10 / 1:00');
expect(expectedValue).toHaveText('0:10 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(10);
......@@ -509,7 +510,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:15 / 1:00');
expect(expectedValue).toHaveText('0:15 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(15);
......@@ -544,7 +545,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:10 / 1:00');
expect(expectedValue).toHaveText('0:10 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(10);
......@@ -579,7 +580,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:10 / 1:00');
expect(expectedValue).toHaveText('0:10 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(10);
......@@ -614,7 +615,7 @@
runs(function () {
expectedValue = $('.video-controls').find('.vidtime');
expect(expectedValue).toHaveText('0:10 / 1:00');
expect(expectedValue).toHaveText('0:10 / 0:20');
expectedValue = sliderEl.slider('option', 'value');
expect(expectedValue).toBe(10);
......
......@@ -26,6 +26,8 @@
expect(state.videoProgressSlider.slider).toBe('.slider');
expect($.fn.slider).toHaveBeenCalledWith({
range: 'min',
min: 0,
max: null,
change: state.videoProgressSlider.onChange,
slide: state.videoProgressSlider.onSlide,
stop: state.videoProgressSlider.onStop
......
......@@ -27,14 +27,24 @@ function() {
return sjson.text.length;
};
var search = function (time) {
function search(time, startTime, endTime) {
var start = getStartTimes(),
max = size() - 1,
min = 0,
results,
index;
if (time < start[min]) {
return -1;
// if we specify a start and end time to search,
// search the filtered list of captions in between
// the start / end times.
// Else, search the unfiltered list.
if (typeof startTime !== 'undefined' &&
typeof endTime !== 'undefined') {
results = filter(startTime, endTime);
start = results.start;
max = results.captions.length - 1;
} else {
start = getStartTimes();
}
while (min < max) {
index = Math.ceil((max + min) / 2);
......@@ -51,10 +61,52 @@ function() {
return min;
};
function filter(start, end) {
/* filters captions that occur between inputs
* `start` and `end`. Start and end should
* be Numbers (doubles) corresponding to the
* number of seconds elapsed since the beginning
* of the video.
*
* Returns an object with properties
* "start" and "captions" representing
* parallel arrays of start times and
* their corresponding captions.
*/
var filteredTimes = [];
var filteredCaptions = [];
var startTimes = getStartTimes();
var captions = getCaptions();
if (startTimes.length !== captions.length) {
throw new Exception("video caption and start time arrays do not match in length");
}
// if end is null, then it's been set to
// some erroneous value, so filter using the
// entire array as long as it's not empty
if (end === null && startTimes.length) {
end = startTimes[startTimes.length - 1];
}
_.filter(startTimes, function(currentStartTime, i) {
if (currentStartTime >= start && currentStartTime <= end) {
filteredTimes.push(currentStartTime);
filteredCaptions.push(captions[i]);
}
});
return {
'start': filteredTimes,
'captions': filteredCaptions
};
}
return {
getCaptions: getCaptions,
getStartTimes: getStartTimes,
getSize: size,
filter: filter,
search: search
};
};
......
......@@ -320,7 +320,6 @@ function (HTML5Video, Resizer) {
// When the video will start playing again from the start, the
// start-time and end-time will come back into effect.
this.videoPlayer.goToStartTime = true;
this.videoPlayer.stopAtEndTime = true;
}
this.videoPlayer.player.playVideo();
......@@ -340,11 +339,9 @@ function (HTML5Video, Resizer) {
// than end-time. Also, we must make sure that this is only done
// once per video playing from start to end.
if (
this.videoPlayer.stopAtEndTime &&
this.videoPlayer.endTime !== null &&
this.videoPlayer.endTime <= this.videoPlayer.currentTime
) {
this.videoPlayer.stopAtEndTime = false;
this.videoPlayer.pause();
......@@ -463,9 +460,6 @@ function (HTML5Video, Resizer) {
// After the user seeks, the video will start playing from
// the sought point, and stop playing at the end.
this.videoPlayer.goToStartTime = false;
if (time > this.videoPlayer.endTime || this.videoPlayer.endTime === null) {
this.videoPlayer.stopAtEndTime = false;
}
this.videoPlayer.seekTo(time);
this.videoPlayer.log(
......@@ -769,7 +763,6 @@ function (HTML5Video, Resizer) {
videoPlayer.endTime <= videoPlayer.startTime ||
videoPlayer.endTime >= duration
) {
videoPlayer.stopAtEndTime = false;
videoPlayer.endTime = null;
} else if (this.isFlashMode()) {
videoPlayer.endTime /= Number(this.speed);
......@@ -822,14 +815,18 @@ function (HTML5Video, Resizer) {
function updatePlayTime(time, skip_seek) {
var videoPlayer = this.videoPlayer,
duration = this.videoPlayer.duration(),
endTime = this.videoPlayer.duration(),
youTubeId;
if (this.config.endTime) {
endTime = Math.min(this.config.endTime, endTime);
}
this.trigger(
'videoProgressSlider.updatePlayTime',
{
time: time,
duration: duration
duration: endTime
}
);
......@@ -837,7 +834,7 @@ function (HTML5Video, Resizer) {
'videoControl.updateVcrVidTime',
{
time: time,
duration: duration
duration: endTime
}
);
......
......@@ -296,7 +296,10 @@ function () {
}
function updateVcrVidTime(params) {
this.videoControl.vidTimeEl.html(Time.format(params.time) + ' / ' + Time.format(params.duration));
var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration;
// in case endTime is accidentally specified as being greater than the video
endTime = Math.min(endTime, params.duration);
this.videoControl.vidTimeEl.html(Time.format(params.time) + ' / ' + Time.format(endTime));
}
});
......
......@@ -94,6 +94,8 @@ function () {
this.videoProgressSlider.slider = this.videoProgressSlider.el
.slider({
range: 'min',
min: this.config.startTime,
max: this.config.endTime,
slide: this.videoProgressSlider.onSlide,
stop: this.videoProgressSlider.onStop
});
......@@ -147,25 +149,6 @@ function () {
// with actual starting and ending point of the video.
rangeParams = getRangeParams(start, end, duration);
if (!this.videoProgressSlider.sliderRange) {
this.videoProgressSlider.sliderRange = $('<div />', {
'class': 'ui-slider-range ' +
'ui-widget-header ' +
'ui-corner-all ' +
'slider-range'
})
.css({
left: rangeParams.left,
width: rangeParams.width
});
this.videoProgressSlider.sliderProgress
.after(this.videoProgressSlider.sliderRange);
} else {
this.videoProgressSlider.sliderRange
.css(rangeParams);
}
}
function getRangeParams(startTime, endTime, duration) {
......@@ -181,7 +164,11 @@ function () {
function onSlide(event, ui) {
var time = ui.value,
duration = this.videoPlayer.duration();
endTime = this.videoPlayer.duration();
if (this.config.endTime) {
endTime = Math.min(this.config.endTime, endTime);
}
this.videoProgressSlider.frozen = true;
......@@ -193,7 +180,7 @@ function () {
'videoControl.updateVcrVidTime',
{
time: time,
duration: duration
duration: endTime
}
);
......@@ -235,21 +222,28 @@ function () {
}
function updatePlayTime(params) {
var time = Math.floor(params.time),
duration = Math.floor(params.duration);
var time = Math.floor(params.time);
// params.duration could accidentally be construed as a floating
// point double. Since we're displaying this number, round down
// to nearest second
var endTime = Math.floor(params.duration);
if (this.config.endTime !== null) {
endTime = Math.min(this.config.endTime, endTime);
}
if (
this.videoProgressSlider.slider &&
!this.videoProgressSlider.frozen
) {
this.videoProgressSlider.slider
.slider('option', 'max', duration)
.slider('option', 'max', endTime)
.slider('option', 'value', time);
}
// Update aria values.
this.videoProgressSlider.handle.attr({
'aria-valuemax': duration,
'aria-valuemax': endTime,
'aria-valuenow': time
});
}
......
......@@ -196,6 +196,42 @@ function (Sjson, AsyncProcess) {
},
/**
* @desc Gets the correct start and end times from the state configuration
*
* @returns {array} if [startTime, endTime] are defined
*/
getStartEndTimes: function () {
// due to the way config.startTime/endTime are
// processed in 03_video_player.js, we assume
// endTime can be an integer or null,
// and startTime is an integer > 0
var config = this.state.config;
var startTime = config.startTime * 1000;
var endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
return [startTime, endTime];
},
/**
* @desc Gets captions within the start / end times stored within this.state.config
*
* @returns {object} {start, captions} parallel arrays of
* start times and corresponding captions
*/
getBoundedCaptions: function () {
// get start and caption. If startTime and endTime
// are specified, filter by that range.
var times = this.getStartEndTimes();
var results = this.sjson.filter.apply(this.sjson, times);
var start = results.start;
var captions = results.captions;
return {
'start': start,
'captions': captions
};
},
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
*
......@@ -244,9 +280,9 @@ function (Sjson, AsyncProcess) {
data: data,
success: function (sjson) {
self.sjson = new Sjson(sjson);
var start = self.sjson.getStartTimes(),
captions = self.sjson.getCaptions();
var results = self.getBoundedCaptions();
var start = results.start;
var captions = results.captions;
if (self.loaded) {
if (self.rendered) {
......@@ -622,11 +658,12 @@ function (Sjson, AsyncProcess) {
*
*/
play: function () {
var startAndCaptions, start, end;
if (this.loaded) {
if (!this.rendered) {
var start = this.sjson.getStartTimes(),
captions = this.sjson.getCaptions();
startAndCaptions = this.getBoundedCaptions();
start = startAndCaptions.start;
captions = startAndCaptions.captions;
this.renderCaption(start, captions);
}
......@@ -652,6 +689,9 @@ function (Sjson, AsyncProcess) {
*/
updatePlayTime: function (time) {
var state = this.state,
startTime,
endTime,
params,
newIndex;
if (this.loaded) {
......@@ -660,7 +700,11 @@ function (Sjson, AsyncProcess) {
}
time = Math.round(time * 1000 + 100);
newIndex = this.sjson.search(time);
var times = this.getStartEndTimes();
// if start and end times are defined, limit search.
// else, use the entire list of video captions
params = [time].concat(times);
newIndex = this.sjson.search.apply(this.sjson, params);
if (
typeof newIndex !== 'undefined' &&
......
......@@ -52,13 +52,21 @@ class VideoFields(object):
default=""
)
start_time = RelativeTime( # datetime.timedelta object
help=_("Time you want the video to start if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59."),
help=_(
"Time you want the video to start if you don't want the entire video to play. "
"Not supported in the native mobile app: the full video file will play. "
"Formatted as HH:MM:SS. The maximum value is 23:59:59."
),
display_name=_("Video Start Time"),
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
)
end_time = RelativeTime( # datetime.timedelta object
help=_("Time you want the video to stop if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59."),
help=_(
"Time you want the video to stop if you don't want the entire video to play. "
"Not supported in the native mobile app: the full video file will play. "
"Formatted as HH:MM:SS. The maximum value is 23:59:59."
),
display_name=_("Video Stop Time"),
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
......
......@@ -320,5 +320,3 @@ class CMSVideoTest(CMSVideoBaseTest):
self.save_unit_settings()
self.video.click_player_button('play')
self.assertTrue(self.video.is_slider_range_visible)
......@@ -160,28 +160,3 @@ class VideoTimesTest(VideoBaseTest):
self.video.wait_for_state('pause')
self.assertIn(self.video.position, ('0:35', '0:36'))
@flaky # TODO fix this, see TNL-1619
def test_video_finish_time_with_seek(self):
"""
Scenario: Finish Time works for Youtube video.
Given it has a video in "Youtube" mode with end-time at 1:00, the video starts playing from 2:15
And I seek video to "2:15" position
And I click video button "play"
And I wait until video stop playing
Then I see video slider at "2:20" position
"""
data = {'end_time': '00:01:00'}
self.metadata = self.metadata_for_mode('youtube', additional_data=data)
# go to video
self.navigate_to_video()
self.video.seek('2:15')
self.video.click_player_button('play')
# wait until video stop playing
self.video.wait_for_state('finished')
self.assertEqual(self.video.position, '2:20')
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