Commit 2536224b by Valera Rozuvan

Merge pull request #2176 from edx/valera/fix_start-time-end-time_2

Valera/fix start time end time 2
parents 591470df b6e5db02
......@@ -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 player start-end time range is now shown even before Play is
clicked. Video player VCR time shows correct non-zero total time for YouTube
videos even before Play is clicked. BLD-529.
Blades: Adds CookieStorage utility for video player that provides convenient
way to work with cookies.
......
......@@ -86,7 +86,7 @@ Feature: CMS.Video Component
# 11
Scenario: When start end end times are specified, a range on slider is shown
Given I have created a Video component
Given I have created a Video component with subtitles
And Make sure captions are closed
And I edit the component
And I open tab "Advanced"
......
......@@ -24,7 +24,7 @@ class StubYouTubeServiceTest(unittest.TestCase):
self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func'
)
self.assertEqual('callback_func({"message": "I\'m youtube."})', response.content)
self.assertEqual('callback_func({"data": {"duration": 60, "message": "I\'m youtube.", "id": "OEoXaMPEzfM"}})', response.content)
def test_transcript_url_equal(self):
response = requests.get(
......
......@@ -6,6 +6,8 @@ from .http import StubHttpRequestHandler, StubHttpService
import json
import time
import requests
from urlparse import urlparse
from collections import OrderedDict
class StubYouTubeHandler(StubHttpRequestHandler):
......@@ -54,14 +56,17 @@ class StubYouTubeHandler(StubHttpRequestHandler):
self.send_response(404)
elif 'test_youtube' in self.path:
self._send_video_response("I'm youtube.")
params = urlparse(self.path)
youtube_id = params.path.split('/').pop()
self._send_video_response(youtube_id, "I'm youtube.")
else:
self.send_response(
404, content="Unused url", headers={'Content-type': 'text/plain'}
)
def _send_video_response(self, message):
def _send_video_response(self, youtube_id, message):
"""
Send message back to the client for video player requests.
Requires sending back callback id.
......@@ -71,7 +76,14 @@ class StubYouTubeHandler(StubHttpRequestHandler):
# Construct the response content
callback = self.get_params['callback'][0]
response = callback + '({})'.format(json.dumps({'message': message}))
data = OrderedDict({
'data': OrderedDict({
'id': youtube_id,
'message': message,
'duration': 60,
})
})
response = "{cb}({data})".format(cb=callback, data=json.dumps(data))
self.send_response(200, content=response, headers={'Content-type': 'text/html'})
self.log_message("Youtube: sent response {}".format(message))
......
# Stub Youtube API
window.YT =
Player: ->
PlayerState:
UNSTARTED: -1
ENDED: 0
PLAYING: 1
PAUSED: 2
BUFFERING: 3
CUED: 5
ready: (f) -> f()
window.STATUS = window.YT.PlayerState
oldAjaxWithPrefix = window.jQuery.ajaxWithPrefix
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i
jasmine.stubbedCaption =
end: [3120, 6270, 8490, 21620, 24920, 25750, 27900, 34380, 35550, 40250]
start: [1180, 3120, 6270, 14910, 21620, 24920, 25750, 27900, 34380, 35550]
text: [
"MICHAEL CIMA: So let's do the first one here.",
"Vacancies, where do they come from?",
"Well, imagine a perfect crystal.",
"Now we know at any temperature other than absolute zero there's enough",
"energy going around that some atoms will have more energy",
"than others, right?",
"There's a distribution.",
"If I plot energy here and number, these atoms in the crystal will have a",
"distribution of energy.",
"And some will have quite a bit of energy, just for a moment."
]
# For our purposes, we need to make sure that the function $.ajaxWithPrefix
# does not fail when during tests a captions file is requested.
# It is originally defined in
#
# common/static/coffee/src/ajax_prefix.js
#
# We will replace it with a function that does:
#
# 1.) Return a hard coded captions object if the file name contains 'Z5KLxerq05Y'.
# 2.) Behaves the same a as the origianl in all other cases.
window.jQuery.ajaxWithPrefix = (url, settings) ->
if not settings
settings = url
url = settings.url
success = settings.success
data = settings.data
if url.match(/Z5KLxerq05Y/g) isnt null or url.match(/7tqY6eQzVhE/g) isnt null or url.match(/cogebirgzzM/g) isnt null
if window.jQuery.isFunction(success) is true
success jasmine.stubbedCaption
else if window.jQuery.isFunction(data) is true
data jasmine.stubbedCaption
else
oldAjaxWithPrefix.apply @, arguments
# Time waitsFor() should wait for before failing a test.
window.WAIT_TIMEOUT = 5000
jasmine.getFixtures().fixturesPath += 'fixtures'
jasmine.stubbedMetadata =
'7tqY6eQzVhE':
id: '7tqY6eQzVhE'
duration: 300
'cogebirgzzM':
id: 'cogebirgzzM'
duration: 200
bogus:
duration: 100
jasmine.fireEvent = (el, eventName) ->
if document.createEvent
event = document.createEvent "HTMLEvents"
event.initEvent eventName, true, true
else
event = document.createEventObject()
event.eventType = eventName
event.eventName = eventName
if document.createEvent
el.dispatchEvent(event)
else
el.fireEvent("on" + event.eventType, event)
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
status = match[1].split('_')
if status and status[0] is 'status'
{
always: (callback) ->
callback.call(window, {}, status[1])
error: (callback) ->
callback.call(window, {}, status[1])
done: (callback) ->
callback.call(window, {}, status[1])
}
else if settings.success
# match[1] - it's video ID
settings.success data: jasmine.stubbedMetadata[match[1]]
else {
always: (callback) ->
callback.call(window, {}, 'success')
done: (callback) ->
callback.call(window, {}, 'success')
}
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption
else if settings.url.match /.+\/problem_get$/
settings.success html: readFixtures('problem_content.html')
else if settings.url == '/calculate' ||
settings.url.match(/.+\/goto_position$/) ||
settings.url.match(/event$/) ||
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
# do nothing
else
throw "External request attempted for #{settings.url}, which is not defined."
jasmine.stubYoutubePlayer = ->
YT.Player = ->
obj = jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
'playVideo', 'pauseVideo', 'seekTo', 'getDuration', 'getAvailablePlaybackRates', 'setPlaybackRate']
obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5]
obj
jasmine.stubVideoPlayer = (context, enableParts, html5=false) ->
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
if html5 == false
loadFixtures 'video.html'
else
loadFixtures 'video_html5.html'
jasmine.stubRequests()
YT.Player = undefined
window.OldVideoPlayer = undefined
jasmine.stubYoutubePlayer()
return new Video '#example', '.75:7tqY6eQzVhE,1.0:cogebirgzzM'
# Add custom matchers
beforeEach ->
@addMatchers
toHaveAttrs: (attrs) ->
element = @.actual
result = true
if $.isEmptyObject attrs
return false
$.each attrs, (name, value) ->
result = result && element.attr(name) == value
return result
toBeInRange: (min, max) ->
return min <= @.actual && @.actual <= max
toBeInArray: (array) ->
return $.inArray(@.actual, array) > -1
@addMatchers imagediff.jasmine
# Stub jQuery.cookie
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
# Stub jQuery.qtip
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
# Stub jQuery.scrollTo
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
(function ($, undefined) {
var oldAjaxWithPrefix = $.ajaxWithPrefix;
// Stub YouTube API.
window.YT = {
Player: function () {
var Player = jasmine.createSpyObj(
'YT.Player',
[
'cueVideoById', 'getVideoEmbedCode', 'getCurrentTime',
'getPlayerState', 'getVolume', 'setVolume',
'loadVideoById', 'getAvailablePlaybackRates', 'playVideo',
'pauseVideo', 'seekTo', 'getDuration', 'setPlaybackRate',
'getPlaybackQuality'
]
);
Player.getDuration.andReturn(60);
Player.getAvailablePlaybackRates.andReturn(['0.50', '1.0', '1.50', '2.0']);
return Player;
},
PlayerState: {
UNSTARTED: -1,
ENDED: 0,
PLAYING: 1,
PAUSED: 2,
BUFFERING: 3,
CUED: 5
},
ready: function (f) {
return f();
}
};
window.STATUS = window.YT.PlayerState;
window.onTouchBasedDevice = function () {
return navigator.userAgent.match(/iPhone|iPod|iPad/i);
};
jasmine.stubbedCaption = {
end: [
3120, 6270, 8490, 21620, 24920, 25750, 27900, 34380, 35550, 40250
],
start: [
1180, 3120, 6270, 14910, 21620, 24920, 25750, 27900, 34380, 35550
],
text: [
'MICHAEL CIMA: So let\'s do the first one here.',
'Vacancies, where do they come from?',
'Well, imagine a perfect crystal.',
'Now we know at any temperature other than absolute zero ' +
'there\'s enough',
'energy going around that some atoms will have more energy',
'than others, right?',
'There\'s a distribution.',
'If I plot energy here and number, these atoms in the crystal ' +
'will have a',
'distribution of energy.',
'And some will have quite a bit of energy, just for a moment.'
]
};
// For our purposes, we need to make sure that the function
// $.ajaxWithPrefix does not fail when during tests a captions file is
// requested. It is originally defined in file:
//
// common/static/coffee/src/ajax_prefix.js
//
// We will replace it with a function that does:
//
// 1.) Return a hard coded captions object if the file name contains
// 'Z5KLxerq05Y'.
// 2.) Behaves the same a as the original function in all other cases.
$.ajaxWithPrefix = function (url, settings) {
var data, success;
if (!settings) {
settings = url;
url = settings.url;
success = settings.success;
data = settings.data;
}
if (
url.match(/Z5KLxerq05Y/g) ||
url.match(/7tqY6eQzVhE/g) ||
url.match(/cogebirgzzM/g)
) {
if ($.isFunction(success)) {
return success(jasmine.stubbedCaption);
} else if ($.isFunction(data)) {
return data(jasmine.stubbedCaption);
}
} else {
return oldAjaxWithPrefix.apply(this, arguments);
}
};
// Time waitsFor() should wait for before failing a test.
window.WAIT_TIMEOUT = 5000;
jasmine.getFixtures().fixturesPath += 'fixtures';
jasmine.stubbedMetadata = {
'7tqY6eQzVhE': {
id: '7tqY6eQzVhE',
duration: 300
},
'cogebirgzzM': {
id: 'cogebirgzzM',
duration: 200
},
bogus: {
duration: 100
}
};
jasmine.fireEvent = function (el, eventName) {
var event;
if (document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent(eventName, true, true);
} else {
event = document.createEventObject();
event.eventType = eventName;
}
event.eventName = eventName;
if (document.createEvent) {
el.dispatchEvent(event);
} else {
el.fireEvent('on' + event.eventType, event);
}
};
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'];
jasmine.stubRequests = function () {
return spyOn($, 'ajax').andCallFake(function (settings) {
var match, status, callCallback;
if (
match = settings.url
.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)
) {
status = match[1].split('_');
if (status && status[0] === 'status') {
callCallback = function (callback) {
callback.call(window, {}, status[1]);
};
return {
always: callCallback,
error: callCallback,
done: callCallback
};
} else if (settings.success) {
return settings.success({
data: jasmine.stubbedMetadata[match[1]]
});
} else {
return {
always: function (callback) {
return callback.call(window, {}, 'success');
},
done: function (callback) {
return callback.call(window, {}, 'success');
}
};
}
} else if (
match = settings.url
.match(/static(\/.*)?\/subs\/(.+)\.srt\.sjson/)
) {
return settings.success(jasmine.stubbedCaption);
} else if (settings.url.match(/.+\/problem_get$/)) {
return settings.success({
html: readFixtures('problem_content.html')
});
} else if (
settings.url === '/calculate' ||
settings.url.match(/.+\/goto_position$/) ||
settings.url.match(/event$/) ||
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
) {
// Do nothing.
} else {
throw 'External request attempted for ' +
settings.url +
', which is not defined.';
}
});
};
// Add custom Jasmine matchers.
beforeEach(function () {
this.addMatchers({
toHaveAttrs: function (attrs) {
var element = this.actual,
result = true;
if ($.isEmptyObject(attrs)) {
return false;
}
$.each(attrs, function (name, value) {
return result = result && element.attr(name) === value;
});
return result;
},
toBeInRange: function (min, max) {
return min <= this.actual && this.actual <= max;
},
toBeInArray: function (array) {
return $.inArray(this.actual, array) > -1;
}
});
return this.addMatchers(imagediff.jasmine);
});
// Stub jQuery.cookie module.
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn('1.0');
// # Stub jQuery.qtip module.
$.fn.qtip = jasmine.createSpy('jQuery.qtip');
// Stub jQuery.scrollTo module.
$.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo');
jasmine.initializePlayer = function (fixture, params) {
var state;
if (_.isString(fixture)) {
// `fixture` is a name of a fixture file.
loadFixtures(fixture);
} else {
// `fixture` is not a string. The first parameter is an object?
if (_.isObject(fixture)) {
// The first parameter contains attributes for the main video
// DIV element.
params = fixture;
}
// "video_all.html" is the default HTML template for HTML5 video.
loadFixtures('video_all.html');
}
// If `params` is an object, assign it's properties as data attributes
// to the main video DIV element.
if (_.isObject(params)) {
$('#example')
.find('#video_id')
.data(params);
}
state = new Video('#example');
state.resizer = (function () {
var methods = [
'align',
'alignByWidthOnly',
'alignByHeightOnly',
'setParams',
'setMode'
],
obj = {};
$.each(methods, function (index, method) {
obj[method] = jasmine.createSpy(method).andReturn(obj);
});
return obj;
}());
// We return the `state` object of the newly initialized Video.
return state;
};
jasmine.initializePlayerYouTube = function () {
// "video.html" contains HTML template for a YouTube video.
return jasmine.initializePlayer('video.html');
};
}).call(this, window.jQuery);
(function () {
(function (undefined) {
describe('VideoPlayer Events', function () {
var state, videoPlayer, player, videoControl, videoCaption,
videoProgressSlider, videoSpeedControl, videoVolumeControl,
oldOTBD;
function initialize(fixture, params) {
if (_.isString(fixture)) {
loadFixtures(fixture);
} else {
if (_.isObject(fixture)) {
params = fixture;
}
loadFixtures('video_all.html');
}
if (_.isObject(params)) {
$('#example')
.find('#video_id')
.data(params);
}
state = new Video('#example');
state.videoEl = $('video, iframe');
videoPlayer = state.videoPlayer;
player = videoPlayer.player;
videoControl = state.videoControl;
videoCaption = state.videoCaption;
videoProgressSlider = state.videoProgressSlider;
videoSpeedControl = state.videoSpeedControl;
videoVolumeControl = state.videoVolumeControl;
state.resizer = (function () {
var methods = [
'align',
'alignByWidthOnly',
'alignByHeightOnly',
'setParams',
'setMode'
],
obj = {};
$.each(methods, function (index, method) {
obj[method] = jasmine.createSpy(method).andReturn(obj);
});
var state, oldOTBD;
return obj;
}());
}
function initializeYouTube() {
initialize('video.html');
}
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
.andReturn(null);
this.oldYT = window.YT;
jasmine.stubRequests();
window.YT = {
Player: function () {
return {
getPlaybackQuality: function () {}
};
},
PlayerState: this.oldYT.PlayerState,
ready: function (callback) {
callback();
}
};
});
describe('HTML5', function () {
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
window.YT = this.oldYT;
});
jasmine.stubRequests();
state = jasmine.initializePlayer();
it('initialize', function(){
runs(function () {
initialize();
state.videoEl = $('video, iframe');
});
waitsFor(function () {
return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
it('initialize', function () {
waitsFor(function () {
return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
runs(function () {
expect('initialize').not.toHaveBeenTriggeredOn('.video');
runs(function () {
expect('initialize').not.toHaveBeenTriggeredOn('.video');
});
});
});
it('ready', function() {
runs(function () {
initialize();
it('ready', function () {
waitsFor(function () {
return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
runs(function () {
expect('ready').not.toHaveBeenTriggeredOn('.video');
});
});
waitsFor(function () {
return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
it('play', function () {
state.videoPlayer.play();
expect('play').not.toHaveBeenTriggeredOn('.video');
});
runs(function () {
expect('ready').not.toHaveBeenTriggeredOn('.video');
it('pause', function () {
state.videoPlayer.play();
state.videoPlayer.pause();
expect('pause').not.toHaveBeenTriggeredOn('.video');
});
});
it('play', function() {
initialize();
videoPlayer.play();
expect('play').not.toHaveBeenTriggeredOn('.video');
});
it('volumechange', function () {
state.videoPlayer.onVolumeChange(60);
it('pause', function() {
initialize();
videoPlayer.play();
videoPlayer.pause();
expect('pause').not.toHaveBeenTriggeredOn('.video');
});
expect('volumechange').not.toHaveBeenTriggeredOn('.video');
});
it('volumechange', function() {
initialize();
videoPlayer.onVolumeChange(60);
it('speedchange', function () {
state.videoPlayer.onSpeedChange('2.0');
expect('volumechange').not.toHaveBeenTriggeredOn('.video');
});
expect('speedchange').not.toHaveBeenTriggeredOn('.video');
});
it('speedchange', function() {
initialize();
videoPlayer.onSpeedChange('2.0');
it('seek', function () {
state.videoPlayer.onCaptionSeek({
time: 1,
type: 'any'
});
expect('speedchange').not.toHaveBeenTriggeredOn('.video');
});
expect('seek').not.toHaveBeenTriggeredOn('.video');
});
it('qualitychange', function() {
initializeYouTube();
videoPlayer.onPlaybackQualityChange();
it('ended', function () {
state.videoPlayer.onEnded();
expect('qualitychange').not.toHaveBeenTriggeredOn('.video');
expect('ended').not.toHaveBeenTriggeredOn('.video');
});
});
it('seek', function() {
initialize();
videoPlayer.onCaptionSeek({
time: 1,
type: 'any'
describe('YouTube', function () {
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
jasmine.stubRequests();
state = jasmine.initializePlayerYouTube();
});
expect('seek').not.toHaveBeenTriggeredOn('.video');
});
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
it('ended', function() {
initialize();
videoPlayer.onEnded();
it('qualitychange', function () {
state.videoPlayer.onPlaybackQualityChange();
expect('ended').not.toHaveBeenTriggeredOn('.video');
expect('qualitychange').not.toHaveBeenTriggeredOn('.video');
});
});
});
}).call(this);
(function () {
(function (undefined) {
describe('Video', function () {
var oldOTBD;
......@@ -10,7 +10,6 @@
});
afterEach(function () {
window.OldVideoPlayer = undefined;
$('source').remove();
});
......@@ -30,10 +29,6 @@
expect(this.state.videoType).toEqual('youtube');
});
it('reset the current video player', function () {
expect(window.OldVideoPlayer).toBeUndefined();
});
it('set the elements', function () {
expect(this.state.el).toBe('#video_id');
});
......@@ -76,10 +71,6 @@
expect(state.videoType).toEqual('html5');
});
it('reset the current video player', function () {
expect(window.OldVideoPlayer).toBeUndefined();
});
it('set the elements', function () {
expect(state.el).toBe('#video_id');
});
......
(function (requirejs, require, define) {
(function (requirejs, require, define, undefined) {
require(
['video/00_resizer.js'],
......@@ -104,7 +104,7 @@ function (Resizer) {
beforeEach(function () {
var spiesCount = _.range(3);
spiesList = $.map(spiesCount, function() {
spiesList = $.map(spiesCount, function () {
return jasmine.createSpy();
});
......@@ -113,13 +113,13 @@ function (Resizer) {
it('callbacks are called', function () {
$.each(spiesList, function(index, spy) {
$.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy);
});
resizer.align();
$.each(spiesList, function(index, spy) {
$.each(spiesList, function (index, spy) {
expect(spy).toHaveBeenCalled();
});
});
......@@ -135,20 +135,20 @@ function (Resizer) {
});
it('All callbacks are removed', function () {
$.each(spiesList, function(index, spy) {
$.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy);
});
resizer.callbacks.removeAll();
resizer.align();
$.each(spiesList, function(index, spy) {
$.each(spiesList, function (index, spy) {
expect(spy).not.toHaveBeenCalled();
});
});
it('Specific callback is removed', function () {
$.each(spiesList, function(index, spy) {
$.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy);
});
......@@ -158,14 +158,17 @@ function (Resizer) {
expect(spiesList[1]).not.toHaveBeenCalled();
});
it('Error message is shown when wrong argument type is passed', function () {
it(
'Error message is shown when wrong argument type is passed',
function ()
{
var methods = ['add', 'once'],
errorMessage = 'TypeError: Argument is not a function.',
arg = {};
spyOn(console, 'error');
$.each(methods, function(index, methodName) {
$.each(methods, function (index, methodName) {
resizer.callbacks[methodName](arg);
expect(console.error).toHaveBeenCalledWith(errorMessage);
//reset spy
......
(function () {
(function (undefined) {
describe('Video FocusGrabber', function () {
var state;
......
(function() {
describe('VideoQualityControl', function() {
var state, videoControl, videoQualityControl, oldOTBD;
function initialize() {
loadFixtures('video.html');
state = new Video('#example');
videoControl = state.videoControl;
videoQualityControl = state.videoQualityControl;
}
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
(function (undefined) {
describe('VideoQualityControl', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
});
describe('constructor', function() {
var oldYT = window.YT;
beforeEach(function() {
window.YT = {
Player: function () { },
PlayerState: oldYT.PlayerState,
ready: function(f){f();}
};
initialize();
});
afterEach(function () {
window.YT = oldYT;
});
it('render the quality control', function() {
var container = videoControl.secondaryControlsEl;
expect(container).toContain('a.quality_control');
});
it('add ARIA attributes to quality control', function () {
var qualityControl = $('a.quality_control');
expect(qualityControl).toHaveAttrs({
'role': 'button',
'title': 'HD off',
'aria-disabled': 'false'
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
});
it('bind the quality control', function() {
var handler = videoQualityControl.toggleQuality;
expect($('a.quality_control')).toHandleWith('click', handler);
});
});
});
describe('constructor', function () {
beforeEach(function () {
state = jasmine.initializePlayer('video.html');
});
it('render the quality control', function () {
var container = state.videoControl.secondaryControlsEl;
expect(container).toContain('a.quality_control');
});
it('add ARIA attributes to quality control', function () {
var qualityControl = $('a.quality_control');
expect(qualityControl).toHaveAttrs({
'role': 'button',
'title': 'HD off',
'aria-disabled': 'false'
});
});
it('bind the quality control', function () {
var handler = state.videoQualityControl.toggleQuality;
expect($('a.quality_control')).toHandleWith('click', handler);
});
});
});
}).call(this);
......@@ -476,8 +476,7 @@ function (VideoPlayer) {
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 <= this.config.startTime
) {
this.config.endTime = null;
}
......@@ -560,16 +559,32 @@ function (VideoPlayer) {
// example the length of the video can be determined from the meta
// data.
function fetchMetadata() {
var _this = this;
var _this = this,
metadataXHRs = [];
this.metadata = {};
$.each(this.videos, function (speed, url) {
_this.getVideoMetadata(url, function (data) {
var xhr = _this.getVideoMetadata(url, function (data) {
if (data.data) {
_this.metadata[data.data.id] = data.data;
}
});
metadataXHRs.push(xhr);
});
$.when.apply(this, metadataXHRs).done(function () {
_this.el.trigger('metadata_received');
// Not only do we trigger the "metadata_received" event, we also
// set a flag to notify that metadata has been received. This
// allows for code that will miss the "metadata_received" event
// to know that metadata has been received. This is important in
// cases when some code will subscribe to the "metadata_received"
// event after it has been triggered.
_this.youtubeMetadataReceived = true;
});
}
......
......@@ -60,7 +60,7 @@ function (HTML5Video, Resizer) {
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
function _initialize(state) {
var youTubeId, player;
var youTubeId, player, duration;
// The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's
......@@ -86,7 +86,7 @@ function (HTML5Video, Resizer) {
// At the start, the initial value of the variable
// `seekToStartTimeOldSpeed` should always differ from the value
// returned by the duration function.
// of `state.speed` variable.
state.videoPlayer.seekToStartTimeOldSpeed = 'void';
state.videoPlayer.playerVars = {
......@@ -134,11 +134,20 @@ function (HTML5Video, Resizer) {
_resize(state, videoWidth, videoHeight);
duration = state.videoPlayer.duration();
state.trigger(
'videoControl.updateVcrVidTime',
{
time: 0,
duration: state.videoPlayer.duration()
duration: duration
}
);
state.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
}, false);
......@@ -166,6 +175,22 @@ function (HTML5Video, Resizer) {
videoHeight = player.attr('height') || player.height();
_resize(state, videoWidth, videoHeight);
// After initialization, update the VCR with total time.
// At this point only the metadata duration is available (not
// very precise), but it is better than having 00:00:00 for
// total time.
if (state.youtubeMetadataReceived) {
// Metadata was already received, and is available.
_updateVcrAndRegion(state);
} else {
// We wait for metadata to arrive, before we request the update
// of the VCR video time, and of the start-end time region.
// Metadata contains duration of the video.
state.el.on('metadata_received', function () {
_updateVcrAndRegion(state);
});
}
});
}
......@@ -174,7 +199,26 @@ function (HTML5Video, Resizer) {
}
}
function _resize (state, videoWidth, videoHeight) {
function _updateVcrAndRegion(state) {
var duration = state.videoPlayer.duration();
state.trigger(
'videoControl.updateVcrVidTime',
{
time: 0,
duration: duration
}
);
state.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
}
function _resize(state, videoWidth, videoHeight) {
state.resizer = new Resizer({
element: state.videoEl,
elementRatio: videoWidth/videoHeight,
......@@ -652,20 +696,12 @@ function (HTML5Video, Resizer) {
}
}
// Rebuild the slider start-end range (if it doesn't take up the
// whole slider). Remember that endTime === null means the end time
// is set to the end of video by default.
if (!(
this.videoPlayer.startTime === 0 &&
this.videoPlayer.endTime === null
)) {
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
}
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
// 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
......@@ -735,10 +771,40 @@ function (HTML5Video, Resizer) {
* This instability is internal to the player API (or browser internals).
*/
function duration() {
var dur = this.videoPlayer.player.getDuration();
var dur;
// Sometimes the YouTube API doesn't finish instantiating all of it's
// methods, but the execution point arrives here.
//
// This happens when you have start and end times set, and click "Edit"
// in Studio, and then "Save". The Video editor dialog closes, the
// video reloads, but the start-end range is not visible.
if (this.videoPlayer.player.getDuration) {
dur = this.videoPlayer.player.getDuration();
}
// For YouTube videos, before the video starts playing, the API
// function player.getDuration() will return 0. This means that the VCR
// will show total time as 0 when the page just loads (before the user
// clicks the Play button).
//
// We can do betterin a case when dur is 0 (or less than 0). We can ask
// the getDuration() function for total time, which will query the
// metadata for a duration.
//
// Be careful! Often the metadata duration is not very precise. It
// might differ by one or two seconds against the actual time as will
// be reported later on by the player.getDuration() API function.
if (!isFinite(dur) || dur <= 0) {
if (this.videoType === 'youtube') {
dur = this.getDuration();
}
}
if (!isFinite(dur)) {
dur = this.getDuration();
// Just in case the metadata is garbled, or something went wrong, we
// have a final check.
if (!isFinite(dur) || dur <= 0) {
dur = 0;
}
return Math.floor(dur);
......
......@@ -99,6 +99,9 @@ function () {
.find('.ui-slider-range.ui-widget-header.ui-slider-range-min');
}
// Rebuild the slider start-end range (if it doesn't take up the
// whole slider). Remember that endTime === null means the end time
// is set to the end of video by default.
function updateStartEndTimeRegion(params) {
var left, width, start, end, duration, rangeParams;
......@@ -110,12 +113,33 @@ function () {
duration = params.duration;
}
start = this.videoPlayer.startTime;
start = this.config.startTime;
end = this.config.endTime;
// 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.videoPlayer.endTime || duration;
if (start > duration) {
start = 0;
} else {
if (this.currentPlayerMode === 'flash') {
start /= Number(this.speed);
}
}
// If end is set to null, or it is greater than the duration of the
// video, then we set it to the end of the video.
if (
end === null || end > duration
) {
end = duration;
} else if (end !== null) {
if (this.currentPlayerMode === 'flash') {
end /= Number(this.speed);
}
}
// Don't build a range if it takes up the whole slider.
if (start === 0 && end === duration) {
return;
}
// Because JavaScript has weird rounding rules when a series of
// mathematical operations are performed in a single statement, we will
......
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