Commit 6f2a2b44 by muhammad-ammar

add HLS playback support in video player

TNL-6513
parent be5c4fad
......@@ -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',
......
......@@ -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
......
#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
#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
#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
#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
#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
#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
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="video closed"
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/hls/hls.m3u8", "/base/fixtures/test.mp4","/base/fixtures/test.webm"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/", "source": ""}'
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<section class="video-player">
<div id="id"></div>
<h4 class="hd hd-4 video-hls-error is-hidden">
Your browser does not support this video format. Try using a different browser.
</h4>
</section>
<section class="video-controls is-hidden"></section>
</article>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
</div>
......@@ -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: [
......
......@@ -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);
......@@ -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);
(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']);
......
(function(undefined) {
'use strict';
describe('VideoPlayer Events plugin', function() {
var state, oldOTBD, Logger = window.Logger;
var describeInfo, state, oldOTBD;
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.and.returnValue(null);
describeInfo = new jasmine.DescribeInfo('', function() {
var Logger = window.Logger;
state = jasmine.initializePlayer();
beforeEach(function() {
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);
......@@ -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));
/* 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) {
......
/* 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));
/* eslint-disable no-console, no-param-reassign */
(function(requirejs, require, define) {
// VideoPlayer module.
define(
'video/03_video_player.js',
['video/02_html5_video.js', 'video/00_resizer.js'],
function(HTML5Video, Resizer) {
['video/02_html5_video.js', 'video/02_html5_hls_video.js', 'video/00_resizer.js', 'hls', 'underscore'],
function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _) {
var dfd = $.Deferred(),
VideoPlayer = function(state) {
state.videoPlayer = {};
......@@ -100,8 +101,13 @@ function(HTML5Video, Resizer) {
// initial configuration. Also make the created DOM elements available
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
// eslint-disable-next-line no-underscore-dangle
function _initialize(state) {
var youTubeId, player, userAgent;
var youTubeId,
player,
userAgent,
commonPlayerConfig,
eventToBeTriggered = 'loadedmetadata';
// The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's
......@@ -147,19 +153,42 @@ function(HTML5Video, Resizer) {
state.browserIsSafari = (userAgent.indexOf('safari') > -1 &&
!state.browserIsChrome);
if (state.videoType === 'html5') {
state.videoPlayer.player = new HTML5Video.Player(state.el, {
playerVars: state.videoPlayer.playerVars,
videoSources: state.config.sources,
events: {
onReady: state.videoPlayer.onReady,
onStateChange: state.videoPlayer.onStateChange,
onError: state.videoPlayer.onError
}
});
// Browser can play HLS videos if either `Media Source Extensions`
// feature is supported or browser is safari (native HLS support)
state.canPlayHLS = state.HLSVideoSources.length > 0 && (HLS.isSupported() || state.browserIsSafari);
state.HLSOnlySources = state.config.sources.length > 0 &&
state.config.sources.length === state.HLSVideoSources.length;
commonPlayerConfig = {
playerVars: state.videoPlayer.playerVars,
videoSources: state.config.sources,
browserIsSafari: state.browserIsSafari,
events: {
onReady: state.videoPlayer.onReady,
onStateChange: state.videoPlayer.onStateChange,
onError: state.videoPlayer.onError
}
};
if (state.videoType === 'html5') {
if (state.canPlayHLS || state.HLSOnlySources) {
state.videoPlayer.player = new HTML5HLSVideo.Player(
state.el,
_.extend({}, commonPlayerConfig, {
videoSources: state.HLSVideoSources,
canPlayHLS: state.canPlayHLS,
HLSOnlySources: state.HLSOnlySources
})
);
// `loadedmetadata` event triggered too early on Safari due
// to which correct video dimensions were not calculated
eventToBeTriggered = state.browserIsSafari ? 'loadeddata' : eventToBeTriggered;
} else {
state.videoPlayer.player = new HTML5Video.Player(state.el, commonPlayerConfig);
}
player = state.videoEl = state.videoPlayer.player.videoEl;
player[0].addEventListener('loadedmetadata', state.videoPlayer.onLoadMetadataHtml5, false);
player[0].addEventListener(eventToBeTriggered, state.videoPlayer.onLoadMetadataHtml5, false);
player.on('remove', state.videoPlayer.destroy);
} else {
youTubeId = state.youtubeId();
......@@ -179,6 +208,8 @@ function(HTML5Video, Resizer) {
videoWidth = player.attr('width') || player.width(),
videoHeight = player.attr('height') || player.height();
player.on('remove', state.videoPlayer.destroy);
_resize(state, videoWidth, videoHeight);
_updateVcrAndRegion(state, true);
});
......@@ -323,6 +354,9 @@ function(HTML5Video, Resizer) {
if (player && _.isFunction(player.destroy)) {
player.destroy();
}
if (this.canPlayHLS && player.hls) {
player.hls.destroy();
}
delete this.videoPlayer;
}
......
......@@ -142,7 +142,8 @@
log: function(eventName, data) {
var logInfo = _.extend({
id: this.state.id,
code: this.state.isYoutubeType() ? this.state.youtubeId() : 'html5'
// eslint-disable-next-line no-nested-ternary
code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5'
}, data, this.options.data);
Logger.log(eventName, logInfo);
}
......
......@@ -124,6 +124,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
resource_string(module, 'js/src/video/01_initialize.js'),
resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'),
resource_string(module, 'js/src/video/02_html5_hls_video.js'),
resource_string(module, 'js/src/video/03_video_player.js'),
resource_string(module, 'js/src/video/035_video_accessible_menu.js'),
resource_string(module, 'js/src/video/04_video_control.js'),
......
......@@ -39,7 +39,8 @@
'backbone-super': 'js/vendor/backbone-super',
'jasmine-imagediff': 'js/vendor/jasmine-imagediff',
'URI': 'js/vendor/URI.min',
'draggabilly': 'js/vendor/draggabilly'
'draggabilly': 'js/vendor/draggabilly',
'hls': 'common/js/vendor/hls'
},
shim: {
'gettext': {
......
......@@ -33,7 +33,7 @@ CSS_CLASS_NAMES = {
'captions_rendered': '.video.is-captions-rendered',
'captions': '.subtitles',
'captions_text': '.subtitles li span',
'captions_text_getter': '.subtitles li span[role="link"][data-index="1"]',
'captions_text_getter': '.subtitles li span[role="link"][data-index="{}"]',
'closed_captions': '.closed-captions',
'error_message': '.video .video-player .video-error',
'video_container': '.video',
......@@ -46,11 +46,13 @@ CSS_CLASS_NAMES = {
'captions_lang_list': '.langs-list li',
'video_speed': '.speeds .value',
'poster': '.poster',
'active_caption_text': '.subtitles-menu > li.current span',
}
VIDEO_MODES = {
'html5': '.video video',
'youtube': '.video iframe'
'youtube': '.video iframe',
'hls': '.video video',
}
VIDEO_MENUS = {
......@@ -204,7 +206,7 @@ class VideoPage(PageObject):
Check that if video is rendered in `mode`.
Arguments:
mode (str): Video mode, `html5` or `youtube`.
mode (str): Video mode, one of `html5`, `youtube`, `hls`.
Returns:
bool: Tells if video is rendered in `mode`.
......@@ -222,11 +224,26 @@ class VideoPage(PageObject):
"""
is_present = self.q(css=selector).present
# There is no way to get actual HLS video URL. Becuase in hls video
# src attribute is not set to original url. https://github.com/video-dev/hls.js/issues/1052
# http://www.streambox.fr/playlists/x36xhzz/x36xhzz.m3u8 becomes
# "blob:https://studio-hlsvideo.sandbox.edx.org/0e2e72e0-904e-d946-9ce0-06c542894cda"
if mode == 'hls':
href_src = self.q(css=selector).attrs('src')[0]
is_present = href_src.startswith('blob:') or href_src.startswith('mediasource:')
return is_present, is_present
return Promise(_is_element_present, 'Video Rendering Failed in {0} mode.'.format(mode)).fulfill()
@property
def video_download_url(self):
"""
Return video download url or None
"""
browser_query = self.q(css='.wrapper-download-video .btn-link.video-sources')
return browser_query.attrs('href')[0] if browser_query.visible else None
@property
def is_autoplay_enabled(self):
"""
Extract autoplay value of `data-metadata` attribute to check video autoplay is enabled or disabled.
......@@ -409,16 +426,26 @@ class VideoPage(PageObject):
return ' '.join(subs)
def click_first_line_in_transcript(self):
def click_transcript_line(self, line_no):
"""
Clicks a line in the transcript updating the current caption.
Arguments:
line_no (int): line number to be clicked
"""
self.wait_for_captions()
captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'])
captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'].format(line_no))
captions_selector.click()
@property
def active_caption_text(self):
"""
Return active caption text.
"""
return self.q(css=CSS_CLASS_NAMES['active_caption_text']).text[0]
@property
def speed(self):
"""
Get current video speed value.
......
......@@ -2,7 +2,9 @@
import datetime
import json
from mock import patch
from nose.plugins.attrib import attr
import os
import ddt
from common.test.acceptance.tests.helpers import EventsTestMixin
......@@ -149,6 +151,45 @@ class VideoEventsTest(VideoEventsTestMixin):
assert_events_equal(static_fields_pattern, load_video_event)
class VideoHLSEventsTest(VideoEventsTestMixin):
"""
Test video player event emission for HLS video
"""
def test_event_data_for_hls(self):
"""
Scenario: Video component with HLS video emits events correctly
Given the course has a Video component with Youtube, HTML5 and HLS sources available.
And I play the video
And the video starts playing
And I watch 3 seconds of it
When I pause and seek the video
And I play the video to the end
Then I verify that all expected events are triggered
And triggered events have correct data
"""
video_events = ('load_video', 'play_video', 'pause_video', 'seek_video')
def is_video_event(event):
"""
Filter out anything other than the video events of interest
"""
return event['event_type'] in video_events
captured_events = []
with self.capture_events(is_video_event, captured_events=captured_events):
self.metadata = self.metadata_for_mode('hls')
self.navigate_to_video()
self.video.click_player_button('play')
self.video.wait_for_position('0:03')
self.video.click_player_button('pause')
self.video.seek('0:08')
expected_events = [{'name': event, 'event': {'code': 'hls'}} for event in video_events]
self.assert_events_match(expected_events, captured_events)
@attr(shard=8)
@ddt.ddt
class VideoBumperEventsTest(VideoEventsTestMixin):
......
0
00:00:00,000 --> 00:00:01,000
Hi, edX welcomes you0.
1
00:00:01,000 --> 00:00:02,000
Hi, edX welcomes you1.
2
00:00:02,000 --> 00:00:03,000
Hi, edX welcomes you2.
3
00:00:03,000 --> 00:00:04,000
Hi, edX welcomes you3.
4
00:00:04,000 --> 00:00:05,000
Hi, edX welcomes you4.
5
00:00:05,000 --> 00:00:06,000
Hi, edX welcomes you5.
6
00:00:06,000 --> 00:00:07,000
Hi, edX welcomes you6.
7
00:00:07,000 --> 00:00:08,000
Hi, edX welcomes you7.
8
00:00:08,000 --> 00:00:09,000
Hi, edX welcomes you8.
9
00:00:09,000 --> 00:00:10,000
Hi, edX welcomes you9.
......@@ -1740,7 +1740,8 @@ REQUIRE_JS_PATH_OVERRIDES = {
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory.js',
'js/courseware/courseware_factory': 'js/courseware/courseware_factory.js',
'js/groups/views/cohorts_dashboard_factory': 'js/groups/views/cohorts_dashboard_factory.js',
'draggabilly': 'js/vendor/draggabilly.js'
'draggabilly': 'js/vendor/draggabilly.js',
'hls': 'common/js/vendor/hls.js'
}
########################## DJANGO DEBUG TOOLBAR ###############################
......
......@@ -35,6 +35,7 @@ var options = {
{pattern: 'xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js', included: true},
{pattern: 'xmodule_js/common_static/js/vendor/jquery-ui.min.js', included: true},
{pattern: 'xmodule_js/common_static/js/vendor/URI.min.js', included: true},
{pattern: 'common/js/vendor/hls.js', included: true},
{pattern: 'xmodule_js/src/capa/*.js', included: true},
{pattern: 'xmodule_js/src/video/*.js', included: true},
......
......@@ -108,7 +108,8 @@
'handlebars': 'js/vendor/ova/catch/js/handlebars-1.1.2',
'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'js/vendor/tinymce/js/tinymce/jquery.tinymce.min',
'picturefill': 'common/js/vendor/picturefill'
'picturefill': 'common/js/vendor/picturefill',
'hls': 'common/js/vendor/hls'
// end of files needed by OVA
},
shim: {
......@@ -224,6 +225,9 @@
// global namespace instead of being registered in require.
'draggabilly': {
exports: 'Draggabilly'
},
'hls': {
exports: 'Hls'
}
}
});
......
......@@ -35,11 +35,14 @@
baseUrl: '/base/',
paths: {
moment: 'xmodule_js/common_static/common/js/vendor/moment-with-locales',
'draggabilly': 'xmodule_js/common_static/js/vendor/draggabilly',
'edx-ui-toolkit': 'edx-ui-toolkit'
draggabilly: 'xmodule_js/common_static/js/vendor/draggabilly',
'edx-ui-toolkit': 'edx-ui-toolkit',
hls: 'common/js/vendor/hls'
},
'moment': {
exports: 'moment'
shim: {
moment: {
exports: 'moment'
}
}
});
}).call(this, RequireJS.requirejs, RequireJS.define);
......@@ -26,6 +26,9 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<div class="video-player">
<div id="${id}"></div>
<h4 class="hd hd-4 video-error is-hidden">${_('No playable video sources found.')}</h4>
<h4 class="hd hd-4 video-hls-error is-hidden">
${_('Your browser does not support this video format. Try using a different browser.')}
</h4>
</div>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
......@@ -39,7 +42,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</div>
<div class="focus_grabber last"></div>
% if download_video_link or track or handout or branding_info:
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
......
......@@ -7,6 +7,7 @@
"coffee-script": "1.6.1",
"edx-pattern-library": "0.18.1",
"edx-ui-toolkit": "1.5.1",
"hls.js": "0.7.2",
"jquery": "~2.2.0",
"jquery-migrate": "^1.4.1",
"jquery.scrollto": "~2.1.2",
......
......@@ -58,6 +58,7 @@ NPM_INSTALLED_LIBRARIES = [
'requirejs/require.js',
'underscore.string/dist/underscore.string.js',
'underscore/underscore.js',
'hls.js/dist/hls.js',
]
# A list of NPM installed developer libraries that should be copied into the common
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment