diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 2f22ba400f..7d37c193a5 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -78,6 +78,9 @@ class CourseMetadata(object): if not settings.FEATURES.get('ENABLE_TEAMS'): filtered_list.append('teams_configuration') + if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): + filtered_list.append('video_bumper') + return filtered_list @classmethod diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index df8d0b73b1..dcd120abf1 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True # Enable custom content licensing FEATURES['LICENSING'] = True +FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio +FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings + ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True diff --git a/cms/envs/common.py b/cms/envs/common.py index 39a3008c30..f8d9e27354 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -163,6 +163,13 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': False, + + # Show video bumper in Studio + 'ENABLE_VIDEO_BUMPER': False, + + # How many seconds to show the bumper again, default is 7 days: + 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, + } ENABLE_JASMINE = False @@ -645,6 +652,8 @@ YOUTUBE = { 'v': 'set_youtube_id_of_11_symbols_here', }, }, + + 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 } ############################# VIDEO UPLOAD PIPELINE ############################# diff --git a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss index 3c283df770..05739ef2ad 100644 --- a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss +++ b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss @@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%); } .a11y-menu-list { - @extend %ui-depth1; + @extend %ui-depth3; top: 100%; margin: 0; padding: 0; diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 8bbfdd1b25..eb7ae84149 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -27,6 +27,23 @@ div.video { } } + // CASE: video pre-roll state + &.is-pre-roll { + .slider { + visibility: hidden; + } + + .video-player { + position: relative; + &:before { + display: block; + content: ""; + width: 100%; + padding-top: 55%; + } + } + } + div.tc-wrapper { @include clearfix(); position: relative; @@ -169,6 +186,7 @@ div.video { } object, iframe, video { + display: block; border: none; width: 100%; } @@ -282,7 +300,7 @@ div.video { } } - ul.vcr { + .vcr { float: left; list-style: none; margin: 0 lh() 0 0; @@ -293,49 +311,52 @@ div.video { font-size: em(14); } - li { + .video_control { + @extend %video-button; float: left; - margin-bottom: 0; + background-image: url('../images/vcr.png'); + background-position: 15px 15px ; + background-repeat: no-repeat; + border-left: none; + padding: 0 lh(.75); + width: 14px; - a { - @extend %video-button; - background-image: url('../images/vcr.png'); - background-position: 15px 15px ; - background-repeat: no-repeat; - border-left: none; - box-shadow: 1px 0 0 #555; - padding: 0 lh(.75); - width: 14px; - - &:focus { - @extend %ui-depth4; - position: relative; - outline: $white dotted thin; - outline-offset: -2px; - } - - &:empty { - height: 46px; - background-position: 15px 15px; - } - - &.play { - background-position: 17px -114px; - } - - &.pause { - background-position: 16px -50px; - } + &:focus { + @extend %ui-depth4; + position: relative; + outline: $white dotted thin; + outline-offset: -2px; } - div.vidtime { - font-weight: bold; - line-height: 46px; //height of play pause buttons - -webkit-font-smoothing: antialiased; - padding-left: lh(.75); - @media (max-width: 1120px) { - padding-left: lh(0.5); - } + &:empty { + height: 46px; + background-position: 15px 15px; + } + + &.play { + background-position: 17px -114px; + } + + &.pause { + background-position: 16px -50px; + } + + &.skip { + background-image: none; + text-indent: 0; + width: initial; + white-space: nowrap; + } + } + + div.vidtime { + @extend %t-strong; + float: left; + line-height: 46px; //height of play pause buttons + -webkit-font-smoothing: antialiased; + padding-left: lh(.75); + @media (max-width: 1120px) { + padding-left: lh(0.5); } } } @@ -504,11 +525,14 @@ div.video { background-image: url('../images/volume.png'); background-position: 10px center; background-repeat: no-repeat; - border-left: none; width: 30px; height: 46px; } + &:not(:first-child) > a { + border-left: none; + } + .volume-slider-container { @include transition(none); @extend %ui-depth1; @@ -686,8 +710,7 @@ div.video { } ol.subtitles { - width: 0; - height: 0; + @extend .is-hidden; } ol.subtitles.html5 { @@ -792,13 +815,38 @@ div.video { &.is-touch { div.tc-wrapper { article.video-wrapper { - object, iframe, video{ + object, iframe, video { width: 100%; height: 100%; } } } } + + .video-pre-roll { + @extend %ui-depth3; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: 100%; + background-color: $black; + + &.is-html5 { + background-size: 15%; + } + + .btn-play { + text-indent: -999px; + overflow: hidden; + border: none; + box-shadow: none; + line-height: 0; + } + } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/poster.jpg b/common/lib/xmodule/xmodule/js/fixtures/poster.jpg new file mode 100644 index 0000000000..7f987f944e Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/poster.jpg differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index e1fe11ae47..dabb3801b9 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -4,22 +4,7 @@
@@ -35,35 +20,11 @@ - -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html index 1b0727d3d7..617d958357 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html @@ -4,23 +4,7 @@
@@ -36,35 +20,11 @@ - -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index 678803a90d..47be4f04fc 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -4,23 +4,7 @@
@@ -33,8 +17,6 @@ - -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html index a34df976bf..77017d403d 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html @@ -4,22 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html new file mode 100644 index 0000000000..22bd206268 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html @@ -0,0 +1,36 @@ +
+
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index 8086c2b269..8842b1e592 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -4,22 +4,7 @@
@@ -35,35 +20,11 @@ - -
@@ -77,20 +38,7 @@
@@ -142,20 +68,7 @@
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js index f188c9c639..97d422d5d8 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.js +++ b/common/lib/xmodule/xmodule/js/spec/helper.js @@ -206,6 +206,9 @@ }, toBeInArray: function (array) { return $.inArray(this.actual, array) > -1; + }, + toBeFocused: function () { + return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement; } }); @@ -239,12 +242,11 @@ loadFixtures('video_all.html'); } - // If `params` is an object, assign it's properties as data attributes + // If `params` is an object, assign its properties as data attributes // to the main video DIV element. if (_.isObject(params)) { - $('#example') - .find('#video_id') - .data(params); + var metadata = _.extend($('#video_id').data('metadata'), params); + $('#video_id').data('metadata', metadata); } jasmine.stubRequests(); diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index 72fc269fd8..7cefab5e49 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -1,6 +1,6 @@ (function (undefined) { describe('Video', function () { - var oldOTBD; + var oldOTBD, state; beforeEach(function () { jasmine.stubRequests(); @@ -17,11 +17,12 @@ beforeEach(function () { loadFixtures('video.html'); $.cookie.andReturn('0.50'); + this.state = jasmine.initializePlayerYouTube('video_html5.html'); }); describe('by default', function () { - beforeEach(function () { - this.state = new window.Video('#example'); + afterEach(function () { + this.state.videoPlayer.destroy(); }); it('check videoType', function () { @@ -54,19 +55,16 @@ var state; beforeEach(function () { - loadFixtures('video_html5.html'); $.cookie.andReturn('0.75'); + state = jasmine.initializePlayer('video_html5.html'); + }); + + afterEach(function () { + state.videoPlayer.destroy(); + state = undefined; }); describe('by default', function () { - beforeEach(function () { - state = new window.Video('#example'); - }); - - afterEach(function () { - state = undefined; - }); - it('check videoType', function () { expect(state.videoType).toEqual('html5'); }); @@ -95,14 +93,6 @@ // 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'); - }); - - afterEach(function () { - state = null; - }); - it('create the Video Player', function () { expect(state.videoPlayer.player).not.toBeUndefined(); }); @@ -113,8 +103,11 @@ describe('YouTube API is not loaded', function () { beforeEach(function () { window.YT = undefined; + state = jasmine.initializePlayerYouTube(); + }) - state = jasmine.initializePlayerYouTube('video.html'); + afterEach(function () { + state.videoPlayer.destroy(); }); it('callback, to be called after YouTube API loads, exists and is called', function () { @@ -159,9 +152,8 @@ } ]; - beforeEach(function () { - loadFixtures('video.html'); - + afterEach(function () { + state.videoPlayer.destroy(); }); $.each(miniTestSuite, function (index, test) { @@ -172,13 +164,10 @@ function itFabrique(itDescription, data, expectData) { it(itDescription, function () { - $('#example').find('.video') - .data({ - 'start': data.start, - 'end': data.end - }); - - state = new Video('#example'); + state = jasmine.initializePlayer('video.html', { + 'start': data.start, + 'end': data.end + }); expect(state.config.startTime).toBe(expectData.start); expect(state.config.endTime).toBe(expectData.end); @@ -238,26 +227,5 @@ expect(numAjaxCalls).toBe(1); }); }); - - describe('log', function () { - beforeEach(function () { - loadFixtures('video_html5.html'); - state = new Video('#example'); - spyOn(Logger, 'log'); - state.videoPlayer.log('someEvent', { - currentTime: 25, - speed: '1.0' - }); - }); - - it('call the logger with valid extra parameters', function () { - expect(Logger.log).toHaveBeenCalledWith('someEvent', { - id: 'id', - code: 'html5', - currentTime: 25, - speed: '1.0' - }); - }); - }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js index baefb4dea1..b22cdd375e 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js @@ -10,8 +10,8 @@ afterEach(function () { state.storage.clear(); + state.videoPlayer.destroy(); $.fn.scrollTo.reset(); - $('.subtitles').remove(); $('source').remove(); window.onTouchBasedDevice = oldOTBD; }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js index e08ba56a75..f3194b8bce 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js @@ -12,158 +12,6 @@ function (Initialize) { state = {}; }); - describe('saveState function', function () { - var videoPlayerCurrentTime, newCurrentTime, speed; - - // We make sure that `currentTime` is a float. We need to test - // that Math.round() is called. - videoPlayerCurrentTime = 3.1242; - - // We have two times, because one is stored in - // `videoPlayer.currentTime`, and the other is passed directly to - // `saveState` in `data` object. In each case, there is different - // code that handles these times. They have to be different for - // test completeness sake. Also, make sure it is float, as is the - // time above. - newCurrentTime = 5.4; - - speed = '0.75'; - - beforeEach(function () { - state = { - videoPlayer: { - currentTime: videoPlayerCurrentTime - }, - storage: { - setItem: jasmine.createSpy() - }, - config: { - saveStateUrl: 'http://example.com/save_user_state' - } - }; - - spyOn($, 'ajax'); - spyOn(Time, 'formatFull').andCallThrough(); - }); - - it('data is not an object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: videoPlayerCurrentTime, - data: undefined, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) - } - }); - }); - - it('data contains speed, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: undefined, - data: { - speed: speed - }, - ajaxData: { - speed: speed - } - }); - }); - - it('data contains float position, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: newCurrentTime, - data: { - saved_video_position: newCurrentTime - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains speed and rounded position, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: Math.round(newCurrentTime), - data: { - speed: speed, - saved_video_position: Math.round(newCurrentTime) - }, - ajaxData: { - speed: speed, - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains empty object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: undefined, - data: {}, - ajaxData: {} - }); - }); - - it('data contains position 0, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: 0, - data: { - saved_video_position: 0 - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(0)) - } - }); - }); - - return; - - function itSpec(value) { - var asyncVal = value.asyncVal, - speedVal = value.speedVal, - positionVal = value.positionVal, - data = value.data, - ajaxData = value.ajaxData; - - Initialize.prototype.saveState.call(state, asyncVal, data); - - if (speedVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'speed', - speedVal, - true - ); - } - if (positionVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'savedVideoPosition', - positionVal, - true - ); - expect(Time.formatFull).toHaveBeenCalledWith( - positionVal - ); - } - expect($.ajax).toHaveBeenCalledWith({ - url: state.config.saveStateUrl, - type: 'POST', - async: asyncVal, - dataType: 'json', - data: ajaxData - }); - } - }); - describe('getCurrentLanguage', function () { var msg; @@ -356,20 +204,12 @@ function (Initialize) { describe('when new speed is available', function () { beforeEach(function () { - Initialize.prototype.setSpeed.call(state, '0.75', true); + Initialize.prototype.setSpeed.call(state, '0.75'); }); it('set new speed', function () { expect(state.speed).toEqual('0.75'); }); - - it('save setting for new speed', function () { - expect(state.storage.setItem.calls[0].args) - .toEqual(['speed', '0.75', true]); - - expect(state.storage.setItem.calls[1].args) - .toEqual(['general_speed', '0.75']); - }); }); describe('when new speed is not available', function () { @@ -390,7 +230,7 @@ function (Initialize) { }; $.each(map, function(key, expected) { - Initialize.prototype.setSpeed.call(state, key, true); + Initialize.prototype.setSpeed.call(state, key); expect(state.speed).toBe(expected); }); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js index feb332122c..a790f8ecae 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js @@ -5,6 +5,7 @@ afterEach(function () { $('source').remove(); state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -56,24 +57,6 @@ }); */ }); - - it('add ARIA attributes to button, menu, and menu items links', - function () { - expect(button).toHaveAttrs({ - 'role': 'button', - 'title': '.srt', - 'aria-disabled': 'false' - }); - - expect(menuList).toHaveAttr('role', 'menu'); - - menuItemsLinks.each(function(){ - expect($(this)).toHaveAttrs({ - 'role': 'menuitem', - 'aria-disabled': 'false' - }); - }); - }); }); describe('when running', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js new file mode 100644 index 0000000000..0a355e918d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js @@ -0,0 +1,109 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoBumper', function () { + var state, oldOTBD, waitForPlaying; + + waitForPlaying = function (state) { + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + jasmine.Clock.useMock(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the bumper video', function () { + expect($('.is-bumper')).toExist(); + }); + + it('can show the main video on error', function () { + state.el.trigger('error'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video once bumper ends', function () { + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video on skip', function () { + state.bumperState.videoBumper.skip(); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can stop the bumper video playing if it is too long', function () { + state.el.trigger('timeupdate', [state.bumperState.videoBumper.maxBumperDuration + 1]); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can save appropriate states correctly on ended', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skip(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on error', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('error'); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip and do not show again', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skipAndDoNotShowAgain(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true, bumper_do_not_show_again: true}); + }); + + it('can destroy itself', function () { + state.bumperState.videoBumper.destroy(); + expect(state.videoBumper).toBeUndefined(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index 39778d7ba6..f269bde541 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -11,14 +11,13 @@ }); afterEach(function () { - $('.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(); $.fn.scrollTo.reset(); state.storage.clear(); + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); @@ -121,11 +120,6 @@ }); }); - it('bind the hide caption button', function () { - state = jasmine.initializePlayer(); - expect($('.hide-subtitles')).toHandle('click'); - }); - it('bind the mouse movement', function () { state = jasmine.initializePlayer(); expect($('.subtitles')).toHandle('mouseover'); @@ -143,6 +137,27 @@ }); + it('can destroy itself', function () { + spyOn($, 'ajaxWithPrefix'); + state = jasmine.initializePlayer(); + var plugin = state.videoCaption; + + spyOn($.fn, 'off').andCallThrough(); + state.videoCaption.destroy(); + + expect(state.videoCaption).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'caption:fetch': plugin.fetchCaption, + 'caption:resize': plugin.onResize, + 'caption:update': plugin.onCaptionUpdate, + 'ended': plugin.pause, + 'fullscreen': plugin.onResize, + 'pause': plugin.pause, + 'play': plugin.play, + 'destroy': plugin.destroy + }); + }); + describe('renderLanguageMenu', function () { describe('is rendered', function () { it('if languages more than 1', function () { @@ -593,7 +608,7 @@ it(msg, function () { spyOn(Caption, 'fetchAvailableTranslations'); $.ajax.andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = {}; @@ -612,7 +627,7 @@ xit(msg, function () { $.ajax .andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = { @@ -690,7 +705,7 @@ msg = 'on error: captions are hidden if there are no transcript'; it(msg, function () { $.ajax.andCallFake(function (settings) { - settings.error(); + _.result(settings, 'error'); }); Caption.fetchAvailableTranslations(); @@ -907,8 +922,8 @@ $('.subtitles').css('maxHeight'), 10 ); videoWrapperHeight = $('.video-wrapper').height(); - progressSliderHeight = videoControl.sliderEl.height(); - controlHeight = videoControl.el.height(); + progressSliderHeight = state.el.find('.slider').height(); + controlHeight = state.el.find('.video-controls').height(); shouldBeHeight = videoWrapperHeight - 0.5 * progressSliderHeight - controlHeight; @@ -1043,7 +1058,6 @@ describe('toggle', function () { beforeEach(function () { state = jasmine.initializePlayer(); - spyOn(state.videoPlayer, 'log'); $('.subtitles li[data-index=1]').addClass('current'); }); @@ -1053,15 +1067,6 @@ state.videoCaption.toggle(jQuery.Event('click')); }); - it('log the hide_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'hide_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('hide the caption', function () { expect(state.el).toHaveClass('closed'); }); @@ -1079,15 +1084,6 @@ jasmine.Clock.useMock(); }); - it('log the show_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'show_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('show the caption', function () { expect(state.el).not.toHaveClass('closed'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js index 269a75053c..295b151a4f 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js @@ -68,6 +68,7 @@ $('source').remove(); _.result(state.storage, 'clear'); _.result($('video').data('contextmenu'), 'destroy'); + _.result(state.videoPlayer, 'destroy'); }); describe('constructor', function () { @@ -219,12 +220,13 @@ it('mouse left/right-clicking behaves as expected on play/pause menu item', function () { var menuItem = menuItems.first(); + spyOn(state.videoPlayer, 'isPlaying'); spyOn(state.videoPlayer, 'play').andCallFake(function () { - state.videoControl.isPlaying = true; + state.videoPlayer.isPlaying.andReturn(true); state.el.trigger('play'); }); spyOn(state.videoPlayer, 'pause').andCallFake(function () { - state.videoControl.isPlaying = false; + state.videoPlayer.isPlaying.andReturn(false); state.el.trigger('pause'); }); // Left-click on play diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js index 98569620c2..85794dc2d5 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js @@ -13,12 +13,13 @@ afterEach(function () { $('source').remove(); state.storage.clear(); - window.Video.previousState = null; + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); describe('constructor', function () { beforeEach(function () { + window.VideoState = {}; state = jasmine.initializePlayer(); }); @@ -28,83 +29,13 @@ '.slider', 'ul.vcr', 'a.play', - '.vidtime', - '.add-fullscreen' + '.vidtime' ].join(',') ); expect($('.video-controls').find('.vidtime')) .toHaveText('0:00 / 0:00'); }); - - it('add ARIA attributes to time control', function () { - var timeControl = $('div.slider > a'); - - expect(timeControl).toHaveAttrs({ - 'role': 'slider', - 'title': 'Video position', - 'aria-disabled': 'false' - }); - - expect(timeControl).toHaveAttr('aria-valuetext'); - }); - - it('add ARIA attributes to play control', function () { - var playControl = $('ul.vcr a'); - - expect(playControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Play', - 'aria-disabled': 'false' - }); - }); - - it('add ARIA attributes to fullscreen control', function () { - var fullScreenControl = $('a.add-fullscreen'); - - expect(fullScreenControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Fill browser', - 'aria-disabled': 'false' - }); - }); - - it('bind the playback button', function () { - expect($('.video_control')).toHandleWith( - 'click', - state.videoControl.togglePlayback - ); - }); - - describe('when on a non-touch based device', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - it('add the play class to video control', function () { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); - - describe('when on a touch based device', function () { - beforeEach(function () { - window.onTouchBasedDevice.andReturn(['iPad']); - state = jasmine.initializePlayer(); - }); - - it( - 'does not add the play class to video control', - function () - { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); }); describe('constructor with start-time', function () { @@ -115,6 +46,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 0 @@ -147,6 +79,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 15 @@ -181,6 +114,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: -15 @@ -215,6 +149,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 'a' @@ -249,6 +184,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 10000 @@ -278,13 +214,14 @@ 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; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 0 @@ -319,6 +256,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 15 @@ -353,6 +291,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: -15 @@ -387,6 +326,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 'a' @@ -422,6 +362,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 10000 @@ -457,6 +398,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -492,6 +434,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -527,6 +470,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -562,6 +506,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -597,6 +542,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -625,217 +571,8 @@ }); }); - it('Controls height is actual on switch to fullscreen', function () { - spyOn($.fn, 'height').andCallFake(function (val) { - return _.isUndefined(val) ? 100: this; - }); - - state = jasmine.initializePlayer(); - $(state.el).trigger('fullscreen'); - - expect(state.videoControl.height).toBe(150); - }); - - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.play(); - }); - - it('switch playback button to play state', function () { - expect($('.video_control')).not.toHaveClass('play'); - expect($('.video_control')).toHaveClass('pause'); - expect($('.video_control')).toHaveAttr('title', 'Pause'); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.pause(); - }); - - it('switch playback button to pause state', function () { - expect($('.video_control')).not.toHaveClass('pause'); - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr('title', 'Play'); - }); - }); - - describe('togglePlayback', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - describe( - 'when the control does not have play or pause class', - function () - { - beforeEach(function () { - $('.video_control').removeClass('play') - .removeClass('pause'); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - $('.video_control').addClass('play'); - spyOnEvent(state.videoControl, 'pause'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the pause event', function () { - expect('pause').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - - describe('when the video is paused', function () { - beforeEach(function () { - $('.video_control').addClass('pause'); - spyOnEvent(state.videoControl, 'play'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the play event', function () { - expect('play').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - }); - }); - - describe('Play placeholder', function () { - var cases = [ - { - name: 'PC', - isShown: false, - isTouch: null - }, { - name: 'iPad', - isShown: true, - isTouch: ['iPad'] - }, { - name: 'Android', - isShown: true, - isTouch: ['Android'] - }, { - name: 'iPhone', - isShown: false, - isTouch: ['iPhone'] - } - ]; - - beforeEach(function () { - jasmine.stubRequests(); - - spyOn(window.YT, 'Player').andCallThrough(); - }); - - it ('works correctly on calling proper methods', function () { - var btnPlay; - - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.showPlayPlaceholder(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'false', - 'tabindex': 0 - }); - - state.videoControl.hidePlayPlaceholder(); - - expect(btnPlay).toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'true', - 'tabindex': -1 - }); - }); - - $.each(cases, function (index, data) { - var message = [ - (data.isShown) ? 'is' : 'is not', - ' shown on', - data.name - ].join(''); - - it(message, function () { - var btnPlay; - - window.onTouchBasedDevice.andReturn(data.isTouch); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - if (data.isShown) { - expect(btnPlay).not.toHaveClass('is-hidden'); - } else { - expect(btnPlay).toHaveClass('is-hidden'); - } - }); - }); - - $.each(['iPad', 'Android'], function (index, device) { - it( - 'is shown on paused video on ' + device + - ' in HTML5 player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - }); - - it( - 'is hidden on playing video on ' + device + - ' in HTML5 player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - - it( - 'is hidden on paused video on ' + device + - ' in YouTube player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayerYouTube(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - }); - }); - it('show', function () { var controls; - state = jasmine.initializePlayer(); controls = state.el.find('.video-controls'); controls.addClass('is-hidden'); @@ -843,5 +580,23 @@ state.videoControl.show(); expect(controls).not.toHaveClass('is-hidden'); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoControl.destroy(); + expect(state.videoControl).toBeUndefined(); + }); + + it('can focus the first control', function () { + var btnPlay; + state = jasmine.initializePlayer({focusFirstControl: true}); + btnPlay = state.el.find('.video-controls .play'); + waitsFor(function () { + return state.el.hasClass('is-initialized'); + }, 'Player is not initialized', WAIT_TIMEOUT); + runs(function () { + expect(btnPlay).toBeFocused(); + }); + }); }); }).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js new file mode 100644 index 0000000000..e41b40e782 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js @@ -0,0 +1,157 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events Bumper plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer('video_with_bumper.html'); + spyOn(Logger, 'log'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getCurrentTime').andReturn(10); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getDuration').andReturn(20); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "edx.video.bumper.loaded" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.loaded', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.played" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.played', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.stopped" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.skipped" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.skipped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.dismissed" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.dismissed', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.shown" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.hidden" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.shown" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.hidden" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoEventsBumperPlugin; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoEventsBumperPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'ended stop': plugin.onEnded, + 'skip': plugin.onSkip, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js new file mode 100644 index 0000000000..78352e575f --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js @@ -0,0 +1,166 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(Logger, 'log'); + spyOn(state.videoEventsPlugin, 'getCurrentTime').andReturn(10); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "load_video" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('load_video', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "play_video" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('play_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "pause_video" event', function () { + state.el.trigger('pause'); + expect(Logger.log).toHaveBeenCalledWith('pause_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "speed_change_video" event', function () { + state.el.trigger('speedchange', ['2.0', '1.0']); + expect(Logger.log).toHaveBeenCalledWith('speed_change_video', { + id: 'id', + code: 'html5', + current_time: 10, + old_speed: '1.0', + new_speed: '2.0' + }); + }); + + it('can emit "seek_video" event', function () { + state.el.trigger('seek', [1, 0, 'any']); + expect(Logger.log).toHaveBeenCalledWith('seek_video', { + id: 'id', + code: 'html5', + old_time: 0, + new_time: 1, + type: 'any' + }); + }); + + it('can emit "stop_video" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "skip_video" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('skip_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "do_not_show_again_video" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "video_show_cc_menu" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('video_show_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "video_hide_cc_menu" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('video_hide_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "show_transcript" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('show_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can emit "hide_transcript" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('hide_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoEventsPlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoEventsPlugin.destroy(); + expect(state.videoEventsPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'pause': plugin.onPause, + 'ended stop': plugin.onEnded, + 'seek': plugin.onSeek, + 'skip': plugin.onSkip, + 'speedchange': plugin.onSpeedChange, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js index ab3c12df6f..5f69d2c7c7 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js @@ -26,6 +26,7 @@ afterEach(function () { // Turn jQuery animations back on. jQuery.fx.off = true; + state.videoPlayer.destroy(); }); it( diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js new file mode 100644 index 0000000000..215b891f41 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js @@ -0,0 +1,102 @@ +(function () { + 'use strict'; + describe('VideoFullScreen', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + describe('constructor', function () { + beforeEach(function () { + state = jasmine.initializePlayer(); + }); + + it('renders the fullscreen control', function () { + expect($('.add-fullscreen')).toExist(); + expect(state.videoFullScreen.fullScreenState).toBe(false); + }); + + it('correctly adds ARIA attributes to fullscreen control', function () { + var fullScreenControl = $('.add-fullscreen'); + + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly triggers the event handler to toggle fullscreen mode', function () { + spyOn(state.videoFullScreen, 'exit'); + spyOn(state.videoFullScreen, 'enter'); + + state.videoFullScreen.fullScreenState = false; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.enter).toHaveBeenCalled(); + + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.exit).toHaveBeenCalled(); + }); + + it('correctly updates ARIA on state change', function () { + var fullScreenControl = $('.add-fullscreen'); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Exit full browser', + 'aria-disabled': 'false' + }); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly can out of fullscreen by pressing esc', function () { + spyOn(state.videoCommands, 'execute'); + var esc = $.Event('keyup'); + esc.keyCode = 27; + state.isFullScreen = true; + $(document).trigger(esc); + expect(state.videoCommands.execute).toHaveBeenCalledWith('toggleFullScreen'); + }); + + it('can update video dimensions on state change', function () { + state.el.trigger('fullscreen', [true]); + expect(state.resizer.setMode).toHaveBeenCalledWith('both'); + state.el.trigger('fullscreen', [false]); + expect(state.resizer.setMode).toHaveBeenCalledWith('width'); + }); + + it('can destroy itself', function () { + state.videoFullScreen.destroy(); + expect($('.add-fullscreen')).not.toExist(); + expect(state.videoFullScreen).toBeUndefined(); + }); + }); + + it('Controls height is actual on switch to fullscreen', function () { + spyOn($.fn, 'height').andCallFake(function (val) { + return _.isUndefined(val) ? 100: this; + }); + + state = jasmine.initializePlayer(); + $(state.el).trigger('fullscreen'); + + expect(state.videoFullScreen.height).toBe(150); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js new file mode 100644 index 0000000000..877dc9861e --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js @@ -0,0 +1,68 @@ +(function () { + 'use strict'; + describe('VideoPlayPauseControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + spyOn(state.videoSaveStatePlugin, 'saveState'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on play', function () { + state.el.trigger('play'); + expect($('.video_control.pause')).toHaveAttrs({ + 'role': 'button', + 'title': 'Pause', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on video ends', function () { + state.el.trigger('play'); + state.el.trigger('ended'); + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on pause', function () { + state.el.trigger('pause'); + expect(state.videoSaveStatePlugin.saveState).toHaveBeenCalledWith(true); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('togglePlayback'); + }); + + it('can destroy itself', function () { + state.videoPlayPauseControl.destroy(); + expect(state.videoPlayPauseControl).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js new file mode 100644 index 0000000000..d99c12e24c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js @@ -0,0 +1,151 @@ +(function () { + 'use strict'; + describe('VideoPlayPlaceholder', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(['iPad']); + + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + var cases = [ + { + name: 'PC', + isShown: false, + isTouch: null + }, { + name: 'iPad', + isShown: true, + isTouch: ['iPad'] + }, { + name: 'Android', + isShown: true, + isTouch: ['Android'] + }, { + name: 'iPhone', + isShown: false, + isTouch: ['iPhone'] + } + ]; + + beforeEach(function () { + jasmine.stubRequests(); + spyOn(window.YT, 'Player').andCallThrough(); + }); + + it ('works correctly on calling proper methods', function () { + var btnPlay; + + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.videoPlayPlaceholder.show(); + + expect(btnPlay).not.toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'false', + 'tabindex': 0 + }); + + state.videoPlayPlaceholder.hide(); + + expect(btnPlay).toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'true', + 'tabindex': -1 + }); + }); + + $.each(cases, function (index, data) { + var message = [ + (data.isShown) ? 'is' : 'is not', + ' shown on', + data.name + ].join(''); + + it(message, function () { + var btnPlay; + + window.onTouchBasedDevice.andReturn(data.isTouch); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + if (data.isShown) { + expect(btnPlay).not.toHaveClass('is-hidden'); + } else { + expect(btnPlay).toHaveClass('is-hidden'); + } + }); + }); + + $.each(['iPad', 'Android'], function (index, device) { + it( + 'is shown on paused video on ' + device + + ' in HTML5 player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).not.toHaveClass('is-hidden'); + }); + + it( + 'is hidden on playing video on ' + device + + ' in HTML5 player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + + it( + 'is hidden on paused video on ' + device + + ' in YouTube player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayerYouTube(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + }); + + it('starts play the video on click', function () { + $('.btn-play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can destroy itself', function () { + state.videoPlayPlaceholder.destroy(); + expect(state.videoPlayPlaceholder).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js new file mode 100644 index 0000000000..9ccea6a0ab --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js @@ -0,0 +1,64 @@ +(function () { + 'use strict'; + describe('VideoPlaySkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on play', function () { + state.el.trigger('play'); + expect($('.video_control.play')).not.toExist(); + expect($('.video_control.skip')).toExist(); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can skip the video on click', function () { + state.el.trigger('play'); + spyOn(state.bumperState.videoPlayer, 'isPlaying').andReturn(true); + $('.video_control.skip').first().click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip'); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoPlaySkipControl, + el = plugin.el; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + expect(el).not.toExist(); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index da910d7bb0..fc74ef9229 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -1,5 +1,4 @@ (function (requirejs, require, define, undefined) { - 'use strict'; require( @@ -21,6 +20,9 @@ function (VideoPlayer) { if (state.storage) { state.storage.clear(); } + if (state.videoPlayer) { + _.result(state.videoPlayer, 'destroy'); + } }); describe('constructor', function () { @@ -47,7 +49,7 @@ function (VideoPlayer) { expect(state.videoCaption).toBeDefined(); expect(state.speed).toEqual('1.50'); expect(state.config.transcriptTranslationUrl) - .toEqual('/transcript/translation'); + .toEqual('/transcript/translation/__lang__'); }); it('create video speed control', function () { @@ -71,18 +73,15 @@ function (VideoPlayer) { var events; jasmine.stubRequests(); - spyOn(window.YT, 'Player').andCallThrough(); - state = jasmine.initializePlayerYouTube(); - state.videoEl = $('video, iframe'); events = { onReady: state.videoPlayer.onReady, onStateChange: state.videoPlayer.onStateChange, - onPlaybackQualityChange: state.videoPlayer - .onPlaybackQualityChange + onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange, + onError: state.videoPlayer.onError }; expect(YT.Player).toHaveBeenCalledWith('id', { @@ -156,7 +155,7 @@ function (VideoPlayer) { }); it('controls are in paused state', function () { - expect(state.videoControl.isPlaying).toBe(false); + expect(state.videoPlayer.isPlaying()).toBe(false); }); }); }); @@ -166,16 +165,10 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(state.videoPlayer, 'play').andCallThrough(); state.videoPlayer.onReady(); }); - it('log the load_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith('load_video'); - }); - it('autoplay the first video', function () { expect(state.videoPlayer.play).not.toHaveBeenCalled(); }); @@ -197,9 +190,7 @@ function (VideoPlayer) { var playbackRates = state.videoPlayer.player.getAvailablePlaybackRates(); state.currentPlayerMode = 'flash'; - state.videoPlayer.onReady(); - expect(playbackRates.length).toBe(4); expect(state.currentPlayerMode).toBe('html5'); }); @@ -209,10 +200,7 @@ function (VideoPlayer) { describe('when the video is unstarted', function () { beforeEach(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -221,7 +209,7 @@ function (VideoPlayer) { }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -244,9 +232,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(window, 'setInterval').andReturn(100); - spyOn(state.videoControl, 'play'); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -254,23 +240,6 @@ function (VideoPlayer) { }); }); - it('speed_change_video event is not logged when speed not change', function () { - expect(state.videoPlayer.log).not.toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: state.speed, - new_speed: state.speed - } - ); - }); - - it('log the play_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'play_video', { currentTime: 0 } - ); - }); - it('set update interval', function () { expect(window.setInterval).toHaveBeenCalledWith( state.videoPlayer.update, 200 @@ -279,7 +248,7 @@ function (VideoPlayer) { }); it('play the video control', function () { - expect(state.videoControl.play).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('pause'); }); it('play the video caption', function () { @@ -295,10 +264,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.PLAYING }); @@ -310,18 +276,12 @@ function (VideoPlayer) { }); }); - it('log the pause_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'pause_video', { currentTime: 0 } - ); - }); - it('clear update interval', function () { expect(state.videoPlayer.updateInterval).toBeUndefined(); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -334,32 +294,19 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.ENDED }); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { expect($.fn.trigger).toHaveBeenCalledWith('ended', {}); }); - - it('log stop_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'stop_video', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); }); }); @@ -397,25 +344,6 @@ function (VideoPlayer) { }); }); - it('slider event causes log update', function () { - runs(function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - - expect(state.videoPlayer.log).toHaveBeenCalledWith('seek_video', { - old_time: jasmine.any(Number), - new_time: 2, - type: 'onSlideSeek' - }); - }); - }); - it('seek the player', function () { runs(function () { spyOn(state.videoPlayer.player, 'seekTo').andCallThrough(); @@ -469,24 +397,6 @@ function (VideoPlayer) { .andCallThrough(); }); - it('slider event causes log update', function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'seek_video', { - old_time: 0, - new_time: 2, - type: 'onSlideSeek' - } - ); - }); - it('video has a correct speed', function () { state.speed = '2.0'; state.videoPlayer.onPlay(); @@ -785,7 +695,7 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); - state.videoControl.toggleFullScreen(jQuery.Event('click')); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -810,11 +720,10 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); state.el.addClass('video-fullscreen'); - state.videoControl.fullScreenState = true; - state.videoControl.isFullScreen = true; - state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen'); - - state.videoControl.toggleFullScreen(jQuery.Event('click')); + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.isFullScreen = true; + state.videoFullScreen.fullScreenEl.attr('title', 'Exit-fullscreen'); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -835,83 +744,6 @@ function (VideoPlayer) { }); }); - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'playVideo').andCallThrough(); - }); - - describe('when the player is not ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo = void 0; - state.videoPlayer.play(); - }); - - it('does nothing', function () { - expect(state.videoPlayer.player.playVideo).toBeUndefined(); - }); - }); - - describe('when the player is ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo.andReturn(true); - state.videoPlayer.play(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.playVideo).toHaveBeenCalled(); - }); - }); - }); - - describe('isPlaying', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'getPlayerState').andCallThrough(); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PLAYING); - }); - - it('return true', function () { - expect(state.videoPlayer.isPlaying()).toBeTruthy(); - }); - }); - - describe('when the video is not playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PAUSED); - }); - - it('return false', function () { - expect(state.videoPlayer.isPlaying()).toBeFalsy(); - }); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'pauseVideo').andCallThrough(); - state.videoPlayer.pause(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.pauseVideo).toHaveBeenCalled(); - }); - }); - describe('duration', function () { beforeEach(function () { state = jasmine.initializePlayer(); @@ -1016,9 +848,7 @@ function (VideoPlayer) { runs(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - controls = state.el.find('.video-controls'); }); @@ -1053,7 +883,6 @@ function (VideoPlayer) { saveState: jasmine.createSpy(), videoPlayer: { currentTime: 60, - log: jasmine.createSpy(), updatePlayTime: jasmine.createSpy(), setPlaybackRate: jasmine.createSpy(), player: jasmine.createSpyObj('player', ['setPlaybackRate']) @@ -1063,18 +892,6 @@ function (VideoPlayer) { }); describe('always', function () { - it('check if speed_change_video is logged', function () { - VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: '1.50', - new_speed: '0.75' - } - ); - }); - it('convert the current time to the new speed', function () { state.isFlashMode.andReturn(true); VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); @@ -1083,10 +900,7 @@ function (VideoPlayer) { it('set video speed to the new speed', function () { VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.setSpeed).toHaveBeenCalledWith('0.75', true); - expect(state.saveState).toHaveBeenCalledWith(true, { - speed: '0.75' - }); + expect(state.setSpeed).toHaveBeenCalledWith('0.75'); expect(state.videoPlayer.setPlaybackRate) .toHaveBeenCalledWith('0.75'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js new file mode 100644 index 0000000000..18a6f6874c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js @@ -0,0 +1,42 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoPoster', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the poster', function () { + expect($('.poster')).toExist(); + expect($('.btn-play')).toExist(); + }); + + it('can start playing the video on click', function () { + $('.btn-play').click(); + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }); + + it('destroy itself on "play" event', function () { + $('.btn-play').click(); + expect($('.poster')).not.toExist(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js index d7a5685719..491e98fae7 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js @@ -12,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -38,6 +39,18 @@ expect(state.videoProgressSlider.handle) .toBe('.slider .ui-slider-handle'); }); + + it('add ARIA attributes to time control', function () { + var timeControl = $('div.slider > a'); + + expect(timeControl).toHaveAttrs({ + 'role': 'slider', + 'title': 'Video position', + 'aria-disabled': 'false' + }); + + expect(timeControl).toHaveAttr('aria-valuetext'); + }); }); describe('on a touch-based device', function () { @@ -304,6 +317,13 @@ }); }); + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoProgressSlider.destroy(); + expect(state.videoProgressSlider).toBeUndefined(); + expect($('.slider')).toBeEmpty(); + }); + }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js index 1ade3cb9ce..0bf3722a4c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js @@ -1,12 +1,13 @@ (function (undefined) { describe('VideoQualityControl', function () { - var state, qualityControl, qualityControlEl, videoPlayer, player; + var state, qualityControl, videoPlayer, player; afterEach(function () { $('source').remove(); if (state.storage) { state.storage.clear(); } + state.videoPlayer.destroy(); }); describe('constructor, YouTube mode', function () { @@ -105,6 +106,11 @@ expect(qualityControl.el).toHaveClass('active'); }); + it('can destroy itself', function () { + state.videoQualityControl.destroy(); + expect(state.videoQualityControl).toBeUndefined(); + expect($('.quality-control')).not.toExist(); + }); }); describe('constructor, HTML5 mode', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js new file mode 100644 index 0000000000..7c101cdb32 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js @@ -0,0 +1,230 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Save State plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(state.storage, 'setItem'); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + describe('saveState function', function () { + var videoPlayerCurrentTime, newCurrentTime, speed; + + // We make sure that `currentTime` is a float. We need to test + // that Math.round() is called. + videoPlayerCurrentTime = 3.1242; + + // We have two times, because one is stored in + // `videoPlayer.currentTime`, and the other is passed directly to + // `saveState` in `data` object. In each case, there is different + // code that handles these times. They have to be different for + // test completeness sake. Also, make sure it is float, as is the + // time above. + newCurrentTime = 5.4; + speed = '0.75'; + + beforeEach(function () { + state.videoPlayer.currentTime = videoPlayerCurrentTime; + spyOn(Time, 'formatFull').andCallThrough(); + }); + + it('data is not an object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: videoPlayerCurrentTime, + data: undefined, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) + } + }); + }); + + it('data contains speed, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: undefined, + data: { + speed: speed + }, + ajaxData: { + speed: speed + } + }); + }); + + it('data contains float position, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: newCurrentTime, + data: { + saved_video_position: newCurrentTime + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains speed and rounded position, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: Math.round(newCurrentTime), + data: { + speed: speed, + saved_video_position: Math.round(newCurrentTime) + }, + ajaxData: { + speed: speed, + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains empty object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: undefined, + data: {}, + ajaxData: {} + }); + }); + + it('data contains position 0, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: 0, + data: { + saved_video_position: 0 + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(0)) + } + }); + }); + + function itSpec(value) { + var asyncVal = value.asyncVal, + speedVal = value.speedVal, + positionVal = value.positionVal, + data = value.data, + ajaxData = value.ajaxData; + + state.videoSaveStatePlugin.saveState(asyncVal, data); + + if (speedVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'speed', + speedVal, + true + ); + } + if (positionVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'savedVideoPosition', + positionVal, + true + ); + expect(Time.formatFull).toHaveBeenCalledWith( + positionVal + ); + } + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: asyncVal, + dataType: 'json', + data: ajaxData + }); + } + }); + + it('can save state on speed change', function () { + state.el.trigger('speedchange', ['2.0']); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {speed: '2.0'} + }); + }); + + it('can save state on page unload', function () { + $.ajax.reset(); + state.videoSaveStatePlugin.onUnload(); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: false, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on pause', function () { + state.el.trigger('pause'); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on language change', function () { + state.el.trigger('language_menu:change', ['ua']); + expect(state.storage.setItem).toHaveBeenCalledWith('language', 'ua'); + }); + + it('can save information about youtube availability', function () { + state.el.trigger('youtube_availability', [true]); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {youtube_is_available: true} + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoSaveStatePlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoSaveStatePlugin.destroy(); + expect(state.videoSaveStatePlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'speedchange': plugin.onSpeedChange, + 'play': plugin.bindUnloadHandler, + 'pause destroy': plugin.saveStateHandler, + 'language_menu:change': plugin.onLanguageChange, + 'youtube_availability': plugin.onYoutubeAvailability + }); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + expect($.fn.off).toHaveBeenCalledWith('unload', plugin.onUnload); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js new file mode 100644 index 0000000000..da3a87845b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js @@ -0,0 +1,55 @@ +(function () { + 'use strict'; + describe('VideoSkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute').andCallThrough(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control when video starts playing', function () { + expect($('.skip-control')).not.toExist(); + state.el.trigger('play'); + expect($('.skip-control')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + state.el.trigger('play'); + expect($('.skip-control')).toHaveAttrs({ + 'role': 'button', + 'title': 'Do not show again', + 'aria-disabled': 'false' + }); + }); + + it('can skip the video on click', function () { + spyOn(state.bumperState.videoBumper, 'skipAndDoNotShowAgain'); + state.el.trigger('play'); + $('.skip-control').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip', true); + expect(state.bumperState.videoBumper.skipAndDoNotShowAgain).toHaveBeenCalled(); + }); + + it('can destroy itself', function () { + state.bumperState.videoPlaySkipControl.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js index 45db124f0a..d5b14e6b2d 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js @@ -12,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -247,5 +248,13 @@ expect($('.speeds .value')).toHaveHtml('0.75x'); }); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoSpeedControl.destroy(); + expect(state.videoSpeedControl).toBeUndefined(); + expect($('.video-speeds')).not.toExist(); + expect($('.speed-button')).not.toExist(); + }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js index 7ff313f956..e1edb571d3 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js @@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () { $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); it('Volume level has correct value even if cookie is broken', function () { @@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () { }); it('render the volume control', function () { - expect(state.videoControl.secondaryControlsEl.html()) - .toContain('
\n'); + expect($('.volume')).toExist(); }); it('create the slider', function () { @@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () { shiftKey: true }); }); - }) + }); describe('keyDownButtonHandler', function () { beforeEach(function () { @@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () { })); expect(volumeControl.getMuteStatus()).toEqual(isMuted); }); - }) + }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js index cbf7df47ee..f0c1debcd0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js +++ b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js @@ -177,9 +177,8 @@ function () { } }; - var cleanDelta = function () { - delta['height'] = 0; - delta['width'] = 0; + var resetDelta = function () { + delta['height'] = delta['width'] = 0; return module; }; @@ -200,12 +199,23 @@ function () { return module; }; + var destroy = function () { + var data = getData(); + data.element.css({ + 'height': '', 'width': '', 'top': '', 'left': '' + }); + removeCallbacks(); + resetDelta(); + mode = null; + }; + initialize.apply(module, arguments); return $.extend(true, module, { align: align, alignByWidthOnly: alignByWidthOnly, alignByHeightOnly: alignByHeightOnly, + destroy: destroy, setParams: initialize, setMode: setMode, setElement: setElement, @@ -218,7 +228,7 @@ function () { delta: { add: addDelta, substract: substractDelta, - reset: cleanDelta + reset: resetDelta } }); }; diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 05654966f5..ad7edc8b56 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -14,8 +14,8 @@ define( 'video/01_initialize.js', -['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'], -function (VideoPlayer, VideoStorage, i18n) { +['video/03_video_player.js', 'video/00_i18n.js'], +function (VideoPlayer, i18n) { /** * @function * @@ -71,7 +71,6 @@ function (VideoPlayer, VideoStorage, i18n) { isYoutubeType: isYoutubeType, parseSpeed: parseSpeed, parseYoutubeStreams: parseYoutubeStreams, - saveState: saveState, setPlayerMode: setPlayerMode, setSpeed: setSpeed, speedToString: speedToString, @@ -145,9 +144,7 @@ function (VideoPlayer, VideoStorage, i18n) { _youtubeApiDeferred.resolve(); } - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); } else { // There is only one global variable window.onYouTubeIframeAPIReady which // is supposed to be a function that will be called by the YouTube API @@ -191,9 +188,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Attach a callback to our Deferred object to be called once the // YouTube API loads. window.onYouTubeIframeAPIReady.done(function () { - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); }); } } else { @@ -212,20 +207,15 @@ function (VideoPlayer, VideoStorage, i18n) { // callback, which will set `state.youtubeApiAvailable` to `true`. // If something goes wrong at this stage, `state.youtubeApiAvailable` is // `false`. - _reportToServer(state, state.youtubeApiAvailable); + if (!state.youtubeIsAvailable) { + console.log('[Video info]: YouTube API is not available.'); + } + state.el.trigger('youtube_availability', [state.youtubeIsAvailable]); }, state.config.ytTestTimeout); $.getScript(document.location.protocol + '//' + state.config.ytApiUrl); } - function _reportToServer(state, youtubeIsAvailable) { - if (!youtubeIsAvailable) { - console.log('[Video info]: YouTube API is not available.'); - } - - state.saveState(true, { youtube_is_available: youtubeIsAvailable }); - } - // function _configureCaptions(state) // Configure displaying of captions. // @@ -296,8 +286,7 @@ function (VideoPlayer, VideoStorage, i18n) { state.videoType = 'html5'; - if (!state.config.sub || !state.config.sub.length) { - state.config.sub = ''; + if (!_.keys(state.config.transcriptLanguages).length) { state.config.showCaptions = false; } state.setSpeed(state.speed); @@ -328,8 +317,9 @@ function (VideoPlayer, VideoStorage, i18n) { function _initializeModules(state, i18n) { var dfd = $.Deferred(), modulesList = $.map(state.modules, function(module) { - if ($.isFunction(module)) { - return module(state, i18n); + var options = state.options[module.moduleName] || {}; + if (_.isFunction(module)) { + return module(state, i18n, options); } else if ($.isPlainObject(module)) { return module; } @@ -388,7 +378,6 @@ function (VideoPlayer, VideoStorage, i18n) { }, 'startTime': function (value) { value = parseInt(value, 10); - if (!isFinite(value) || value < 0) { return 0; } @@ -407,6 +396,13 @@ function (VideoPlayer, VideoStorage, i18n) { }, config = {}; + data = _.extend({ + startTime: 0, + endTime: null, + sub: '', + streams: '' + }, data); + $.each(data, function(option, value) { // Extract option that is in `extractKeys`. if ($.inArray(option, extractKeys) !== -1) { @@ -420,7 +416,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Pre-process data. if (conversions[option]) { - if ($.isFunction(conversions[option])) { + if (_.isFunction(conversions[option])) { value = conversions[option].call(this, value); } else { throw new TypeError(option + ' is not a function.'); @@ -463,12 +459,11 @@ function (VideoPlayer, VideoStorage, i18n) { function initialize(element) { var self = this, - el = $(element).find('.video'), + el = this.el, + id = this.id, container = el.find('.video-wrapper'), - id = el.attr('id').replace(/video_/, ''), __dfd__ = $.Deferred(), - isTouch = onTouchBasedDevice() || '', - storage = VideoStorage('VideoState', id); + isTouch = onTouchBasedDevice() || ''; if (isTouch) { el.addClass('is-touch'); @@ -476,23 +471,18 @@ function (VideoPlayer, VideoStorage, i18n) { $.extend(this, { __dfd__: __dfd__, - el: el, container: container, - id: id, isFullScreen: false, - isTouch: isTouch, - storage: storage + isTouch: isTouch }); - console.log( - '[Video info]: Initializing video with id "' + id + '".' - ); + console.log('[Video info]: Initializing video with id "%s".', id); // We store all settings passed to us by the server in one place. These // are "read only", so don't modify them. All variable content lives in // 'state' object. // jQuery .data() return object with keys in lower camelCase format. - this.config = $.extend({}, _getConfiguration(el.data(), storage), { + this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), { element: element, fadeOutTimeout: 1400, captionsFreezeTime: 10000, @@ -602,26 +592,18 @@ function (VideoPlayer, VideoStorage, i18n) { // true: Parsing of YouTube video IDs went OK, and we can proceed // onwards to play YouTube videos. function parseYoutubeStreams(youtubeStreams) { - var _this; - - if ( - typeof youtubeStreams === 'undefined' || - youtubeStreams.length === 0 - ) { + if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) { return false; } - _this = this; this.videos = {}; - $.each(youtubeStreams.split(/,/), function (index, video) { + _.each(youtubeStreams.split(/,/), function (video) { var speed; - video = video.split(/:/); - speed = _this.speedToString(video[0]); - - _this.videos[speed] = video[1]; - }); + speed = this.speedToString(video[0]); + this.videos[speed] = video[1]; + }, this); return _.isString(this.videos['1.0']); } @@ -633,23 +615,21 @@ function (VideoPlayer, VideoStorage, i18n) { // example the length of the video can be determined from the meta // data. function fetchMetadata() { - var _this = this, + var self = this, metadataXHRs = []; this.metadata = {}; - $.each(this.videos, function (speed, url) { - var xhr = _this.getVideoMetadata(url, function (data) { + metadataXHRs = _.map(this.videos, function (url, speed) { + return self.getVideoMetadata(url, function (data) { if (data.data) { - _this.metadata[data.data.id] = data.data; + self.metadata[data.data.id] = data.data; } }); - - metadataXHRs.push(xhr); }); $.when.apply(this, metadataXHRs).done(function () { - _this.el.trigger('metadata_received'); + self.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 @@ -657,7 +637,7 @@ function (VideoPlayer, VideoStorage, i18n) { // 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; + self.youtubeMetadataReceived = true; }); } @@ -666,23 +646,21 @@ function (VideoPlayer, VideoStorage, i18n) { // // Create a separate array of available speeds. function parseSpeed() { - this.speeds = ($.map(this.videos, function (url, speed) { - return speed; - })).sort(); + this.speeds = _.keys(this.videos).sort(); } - function setSpeed(newSpeed, updateStorage) { + function setSpeed(newSpeed) { // Possible speeds for each player type. // HTML5 = [0.75, 1, 1.25, 1.5] // Youtube Flash = [0.75, 1, 1.25, 1.5] // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2] var map = { - '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash - }; + '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash + }; if (_.contains(this.speeds, newSpeed)) { this.speed = newSpeed; @@ -690,57 +668,21 @@ function (VideoPlayer, VideoStorage, i18n) { newSpeed = map[newSpeed]; this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0'; } - - if (updateStorage) { - this.storage.setItem('speed', this.speed, true); - this.storage.setItem('general_speed', this.speed); - } } function getVideoMetadata(url, callback) { - var successHandler, xhr; - - if (typeof url !== 'string') { + if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } - successHandler = ($.isFunction(callback)) ? callback : null; - xhr = $.ajax({ + + return $.ajax({ url: [ document.location.protocol, '//', this.config.ytTestUrl, url, '?v=2&alt=jsonc' ].join(''), dataType: 'jsonp', timeout: this.config.ytTestTimeout, - success: successHandler - }); - - return xhr; - } - - function saveState(async, data) { - - if (!($.isPlainObject(data))) { - data = { - saved_video_position: this.videoPlayer.currentTime - }; - } - - if (data.speed) { - this.storage.setItem('speed', data.speed, true); - } - - if (data.hasOwnProperty('saved_video_position')) { - this.storage.setItem('savedVideoPosition', data.saved_video_position, true); - - data.saved_video_position = Time.formatFull(data.saved_video_position); - } - - $.ajax({ - url: this.config.saveStateUrl, - type: 'POST', - async: async ? true : false, - dataType: 'json', - data: data, + success: _.isFunction(callback) ? callback : null }); } diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js index d055b85d62..dc3fd7974b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js @@ -110,6 +110,54 @@ function () { }); }; + Player.prototype.onError = function (event) { + if ($.isFunction(this.config.events.onError)) { + this.config.events.onError(); + } + }; + + Player.prototype.destroy = function () { + this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false); + this.video.removeEventListener('play', this.onPlay, false); + this.video.removeEventListener('playing', this.onPlaying, false); + this.video.removeEventListener('pause', this.onPause, false); + this.video.removeEventListener('ended', this.onEnded, false); + this.el + .find('.video-player div').removeClass('hidden') + .end() + .find('.video-player h3').addClass('hidden') + .end().removeClass('is-initialized') + .find('.spinner').attr({'aria-hidden': 'false'}); + this.videoEl.remove(); + }; + + Player.prototype.onLoadedMetadata = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + if ($.isFunction(this.config.events.onReady)) { + this.config.events.onReady(null); + } + }; + + Player.prototype.onPlay = function () { + this.playerState = HTML5Video.PlayerState.BUFFERING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPlaying = function () { + this.playerState = HTML5Video.PlayerState.PLAYING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPause = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + this.callStateChangeCallback(); + }; + + Player.prototype.onEnded = function () { + this.playerState = HTML5Video.PlayerState.ENDED; + this.callStateChangeCallback(); + }; + return Player; /* @@ -152,6 +200,7 @@ function () { var isTouch = onTouchBasedDevice() || '', sourceList, _this, errorMessage, lastSource; + _.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded'); this.logs = []; // 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 @@ -226,6 +275,8 @@ function () { lastSource = this.videoEl.find('source').last(); lastSource.on('error', this.showErrorMessage.bind(this)); + lastSource.on('error', this.onError.bind(this)); + this.videoEl.on('error', this.onError.bind(this)); if (/iP(hone|od)/i.test(isTouch[0])) { this.videoEl.prop('controls', true); @@ -280,35 +331,11 @@ function () { // When the
- -
    -
  1. -
${_('Go back to start of transcript.')} @@ -116,8 +50,8 @@ % if transcript_download_format: ${_('Download transcript')}
- ${'.' + transcript_download_format} -
    + ${'.' + transcript_download_format} +