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, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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 Blades: Adds CookieStorage utility for video player that provides convenient
way to work with cookies. way to work with cookies.
......
...@@ -86,7 +86,7 @@ Feature: CMS.Video Component ...@@ -86,7 +86,7 @@ Feature: CMS.Video Component
# 11 # 11
Scenario: When start end end times are specified, a range on slider is shown 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 Make sure captions are closed
And I edit the component And I edit the component
And I open tab "Advanced" And I open tab "Advanced"
......
...@@ -24,7 +24,7 @@ class StubYouTubeServiceTest(unittest.TestCase): ...@@ -24,7 +24,7 @@ class StubYouTubeServiceTest(unittest.TestCase):
self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func' 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): def test_transcript_url_equal(self):
response = requests.get( response = requests.get(
......
...@@ -6,6 +6,8 @@ from .http import StubHttpRequestHandler, StubHttpService ...@@ -6,6 +6,8 @@ from .http import StubHttpRequestHandler, StubHttpService
import json import json
import time import time
import requests import requests
from urlparse import urlparse
from collections import OrderedDict
class StubYouTubeHandler(StubHttpRequestHandler): class StubYouTubeHandler(StubHttpRequestHandler):
...@@ -54,14 +56,17 @@ class StubYouTubeHandler(StubHttpRequestHandler): ...@@ -54,14 +56,17 @@ class StubYouTubeHandler(StubHttpRequestHandler):
self.send_response(404) self.send_response(404)
elif 'test_youtube' in self.path: 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: else:
self.send_response( self.send_response(
404, content="Unused url", headers={'Content-type': 'text/plain'} 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. Send message back to the client for video player requests.
Requires sending back callback id. Requires sending back callback id.
...@@ -71,7 +76,14 @@ class StubYouTubeHandler(StubHttpRequestHandler): ...@@ -71,7 +76,14 @@ class StubYouTubeHandler(StubHttpRequestHandler):
# Construct the response content # Construct the response content
callback = self.get_params['callback'][0] 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.send_response(200, content=response, headers={'Content-type': 'text/html'})
self.log_message("Youtube: sent response {}".format(message)) 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 () { describe('VideoPlayer Events', function () {
var state, videoPlayer, player, videoControl, videoCaption, var state, oldOTBD;
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);
});
return obj; describe('HTML5', function () {
}()); beforeEach(function () {
} oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
function initializeYouTube() { .createSpy('onTouchBasedDevice')
initialize('video.html'); .andReturn(null);
}
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();
}
};
});
afterEach(function () { jasmine.stubRequests();
$('source').remove();
window.onTouchBasedDevice = oldOTBD; state = jasmine.initializePlayer();
window.YT = this.oldYT;
});
it('initialize', function(){ state.videoEl = $('video, iframe');
runs(function () {
initialize();
}); });
waitsFor(function () { afterEach(function () {
return state.el.hasClass('is-initialized'); $('source').remove();
}, 'Player is not initialized.', WAIT_TIMEOUT); window.onTouchBasedDevice = oldOTBD;
});
it('initialize', function () {
waitsFor(function () {
return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
runs(function () { runs(function () {
expect('initialize').not.toHaveBeenTriggeredOn('.video'); expect('initialize').not.toHaveBeenTriggeredOn('.video');
});
}); });
});
it('ready', function() { it('ready', function () {
runs(function () { waitsFor(function () {
initialize(); return state.el.hasClass('is-initialized');
}, 'Player is not initialized.', WAIT_TIMEOUT);
runs(function () {
expect('ready').not.toHaveBeenTriggeredOn('.video');
});
}); });
waitsFor(function () { it('play', function () {
return state.el.hasClass('is-initialized'); state.videoPlayer.play();
}, 'Player is not initialized.', WAIT_TIMEOUT); expect('play').not.toHaveBeenTriggeredOn('.video');
});
runs(function () { it('pause', function () {
expect('ready').not.toHaveBeenTriggeredOn('.video'); state.videoPlayer.play();
state.videoPlayer.pause();
expect('pause').not.toHaveBeenTriggeredOn('.video');
}); });
});
it('play', function() { it('volumechange', function () {
initialize(); state.videoPlayer.onVolumeChange(60);
videoPlayer.play();
expect('play').not.toHaveBeenTriggeredOn('.video');
});
it('pause', function() { expect('volumechange').not.toHaveBeenTriggeredOn('.video');
initialize(); });
videoPlayer.play();
videoPlayer.pause();
expect('pause').not.toHaveBeenTriggeredOn('.video');
});
it('volumechange', function() { it('speedchange', function () {
initialize(); state.videoPlayer.onSpeedChange('2.0');
videoPlayer.onVolumeChange(60);
expect('volumechange').not.toHaveBeenTriggeredOn('.video'); expect('speedchange').not.toHaveBeenTriggeredOn('.video');
}); });
it('speedchange', function() { it('seek', function () {
initialize(); state.videoPlayer.onCaptionSeek({
videoPlayer.onSpeedChange('2.0'); time: 1,
type: 'any'
});
expect('speedchange').not.toHaveBeenTriggeredOn('.video'); expect('seek').not.toHaveBeenTriggeredOn('.video');
}); });
it('qualitychange', function() { it('ended', function () {
initializeYouTube(); state.videoPlayer.onEnded();
videoPlayer.onPlaybackQualityChange();
expect('qualitychange').not.toHaveBeenTriggeredOn('.video'); expect('ended').not.toHaveBeenTriggeredOn('.video');
});
}); });
it('seek', function() { describe('YouTube', function () {
initialize(); beforeEach(function () {
videoPlayer.onCaptionSeek({ oldOTBD = window.onTouchBasedDevice;
time: 1, window.onTouchBasedDevice = jasmine
type: 'any' .createSpy('onTouchBasedDevice')
.andReturn(null);
jasmine.stubRequests();
state = jasmine.initializePlayerYouTube();
}); });
expect('seek').not.toHaveBeenTriggeredOn('.video'); afterEach(function () {
}); $('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
it('ended', function() { it('qualitychange', function () {
initialize(); state.videoPlayer.onPlaybackQualityChange();
videoPlayer.onEnded();
expect('ended').not.toHaveBeenTriggeredOn('.video'); expect('qualitychange').not.toHaveBeenTriggeredOn('.video');
});
}); });
}); });
}).call(this); }).call(this);
(function () { (function (undefined) {
describe('Video', function () { describe('Video', function () {
var oldOTBD; var oldOTBD;
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
}); });
afterEach(function () { afterEach(function () {
window.OldVideoPlayer = undefined;
$('source').remove(); $('source').remove();
}); });
...@@ -30,10 +29,6 @@ ...@@ -30,10 +29,6 @@
expect(this.state.videoType).toEqual('youtube'); expect(this.state.videoType).toEqual('youtube');
}); });
it('reset the current video player', function () {
expect(window.OldVideoPlayer).toBeUndefined();
});
it('set the elements', function () { it('set the elements', function () {
expect(this.state.el).toBe('#video_id'); expect(this.state.el).toBe('#video_id');
}); });
...@@ -76,10 +71,6 @@ ...@@ -76,10 +71,6 @@
expect(state.videoType).toEqual('html5'); expect(state.videoType).toEqual('html5');
}); });
it('reset the current video player', function () {
expect(window.OldVideoPlayer).toBeUndefined();
});
it('set the elements', function () { it('set the elements', function () {
expect(state.el).toBe('#video_id'); expect(state.el).toBe('#video_id');
}); });
......
(function (requirejs, require, define) { (function (requirejs, require, define, undefined) {
require( require(
['video/00_resizer.js'], ['video/00_resizer.js'],
...@@ -104,7 +104,7 @@ function (Resizer) { ...@@ -104,7 +104,7 @@ function (Resizer) {
beforeEach(function () { beforeEach(function () {
var spiesCount = _.range(3); var spiesCount = _.range(3);
spiesList = $.map(spiesCount, function() { spiesList = $.map(spiesCount, function () {
return jasmine.createSpy(); return jasmine.createSpy();
}); });
...@@ -113,13 +113,13 @@ function (Resizer) { ...@@ -113,13 +113,13 @@ function (Resizer) {
it('callbacks are called', function () { it('callbacks are called', function () {
$.each(spiesList, function(index, spy) { $.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy); resizer.callbacks.add(spy);
}); });
resizer.align(); resizer.align();
$.each(spiesList, function(index, spy) { $.each(spiesList, function (index, spy) {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
}); });
...@@ -135,20 +135,20 @@ function (Resizer) { ...@@ -135,20 +135,20 @@ function (Resizer) {
}); });
it('All callbacks are removed', function () { it('All callbacks are removed', function () {
$.each(spiesList, function(index, spy) { $.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy); resizer.callbacks.add(spy);
}); });
resizer.callbacks.removeAll(); resizer.callbacks.removeAll();
resizer.align(); resizer.align();
$.each(spiesList, function(index, spy) { $.each(spiesList, function (index, spy) {
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
}); });
}); });
it('Specific callback is removed', function () { it('Specific callback is removed', function () {
$.each(spiesList, function(index, spy) { $.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy); resizer.callbacks.add(spy);
}); });
...@@ -158,14 +158,17 @@ function (Resizer) { ...@@ -158,14 +158,17 @@ function (Resizer) {
expect(spiesList[1]).not.toHaveBeenCalled(); 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'], var methods = ['add', 'once'],
errorMessage = 'TypeError: Argument is not a function.', errorMessage = 'TypeError: Argument is not a function.',
arg = {}; arg = {};
spyOn(console, 'error'); spyOn(console, 'error');
$.each(methods, function(index, methodName) { $.each(methods, function (index, methodName) {
resizer.callbacks[methodName](arg); resizer.callbacks[methodName](arg);
expect(console.error).toHaveBeenCalledWith(errorMessage); expect(console.error).toHaveBeenCalledWith(errorMessage);
//reset spy //reset spy
......
(function () { (function (undefined) {
describe('Video FocusGrabber', function () { describe('Video FocusGrabber', function () {
var state; var state;
......
(function() { (function (undefined) {
describe('VideoQualityControl', function() { describe('VideoQualityControl', function () {
var state, videoControl, videoQualityControl, oldOTBD; var state, oldOTBD;
function initialize() { beforeEach(function () {
loadFixtures('video.html'); oldOTBD = window.onTouchBasedDevice;
state = new Video('#example'); window.onTouchBasedDevice = jasmine
videoControl = state.videoControl; .createSpy('onTouchBasedDevice')
videoQualityControl = state.videoQualityControl; .andReturn(null);
} });
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() { afterEach(function () {
var oldYT = window.YT; $('source').remove();
window.onTouchBasedDevice = oldOTBD;
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'
}); });
});
it('bind the quality control', function() { describe('constructor', function () {
var handler = videoQualityControl.toggleQuality; beforeEach(function () {
expect($('a.quality_control')).toHandleWith('click', handler); 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); }).call(this);
...@@ -476,8 +476,7 @@ function (VideoPlayer) { ...@@ -476,8 +476,7 @@ function (VideoPlayer) {
this.config.endTime = parseInt(this.config.endTime, 10); this.config.endTime = parseInt(this.config.endTime, 10);
if ( if (
!isFinite(this.config.endTime) || !isFinite(this.config.endTime) ||
this.config.endTime < this.config.startTime || this.config.endTime <= this.config.startTime
this.config.endTime === 0
) { ) {
this.config.endTime = null; this.config.endTime = null;
} }
...@@ -560,16 +559,32 @@ function (VideoPlayer) { ...@@ -560,16 +559,32 @@ function (VideoPlayer) {
// example the length of the video can be determined from the meta // example the length of the video can be determined from the meta
// data. // data.
function fetchMetadata() { function fetchMetadata() {
var _this = this; var _this = this,
metadataXHRs = [];
this.metadata = {}; this.metadata = {};
$.each(this.videos, function (speed, url) { $.each(this.videos, function (speed, url) {
_this.getVideoMetadata(url, function (data) { var xhr = _this.getVideoMetadata(url, function (data) {
if (data.data) { if (data.data) {
_this.metadata[data.data.id] = 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) { ...@@ -60,7 +60,7 @@ function (HTML5Video, Resizer) {
// via the 'state' object. Much easier to work this way - you don't // via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects. // have to do repeated jQuery element selects.
function _initialize(state) { function _initialize(state) {
var youTubeId, player; var youTubeId, player, duration;
// The function is called just once to apply pre-defined configurations // The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's // by student before video starts playing. Waits until the video's
...@@ -86,7 +86,7 @@ function (HTML5Video, Resizer) { ...@@ -86,7 +86,7 @@ function (HTML5Video, Resizer) {
// At the start, the initial value of the variable // At the start, the initial value of the variable
// `seekToStartTimeOldSpeed` should always differ from the value // `seekToStartTimeOldSpeed` should always differ from the value
// returned by the duration function. // of `state.speed` variable.
state.videoPlayer.seekToStartTimeOldSpeed = 'void'; state.videoPlayer.seekToStartTimeOldSpeed = 'void';
state.videoPlayer.playerVars = { state.videoPlayer.playerVars = {
...@@ -134,11 +134,20 @@ function (HTML5Video, Resizer) { ...@@ -134,11 +134,20 @@ function (HTML5Video, Resizer) {
_resize(state, videoWidth, videoHeight); _resize(state, videoWidth, videoHeight);
duration = state.videoPlayer.duration();
state.trigger( state.trigger(
'videoControl.updateVcrVidTime', 'videoControl.updateVcrVidTime',
{ {
time: 0, time: 0,
duration: state.videoPlayer.duration() duration: duration
}
);
state.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
} }
); );
}, false); }, false);
...@@ -166,6 +175,22 @@ function (HTML5Video, Resizer) { ...@@ -166,6 +175,22 @@ function (HTML5Video, Resizer) {
videoHeight = player.attr('height') || player.height(); videoHeight = player.attr('height') || player.height();
_resize(state, videoWidth, videoHeight); _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) { ...@@ -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({ state.resizer = new Resizer({
element: state.videoEl, element: state.videoEl,
elementRatio: videoWidth/videoHeight, elementRatio: videoWidth/videoHeight,
...@@ -652,20 +696,12 @@ function (HTML5Video, Resizer) { ...@@ -652,20 +696,12 @@ function (HTML5Video, Resizer) {
} }
} }
// Rebuild the slider start-end range (if it doesn't take up the this.trigger(
// whole slider). Remember that endTime === null means the end time 'videoProgressSlider.updateStartEndTimeRegion',
// is set to the end of video by default. {
if (!( duration: duration
this.videoPlayer.startTime === 0 && }
this.videoPlayer.endTime === null );
)) {
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
}
// If this is not a duration change (if it is, we continue playing // 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 // from current time), then we need to seek the video to the start
...@@ -735,10 +771,40 @@ function (HTML5Video, Resizer) { ...@@ -735,10 +771,40 @@ function (HTML5Video, Resizer) {
* This instability is internal to the player API (or browser internals). * This instability is internal to the player API (or browser internals).
*/ */
function duration() { 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)) { // Just in case the metadata is garbled, or something went wrong, we
dur = this.getDuration(); // have a final check.
if (!isFinite(dur) || dur <= 0) {
dur = 0;
} }
return Math.floor(dur); return Math.floor(dur);
......
...@@ -99,6 +99,9 @@ function () { ...@@ -99,6 +99,9 @@ function () {
.find('.ui-slider-range.ui-widget-header.ui-slider-range-min'); .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) { function updateStartEndTimeRegion(params) {
var left, width, start, end, duration, rangeParams; var left, width, start, end, duration, rangeParams;
...@@ -110,12 +113,33 @@ function () { ...@@ -110,12 +113,33 @@ function () {
duration = params.duration; 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 if (start > duration) {
// know that start is not a the beginning, therefore we must build a start = 0;
// range. } else {
end = this.videoPlayer.endTime || duration; 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 // Because JavaScript has weird rounding rules when a series of
// mathematical operations are performed in a single statement, we will // 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