diff --git a/cms/envs/common.py b/cms/envs/common.py index dc8ceb2209..ffc1631134 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -934,6 +934,9 @@ INSTALLED_APPS = ( # Self-paced course configuration 'openedx.core.djangoapps.self_paced', + # Video module configs (This will be moved to Video once it becomes an XBlock) + 'openedx.core.djangoapps.video_config', + # django-oauth2-provider (deprecated) 'provider', 'provider.oauth2', diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index c226de12df..68de3e64ad 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -72,6 +72,7 @@ 'ieshim': 'js/src/ie_shim', 'tooltip_manager': 'js/src/tooltip_manager', 'draggabilly': 'js/vendor/draggabilly', + 'hls': 'common/js/vendor/hls', // Files needed for Annotations feature 'annotator': 'js/vendor/ova/annotator-full', diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index e1c7604b94..a99c88a5bc 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -250,7 +250,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark } } - .video-error { + .video-error, .video-hls-error { padding: ($baseline / 5); background: black; color: white !important; // the pattern library headings shim is more scoped diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_.m3u8 new file mode 100644 index 0000000000..19a2cf0008 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.576244, +XXXXXXXXT114-V015600_0_0.ts +#EXTINF:8.842178, +XXXXXXXXT114-V015600_0_1.ts +#EXTINF:9.609611, +XXXXXXXXT114-V015600_0_2.ts +#EXT-X-ENDLIST diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_0.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_0.ts new file mode 100644 index 0000000000..9bcd84bffa Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_0.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_1.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_1.ts new file mode 100644 index 0000000000..e298a596af Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_1.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_2.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_2.ts new file mode 100644 index 0000000000..fd6d34bf70 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_2.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_.m3u8 new file mode 100644 index 0000000000..e9b49ec7b2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.576244, +XXXXXXXXT114-V015600_1_0.ts +#EXTINF:9.042378, +XXXXXXXXT114-V015600_1_1.ts +#EXTINF:9.609611, +XXXXXXXXT114-V015600_1_2.ts +#EXT-X-ENDLIST diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_0.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_0.ts new file mode 100644 index 0000000000..780d3209a5 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_0.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_1.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_1.ts new file mode 100644 index 0000000000..976cda0f8f Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_1.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_2.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_2.ts new file mode 100644 index 0000000000..bb8f864aea Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_2.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_.m3u8 new file mode 100644 index 0000000000..92aea83871 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.576244, +XXXXXXXXT114-V015600_2_0.ts +#EXTINF:9.042378, +XXXXXXXXT114-V015600_2_1.ts +#EXTINF:9.609611, +XXXXXXXXT114-V015600_2_2.ts +#EXT-X-ENDLIST diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_0.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_0.ts new file mode 100644 index 0000000000..66ef4a9459 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_0.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_1.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_1.ts new file mode 100644 index 0000000000..96c7730ac1 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_1.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_2.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_2.ts new file mode 100644 index 0000000000..e6d5d8f631 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_2.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_.m3u8 new file mode 100644 index 0000000000..5918a46217 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.609611, +XXXXXXXXT114-V015600_3_0.ts +#EXTINF:9.009011, +XXXXXXXXT114-V015600_3_1.ts +#EXTINF:9.609611, +XXXXXXXXT114-V015600_3_2.ts +#EXT-X-ENDLIST diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_0.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_0.ts new file mode 100644 index 0000000000..a2936b83b0 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_0.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_1.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_1.ts new file mode 100644 index 0000000000..8ad3c0161a Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_1.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_2.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_2.ts new file mode 100644 index 0000000000..989bc2675c Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_2.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_.m3u8 new file mode 100644 index 0000000000..307a02d18c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.609611, +XXXXXXXXT114-V015600_4_0.ts +#EXTINF:9.009011, +XXXXXXXXT114-V015600_4_1.ts +#EXTINF:9.609611, +XXXXXXXXT114-V015600_4_2.ts +#EXT-X-ENDLIST diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_0.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_0.ts new file mode 100644 index 0000000000..e445515c18 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_0.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_1.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_1.ts new file mode 100644 index 0000000000..ec6d929b02 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_1.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_2.ts b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_2.ts new file mode 100644 index 0000000000..b338417b42 Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/hls/XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_2.ts differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/hls/hls.m3u8 b/common/lib/xmodule/xmodule/js/fixtures/hls/hls.m3u8 new file mode 100644 index 0000000000..cbcfd54645 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/hls/hls.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=264787,RESOLUTION=1280x720 +XXXXXXXXT114-V015600_1_/XXXXXXXXT114-V015600_1_.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=328415,RESOLUTION=1920x1080 +XXXXXXXXT114-V015600_0_/XXXXXXXXT114-V015600_0_.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=70750,RESOLUTION=640x360 +XXXXXXXXT114-V015600_3_/XXXXXXXXT114-V015600_3_.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=148269,RESOLUTION=960x540 +XXXXXXXXT114-V015600_2_/XXXXXXXXT114-V015600_2_.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=41276,RESOLUTION=640x360 +XXXXXXXXT114-V015600_4_/XXXXXXXXT114-V015600_4_.m3u8 diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_hls.html b/common/lib/xmodule/xmodule/js/fixtures/video_hls.html new file mode 100644 index 0000000000..e50a49a608 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/video_hls.html @@ -0,0 +1,29 @@ +
+
+
+
+
+ +
+
+ + +
+
+ +
+ +
+
+ +
+
+
+
+
diff --git a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js index 30050b0b8b..2c53cd4da8 100644 --- a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js +++ b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js @@ -39,6 +39,7 @@ var options = { {pattern: 'common_static/js/src/utility.js', included: true}, {pattern: 'common_static/js/test/add_ajax_prefix.js', included: true}, {pattern: 'common_static/js/test/i18n.js', included: true}, + {pattern: 'common_static/common/js/vendor/hls.js', included: true}, {pattern: 'public/js/split_test_staff.js', included: true}, {pattern: 'src/word_cloud/d3.min.js', included: true}, @@ -77,7 +78,8 @@ var options = { ], fixtureFiles: [ - {pattern: 'fixtures/*.*'} + {pattern: 'fixtures/*.*'}, + {pattern: 'fixtures/hls/**/*.*'} ], runFiles: [ diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js index d1fbb153dc..6c3462f8be 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.js +++ b/common/lib/xmodule/xmodule/js/spec/helper.js @@ -264,8 +264,17 @@ return state; }; + jasmine.initializeHLSPlayer = function(params) { + return jasmine.initializePlayer('video_hls.html', params); + }; + jasmine.initializePlayerYouTube = function(params) { // "video.html" contains HTML template for a YouTube video. return jasmine.initializePlayer('video.html', params); }; + + jasmine.DescribeInfo = function(description, specDefinitions) { + this.description = description; + this.specDefinitions = specDefinitions; + }; }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js index 1f71824335..4b04278962 100644 --- a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js +++ b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js @@ -35,11 +35,17 @@ baseUrl: '/base/', paths: { moment: 'common_static/common/js/vendor/moment-with-locales', - 'draggabilly': 'common_static/js/vendor/draggabilly', - 'edx-ui-toolkit': 'common_static/edx-ui-toolkit' + draggabilly: 'common_static/js/vendor/draggabilly', + 'edx-ui-toolkit': 'common_static/edx-ui-toolkit', + hls: 'common_static/common/js/vendor/hls' }, - 'moment': { - exports: 'moment' + shim: { + moment: { + exports: 'moment' + }, + hls: { + exports: 'Hls' + } } }); }).call(this, RequireJS.requirejs, RequireJS.define); 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 3cb155df53..5f23f4338b 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 @@ -1,7 +1,10 @@ (function(undefined) { describe('Video HTML5Video', function() { var STATUS = window.STATUS; - var state, oldOTBD, playbackRates = [0.75, 1.0, 1.25, 1.5]; + var state, + oldOTBD, + playbackRates = [0.75, 1.0, 1.25, 1.5], + describeInfo; beforeEach(function() { oldOTBD = window.onTouchBasedDevice; @@ -17,10 +20,8 @@ window.onTouchBasedDevice = oldOTBD; }); - describe('on non-Touch devices', function() { + describeInfo = new jasmine.DescribeInfo('on non-Touch devices ', function() { beforeEach(function() { - state = jasmine.initializePlayer('video_html5.html'); - state.videoPlayer.player.config.events.onReady = jasmine.createSpy('onReady'); }); @@ -321,6 +322,22 @@ }); }); + describe('non-hls encoding', function() { + beforeEach(function(done) { + state = jasmine.initializePlayer('video_html5.html'); + done(); + }); + jasmine.getEnv().describe(describeInfo.description, describeInfo.specDefinitions); + }); + + describe('hls encoding', function() { + beforeEach(function(done) { + state = jasmine.initializeHLSPlayer(); + done(); + }); + jasmine.getEnv().describe(describeInfo.description, describeInfo.specDefinitions); + }); + it('native controls are used on iPhone', function() { window.onTouchBasedDevice.and.returnValue(['iPhone']); 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 index 98722843a9..477328ab8c 100644 --- 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 @@ -1,15 +1,11 @@ (function(undefined) { 'use strict'; - describe('VideoPlayer Events plugin', function() { - var state, oldOTBD, Logger = window.Logger; + var describeInfo, state, oldOTBD; + + describeInfo = new jasmine.DescribeInfo('', function() { + var Logger = window.Logger; beforeEach(function() { - oldOTBD = window.onTouchBasedDevice; - window.onTouchBasedDevice = jasmine - .createSpy('onTouchBasedDevice') - .and.returnValue(null); - - state = jasmine.initializePlayer(); spyOn(Logger, 'log'); spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10); }); @@ -27,7 +23,7 @@ state.el.trigger('ready'); expect(Logger.log).toHaveBeenCalledWith('load_video', { id: 'id', - code: 'html5' + code: this.code }); }); @@ -36,7 +32,7 @@ state.el.trigger('play'); expect(Logger.log).toHaveBeenCalledWith('play_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeFalsy(); @@ -52,7 +48,7 @@ state.el.trigger('pause'); expect(Logger.log).toHaveBeenCalledWith('pause_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); @@ -62,7 +58,7 @@ state.el.trigger('speedchange', ['2.0', '1.0']); expect(Logger.log).toHaveBeenCalledWith('speed_change_video', { id: 'id', - code: 'html5', + code: this.code, current_time: 10, old_speed: '1.0', new_speed: '2.0' @@ -73,7 +69,7 @@ state.el.trigger('seek', [1, 0, 'any']); expect(Logger.log).toHaveBeenCalledWith('seek_video', { id: 'id', - code: 'html5', + code: this.code, old_time: 0, new_time: 1, type: 'any' @@ -91,7 +87,7 @@ state.el.trigger('ended'); expect(Logger.log).toHaveBeenCalledWith('stop_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); @@ -100,7 +96,7 @@ state.el.trigger('stop'); expect(Logger.log).toHaveBeenCalledWith('stop_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); @@ -110,7 +106,7 @@ state.el.trigger('skip', [false]); expect(Logger.log).toHaveBeenCalledWith('skip_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); }); @@ -119,7 +115,7 @@ state.el.trigger('skip', [true]); expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', { id: 'id', - code: 'html5', + code: this.code, currentTime: 10 }); }); @@ -128,7 +124,7 @@ state.el.trigger('language_menu:show'); expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.shown', { id: 'id', - code: 'html5' + code: this.code }); }); @@ -136,7 +132,7 @@ state.el.trigger('language_menu:hide'); expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.hidden', { id: 'id', - code: 'html5', + code: this.code, language: 'en' }); }); @@ -145,7 +141,7 @@ state.el.trigger('transcript:show'); expect(Logger.log).toHaveBeenCalledWith('show_transcript', { id: 'id', - code: 'html5', + code: this.code, current_time: 10 }); }); @@ -154,7 +150,7 @@ state.el.trigger('transcript:hide'); expect(Logger.log).toHaveBeenCalledWith('hide_transcript', { id: 'id', - code: 'html5', + code: this.code, current_time: 10 }); }); @@ -163,7 +159,7 @@ state.el.trigger('captions:show'); expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.shown', { id: 'id', - code: 'html5', + code: this.code, current_time: 10 }); }); @@ -172,7 +168,7 @@ state.el.trigger('captions:hide'); expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.hidden', { id: 'id', - code: 'html5', + code: this.code, current_time: 10 }); }); @@ -200,4 +196,31 @@ }); }); }); + + describe('VideoPlayer Events plugin', function() { + beforeEach(function() { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .and.returnValue(null); + }); + + describe('html5 encoding only', function() { + beforeEach(function(done) { + this.code = 'html5'; + state = jasmine.initializePlayer('video_html5.html'); + done(); + }); + jasmine.getEnv().describe(describeInfo.description, describeInfo.specDefinitions); + }); + + describe('hls encoding', function() { + beforeEach(function(done) { + this.code = 'hls'; + state = jasmine.initializeHLSPlayer(); + done(); + }); + jasmine.getEnv().describe(describeInfo.description, describeInfo.specDefinitions); + }); + }); }).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 b2faa75a89..8a34019f36 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 @@ -2,8 +2,8 @@ 'use strict'; require( -['video/03_video_player.js'], -function(VideoPlayer) { +['video/03_video_player.js', 'hls'], +function(VideoPlayer, HLS) { describe('VideoPlayer', function() { var state, oldOTBD, empty_arguments; @@ -969,6 +969,48 @@ function(VideoPlayer) { expect(state.videoPlayer.player.setPlaybackRate).toHaveBeenCalledWith('1.0'); }); }); + + describe('HLS Video', function() { + beforeEach(function() { + state = jasmine.initializeHLSPlayer(); + }); + + it('does not show error message if hls is supported', function() { + expect($('.video-hls-error')).toHaveClass('is-hidden'); + }); + + it('can extract hls video sources correctly', function() { + expect(state.HLSVideoSources).toEqual(['/base/fixtures/hls/hls.m3u8']); + expect(state.videoPlayer.player.hls).toBeDefined(); + }); + + describe('on safari', function() { + beforeEach(function() { + spyOn(HLS, 'isSupported').and.returnValue(false); + state = jasmine.initializeHLSPlayer(); + state.canPlayHLS = true; + state.browserIsSafari = true; + }); + + it('can use native hls playback support', function() { + expect(state.videoPlayer.player.hls).toBeUndefined(); + }); + }); + }); + + describe('HLS Video Errors', function() { + beforeEach(function() { + spyOn(HLS, 'isSupported').and.returnValue(false); + state = jasmine.initializeHLSPlayer({sources: ['/base/fixtures/hls/hls.m3u8']}); + }); + + it('shows error message if hls is not supported', function() { + expect($('.video-hls-error')).not.toHaveClass('is-hidden'); + expect($('.video-hls-error').text().trim()).toEqual( + 'Your browser does not support this video format. Try using a different browser.' + ); + }); + }); }); }); }(RequireJS.requirejs, RequireJS.require, RequireJS.define)); 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 c9e385ab9d..1082a811bc 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -1,4 +1,4 @@ -/* eslint no-console:0 */ +/* eslint-disable no-console, no-param-reassign */ /** * @file Initialize module works with the JSON config, and sets up various * settings, parameters, variables. After all setup actions are performed, it @@ -278,6 +278,18 @@ function(VideoPlayer, i18n, moment, _) { return false; } + /** + * Extract HLS video URLs from available video URLs. + * + * @param {object} state The object contaning the state (properties, methods, modules) of the Video player. + * @returns Array of available HLS video source urls. + */ + function extractHLSVideoSources(state) { + return _.filter(state.config.sources, function(source) { + return /\.m3u8$/.test(source); + }); + } + // function _prepareHTML5Video(state) // The function prepare HTML5 video, parse HTML5 // video sources etc. @@ -325,6 +337,7 @@ function(VideoPlayer, i18n, moment, _) { state.controlHideTimeout = null; state.captionState = 'invisible'; state.captionHideTimeout = null; + state.HLSVideoSources = extractHLSVideoSources(state); } function _initializeModules(state, i18n) { diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_hls_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_hls_video.js new file mode 100644 index 0000000000..4df4794043 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_hls_video.js @@ -0,0 +1,108 @@ +/* eslint-disable no-console, no-param-reassign */ +/** + * HTML5 video player module to support HLS video playback. + * + */ + +(function(requirejs, require, define) { + 'use strict'; + define('video/02_html5_hls_video.js', ['video/02_html5_video.js', 'hls'], + function(HTML5Video, HLS) { + var HLSVideo = {}; + + HLSVideo.Player = (function() { + /** + * Initialize HLS video player. + * + * @param {jQuery} el Reference to video player container element + * @param {Object} config Contains common config for video player + */ + function Player(el, config) { + var self = this; + + // do common initialization independent of player type + this.init(el, config); + + // If we have only HLS sources and browser doesn't support HLS then show error message. + if (config.HLSOnlySources && !config.canPlayHLS) { + this.showErrorMessage(null, '.video-hls-error'); + return; + } + + // Safari has native support to play HLS videos + if (config.browserIsSafari) { + this.videoEl.attr('src', config.videoSources[0]); + } else { + this.hls = new HLS(); + this.hls.loadSource(config.videoSources[0]); + this.hls.attachMedia(this.video); + + this.hls.on(HLS.Events.ERROR, this.onError.bind(this)); + + this.hls.on(HLS.Events.MANIFEST_PARSED, function(event, data) { + console.log( + '[HLS Video]: MANIFEST_PARSED, qualityLevelsInfo: ', + data.levels.map(function(level) { + return { + bitrate: level.bitrate, + resolution: level.width + 'x' + level.height + }; + }) + ); + }); + this.hls.on(HLS.Events.LEVEL_SWITCHED, function(event, data) { + var level = self.hls.levels[data.level]; + console.log( + '[HLS Video]: LEVEL_SWITCHED, qualityLevelInfo: ', + { + bitrate: level.bitrate, + resolution: level.width + 'x' + level.height + } + ); + }); + } + } + + Player.prototype = Object.create(HTML5Video.Player.prototype); + Player.prototype.constructor = Player; + + /** + * Handler for HLS video errors. This only takes care of fatal erros, non-fatal errors + * are automatically handled by hls.js + * + * @param {String} event `hlsError` + * @param {Object} data Contains the information regarding error occurred. + */ + Player.prototype.onError = function(event, data) { + if (data.fatal) { + switch (data.type) { + case HLS.ErrorTypes.NETWORK_ERROR: + console.error( + '[HLS Video]: Fatal network error encountered, try to recover. Details: %s', + data.details + ); + this.hls.startLoad(); + break; + case HLS.ErrorTypes.MEDIA_ERROR: + console.error( + '[HLS Video]: Fatal media error encountered, try to recover. Details: %s', + data.details + ); + this.hls.recoverMediaError(); + break; + default: + console.error( + '[HLS Video]: Unrecoverable error encountered. Details: %s', + data.details + ); + break; + } + } + }; + + return Player; + }()); + + return HLSVideo; + }); +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); 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 93759ee468..d9db98a596 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 @@ -1,3 +1,4 @@ +/* eslint-disable no-console, no-param-reassign */ /** * @file HTML5 video player module. Provides methods to control the in-browser * HTML5 video player. @@ -16,11 +17,94 @@ (function(requirejs, require, define) { define( 'video/02_html5_video.js', -[], -function() { +['underscore'], +function(_) { var HTML5Video = {}; HTML5Video.Player = (function() { + /* + * Constructor function for HTML5 Video player. + * + * @param {String|Object} el A DOM element where the HTML5 player will + * be inserted (as returned by jQuery(selector) function), or a + * selector string which will be used to select an element. This is a + * required parameter. + * + * @param config - An object whose properties will be used as + * configuration options for the HTML5 video player. This is an + * optional parameter. In the case if this parameter is missing, or + * some of the config object's properties are missing, defaults will be + * used. The available options (and their defaults) are as + * follows: + * + * config = { + * + * videoSources: [], // An array with properties being video + * // sources. The property name is the + * // video format of the source. Supported + * // video formats are: 'mp4', 'webm', and + * // 'ogg'. + * + * events: { // Object's properties identify the + * // events that the API fires, and the + * // functions (event listeners) that the + * // API will call when those events occur. + * // If value is null, or property is not + * // specified, then no callback will be + * // called for that event. + * + * onReady: null, + * onStateChange: null + * } + * } + */ + function Player(el, config) { + var errorMessage, lastSource, sourceList; + + // A simple test to see that the 'config' is a normal object. + if ($.isPlainObject(config) === false) { + return; + } + + // We should have at least one video source. Otherwise there is no + // point to continue. + if (!config.videoSources && !config.videoSources.length) { + return; + } + + // Create HTML markup for individual sources of the HTML5