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 0e34696377..b18208f183 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
@@ -1065,6 +1065,37 @@ function(VideoPlayer, HLS, _) {
}).always(done);
});
});
+
+ describe('HLS Primary Playback', function() {
+ beforeEach(function() {
+ spyOn(window.YT, 'Player').and.callThrough();
+ });
+
+ afterEach(function() {
+ YT.Player.calls.reset();
+ });
+
+ it('loads youtube if flag is disabled', function() {
+ state = jasmine.initializePlayer('video_all.html', {
+ prioritizeHls: false,
+ streams: '0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl'
+ });
+ expect(state.config.prioritizeHls).toBeFalsy();
+ expect(YT.Player).toHaveBeenCalled();
+ expect(state.videoPlayer.player.hls).toBeUndefined();
+ });
+
+ it('does not load youtube if flag is enabled', function() {
+ state = jasmine.initializePlayer('video_all.html', {
+ prioritizeHls: true,
+ streams: '0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl',
+ sources: ['/base/fixtures/test.mp4', '/base/fixtures/test.webm', '/base/fixtures/hls/hls.m3u8']
+ });
+ expect(state.config.prioritizeHls).toBeTruthy();
+ expect(YT.Player).not.toHaveBeenCalled();
+ expect(state.videoPlayer.player.hls).toBeDefined();
+ });
+ });
});
});
}(require, 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 996f3637b7..abc3c7ccb8 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -585,7 +585,8 @@ function(VideoPlayer, i18n, moment, _) {
_setConfigurations(this);
- if (!(_parseYouTubeIDs(this))) {
+ // If `prioritizeHls` is set to true than `hls` is the primary playback
+ if (this.config.prioritizeHls || !(_parseYouTubeIDs(this))) {
// If we do not have YouTube ID's, try parsing HTML5 video sources.
if (!_prepareHTML5Video(this)) {
__dfd__.reject();
diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py
index f1d886c4db..9299b8f616 100644
--- a/common/lib/xmodule/xmodule/video_module/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module/video_module.py
@@ -22,9 +22,11 @@ from operator import itemgetter
from pkg_resources import resource_string
from django.conf import settings
+from django.utils.functional import cached_property
from lxml import etree
from opaque_keys.edx.locator import AssetLocator
from openedx.core.djangoapps.video_config.models import HLSPlaybackEnabledFlag
+from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, CourseWaffleFlag
from openedx.core.lib.cache_utils import memoize_in_request_cache
from openedx.core.lib.license import LicenseMixin
from xblock.completable import XBlockCompletionMode
@@ -98,6 +100,11 @@ log = logging.getLogger(__name__)
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text
+# Waffle flags namespace for videos
+WAFFLE_VIDEOS_NAMESPACE = 'videos'
+
+# Waffle flag to enable/disable hls as primary playback
+DEPRECATE_YOUTUBE = 'deprecate_youtube'
EXPORT_IMPORT_COURSE_DIR = u'course'
EXPORT_IMPORT_STATIC_DIR = u'static'
@@ -182,6 +189,36 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
sorted_languages = OrderedDict(sorted_languages)
return track_url, transcript_language, sorted_languages
+ @cached_property
+ def youtube_deprecated(self):
+ """
+ Return True if youtube is deprecated and hls as primary playback is enabled else False
+ """
+ # Return False if `hls` playback feature is disabled.
+ if not HLSPlaybackEnabledFlag.feature_enabled(self.location.course_key):
+ return False
+
+ # check if hls as primary playback is enabled for this course
+ return CourseWaffleFlag(
+ WaffleFlagNamespace(name=WAFFLE_VIDEOS_NAMESPACE),
+ DEPRECATE_YOUTUBE
+ ).is_enabled(self.location.course_key)
+
+ def prioritize_hls(self, youtube_streams, html5_sources):
+ """
+ Decide whether hls can be prioritized as primary playback or not.
+
+ If both the youtube and hls sources are present then make decision on flag
+ If only either youtube or hls is present then play whichever is present
+ """
+ yt_present = bool(youtube_streams.strip())
+ hls_present = any(source for source in html5_sources if source.strip().endswith('.m3u8'))
+
+ if yt_present and hls_present:
+ return self.youtube_deprecated
+
+ return False
+
def get_html(self):
track_status = (self.download_track and self.track)
@@ -363,6 +400,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
# this user, based on what was recorded the last time we saw the
# user, and defaulting to True.
'recordedYoutubeIsAvailable': self.youtube_is_available,
+ 'prioritizeHls': self.prioritize_hls(self.youtube_streams, sources),
}
bumperize(self)
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index a0b1799bdf..607043b413 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -4,36 +4,38 @@ Video xmodule tests in mongo.
"""
import json
+import shutil
from collections import OrderedDict
+from tempfile import mkdtemp
from uuid import uuid4
-from tempfile import mkdtemp
-import shutil
import ddt
from django.conf import settings
from django.core.files import File
from django.core.files.base import ContentFile
from django.test import TestCase
from django.test.utils import override_settings
-from fs.osfs import OSFS
-from fs.path import combine
from edxval.api import (
ValCannotCreateError,
ValVideoNotFoundError,
- create_video_transcript,
create_or_update_video_transcript,
create_profile,
create_video,
+ create_video_transcript,
get_video_info,
get_video_transcript,
get_video_transcript_data
)
from edxval.utils import create_file_in_fs
+from fs.osfs import OSFS
+from fs.path import combine
from lxml import etree
from mock import MagicMock, Mock, patch
from path import Path as path
from openedx.core.lib.tests import attr
+from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
+from waffle.testutils import override_flag
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import ModuleStoreEnum
@@ -43,7 +45,12 @@ from xmodule.tests.test_import import DummySystem
from xmodule.tests.test_video import VideoDescriptorTestBase, instantiate_descriptor
from xmodule.video_module import VideoDescriptor, bumper_utils, rewrite_video_url, video_utils
from xmodule.video_module.transcripts_utils import Transcript, save_to_store, subs_filename
-from xmodule.video_module.video_module import EXPORT_IMPORT_STATIC_DIR, EXPORT_IMPORT_COURSE_DIR
+from xmodule.video_module.video_module import (
+ EXPORT_IMPORT_COURSE_DIR,
+ EXPORT_IMPORT_STATIC_DIR,
+ DEPRECATE_YOUTUBE,
+ WAFFLE_VIDEOS_NAMESPACE,
+)
from xmodule.x_module import STUDENT_VIEW
from .helpers import BaseTestXmodule
@@ -55,6 +62,8 @@ MODULESTORES = {
ModuleStoreEnum.Type.split: TEST_DATA_SPLIT_MODULESTORE,
}
+DEPRECATE_YOUTUBE_FLAG = '{}.{}'.format(WAFFLE_VIDEOS_NAMESPACE, DEPRECATE_YOUTUBE)
+
TRANSCRIPT_FILE_SRT_DATA = u"""
1
00:00:14,370 --> 00:00:16,530
@@ -116,6 +125,7 @@ class TestVideoYouTube(TestVideo):
'completionEnabled': False,
'completionPercentage': 0.95,
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
+ 'prioritizeHls': False,
})),
'track': None,
'transcript_download_format': u'srt',
@@ -197,6 +207,7 @@ class TestVideoNonYouTube(TestVideo):
'completionEnabled': False,
'completionPercentage': 0.95,
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
+ 'prioritizeHls': False,
})),
'track': None,
'transcript_download_format': u'srt',
@@ -219,6 +230,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
'''
Make sure that `get_html` works correctly.
'''
+ maxDiff = None
CATEGORY = "video"
DATA = SOURCE_XML
METADATA = {}
@@ -254,6 +266,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
'completionEnabled': False,
'completionPercentage': 0.95,
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
+ 'prioritizeHls': False,
})
def get_handler_url(self, handler, suffix):
@@ -986,6 +999,70 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.assertIn("\'poster\': \'null\'", context)
+ @patch('xmodule.video_module.video_module.HLSPlaybackEnabledFlag.feature_enabled', Mock(return_value=False))
+ def test_hls_primary_playback_on_toggling_hls_feature(self):
+ """
+ Verify that `prioritize_hls` is set to `False` if `HLSPlaybackEnabledFlag` is disabled.
+ """
+ video_xml = ''
+ self.initialize_module(data=video_xml)
+ context = self.item_descriptor.render(STUDENT_VIEW).content
+ self.assertIn('"prioritizeHls": false', context)
+
+ @ddt.data(
+ {
+ 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on,
+ 'waffle_enabled': False,
+ 'youtube': '3_yD_cEKoCk',
+ 'hls': ['https://hls.com/hls.m3u8'],
+ 'result': 'true'
+ },
+ {
+ 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on,
+ 'waffle_enabled': False,
+ 'youtube': '',
+ 'hls': ['https://hls.com/hls.m3u8'],
+ 'result': 'false'
+ },
+ {
+ 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on,
+ 'waffle_enabled': False,
+ 'youtube': '',
+ 'hls': [],
+ 'result': 'false'
+ },
+ {
+ 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on,
+ 'waffle_enabled': False,
+ 'youtube': '3_yD_cEKoCk',
+ 'hls': [],
+ 'result': 'false'
+ },
+ {
+ 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off,
+ 'waffle_enabled': True,
+ 'youtube': '3_yD_cEKoCk',
+ 'hls': ['https://hls.com/hls.m3u8'],
+ 'result': 'false'
+ },
+ )
+ @patch('xmodule.video_module.video_module.HLSPlaybackEnabledFlag.feature_enabled', Mock(return_value=True))
+ def test_deprecate_youtube_course_waffle_flag(self, data):
+ """
+ Tests various combinations of a `prioritize_hls` flag being set in waffle and overridden for a course.
+ """
+ metadata = {
+ 'html5_sources': ['http://youtu.be/3_yD_cEKoCk.mp4'] + data['hls'],
+ }
+ video_xml = ''.format(
+ data['youtube']
+ )
+ with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']):
+ with override_flag(DEPRECATE_YOUTUBE_FLAG, active=data['waffle_enabled']):
+ self.initialize_module(data=video_xml, metadata=metadata)
+ context = self.item_descriptor.render(STUDENT_VIEW).content
+ self.assertIn('"prioritizeHls": {}'.format(data['result']), context)
+
@attr(shard=7)
@ddt.ddt
@@ -2085,6 +2162,7 @@ class TestVideoWithBumper(TestVideo):
'completionEnabled': False,
'completionPercentage': 0.95,
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
+ 'prioritizeHls': False,
})),
'track': None,
'transcript_download_format': u'srt',
@@ -2107,6 +2185,7 @@ class TestAutoAdvanceVideo(TestVideo):
"""
Tests the server side of video auto-advance.
"""
+ maxDiff = None
CATEGORY = "video"
METADATA = {}
# Use temporary FEATURES in this test without affecting the original
@@ -2160,6 +2239,7 @@ class TestAutoAdvanceVideo(TestVideo):
'completionEnabled': False,
'completionPercentage': 0.95,
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
+ 'prioritizeHls': False,
})),
'track': None,
'transcript_download_format': u'srt',