diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_hls.html b/common/lib/xmodule/xmodule/js/fixtures/video_hls.html index e5347b67db..4ff098c226 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_hls.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_hls.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index c199efd959..3d697b0e44 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -4,7 +4,7 @@
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 477328ab8c..938c2a3bee 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 @@ -23,7 +23,8 @@ state.el.trigger('ready'); expect(Logger.log).toHaveBeenCalledWith('load_video', { id: 'id', - code: this.code + code: this.code, + duration: this.duration }); }); @@ -33,7 +34,8 @@ expect(Logger.log).toHaveBeenCalledWith('play_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeFalsy(); }); @@ -49,7 +51,8 @@ expect(Logger.log).toHaveBeenCalledWith('pause_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); }); @@ -61,7 +64,8 @@ code: this.code, current_time: 10, old_speed: '1.0', - new_speed: '2.0' + new_speed: '2.0', + duration: this.duration }); }); @@ -72,7 +76,8 @@ code: this.code, old_time: 0, new_time: 1, - type: 'any' + type: 'any', + duration: this.duration }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); }); @@ -88,7 +93,8 @@ expect(Logger.log).toHaveBeenCalledWith('stop_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); @@ -97,7 +103,8 @@ expect(Logger.log).toHaveBeenCalledWith('stop_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy(); }); @@ -107,7 +114,8 @@ expect(Logger.log).toHaveBeenCalledWith('skip_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); }); @@ -116,7 +124,8 @@ expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', { id: 'id', code: this.code, - currentTime: 10 + currentTime: 10, + duration: this.duration }); }); @@ -124,7 +133,8 @@ state.el.trigger('language_menu:show'); expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.shown', { id: 'id', - code: this.code + code: this.code, + duration: this.duration }); }); @@ -133,7 +143,8 @@ expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.hidden', { id: 'id', code: this.code, - language: 'en' + language: 'en', + duration: this.duration }); }); @@ -142,7 +153,8 @@ expect(Logger.log).toHaveBeenCalledWith('show_transcript', { id: 'id', code: this.code, - current_time: 10 + current_time: 10, + duration: this.duration }); }); @@ -151,7 +163,8 @@ expect(Logger.log).toHaveBeenCalledWith('hide_transcript', { id: 'id', code: this.code, - current_time: 10 + current_time: 10, + duration: this.duration }); }); @@ -160,7 +173,8 @@ expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.shown', { id: 'id', code: this.code, - current_time: 10 + current_time: 10, + duration: this.duration }); }); @@ -169,7 +183,8 @@ expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.hidden', { id: 'id', code: this.code, - current_time: 10 + current_time: 10, + duration: this.duration }); }); @@ -208,6 +223,7 @@ describe('html5 encoding only', function() { beforeEach(function(done) { this.code = 'html5'; + this.duration = 111; state = jasmine.initializePlayer('video_html5.html'); done(); }); @@ -217,6 +233,7 @@ describe('hls encoding', function() { beforeEach(function(done) { this.code = 'hls'; + this.duration = 111; state = jasmine.initializeHLSPlayer(); done(); }); 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 1c61ba6381..8950b5bcce 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 @@ -1011,6 +1011,21 @@ function(VideoPlayer, HLS) { }); }); + describe('Video duration', function() { + beforeEach(function() { + state = jasmine.initializePlayer(); + spyOn(state.videoPlayer, 'duration').and.returnValue(61); + }); + + it('overrides the duration if not set', function(done) { + jasmine.waitUntil(function() { + return state.duration !== undefined; + }).then(function() { + expect(state.duration).toEqual(61); + }).always(done); + }); + }); + describe('Overlay Play Button', function() { var playButtonOverlaySelector = '.video-wrapper .btn-play.fa.fa-youtube-play.fa-2x'; beforeEach(function() { 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 f99b13f634..825976e808 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -569,6 +569,7 @@ function(VideoPlayer, i18n, moment, _) { this.config.speed || this.config.generalSpeed ); this.htmlPlayerLoaded = false; + this.duration = this.metadata.duration; _setConfigurations(this); diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 82bd43bc52..f9b327ea38 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -653,6 +653,9 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _) { var duration = this.videoPlayer.duration(), time = this.videoPlayer.figureOutStartingTime(duration); + // this.duration will be set initially only if duration is coming from edx-val + this.duration = this.duration || duration; + if (time > 0 && this.videoPlayer.goToStartTime) { this.videoPlayer.seekTo(time); } diff --git a/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js index b00a19f4d1..c07e91b81c 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js @@ -143,7 +143,8 @@ var logInfo = _.extend({ id: this.state.id, // eslint-disable-next-line no-nested-ternary - code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5' + code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5', + duration: this.state.duration }, data, this.options.data); Logger.log(eventName, logInfo); } diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index a6d6d4a7a1..50dcb950cd 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -211,6 +211,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, download_video_link = None branding_info = None youtube_streams = "" + video_duration = None # Determine if there is an alternative source for this video # based on user locale. This exists to support cases where @@ -253,7 +254,11 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, if val_video_urls["youtube"]: youtube_streams = "1.00:{}".format(val_video_urls["youtube"]) - except edxval_api.ValInternalError: + # get video duration + video_data = edxval_api.get_video_info(self.edx_video_id.strip()) + video_duration = video_data.get('duration') + + except (edxval_api.ValInternalError, edxval_api.ValVideoNotFoundError): # VAL raises this exception if it can't find data for the edx video ID. This can happen if the # course data is ported to a machine that does not have the VAL data. So for now, pass on this # exception and fallback to whatever we find in the VideoDescriptor. @@ -326,6 +331,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, 'sub': self.sub, 'sources': sources, 'poster': poster, + 'duration': video_duration, # This won't work when we move to data that # isn't on the filesystem 'captionDataDir': getattr(self, 'data_dir', None), diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index e248519696..b940ddc9c8 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -69,6 +69,7 @@ class TestVideoYouTube(TestVideo): 'streams': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg', 'sub': 'a_sub_file.srt.sjson', 'sources': sources, + 'duration': None, 'poster': None, 'captionDataDir': None, 'showCaptions': 'true', @@ -149,6 +150,7 @@ class TestVideoNonYouTube(TestVideo): 'streams': '1.00:3_yD_cEKoCk', 'sub': 'a_sub_file.srt.sjson', 'sources': sources, + 'duration': None, 'poster': None, 'captionDataDir': None, 'showCaptions': 'true', @@ -206,6 +208,7 @@ class TestGetHtmlMethod(BaseTestXmodule): 'streams': '1.00:3_yD_cEKoCk', 'sub': 'a_sub_file.srt.sjson', 'sources': '[]', + 'duration': 111.0, 'poster': None, 'captionDataDir': None, 'showCaptions': 'true', @@ -306,6 +309,7 @@ class TestGetHtmlMethod(BaseTestXmodule): for data in cases: metadata = self.default_metadata_dict metadata['sources'] = sources + metadata['duration'] = None DATA = SOURCE_XML.format( download_track=data['download_track'], track=data['track'], @@ -424,6 +428,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ], 'poster': 'null', } + initial_context['metadata']['duration'] = None for data in cases: DATA = SOURCE_XML.format( @@ -674,7 +679,7 @@ class TestGetHtmlMethod(BaseTestXmodule): result = create_video( dict( client_video_id='A Client Video id', - duration=111, + duration=111.0, edx_video_id=edx_video_id, status='test', encoded_videos=encoded_videos, @@ -835,6 +840,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ], 'poster': 'null', } + initial_context['metadata']['duration'] = None for data in cases: DATA = SOURCE_XML.format( @@ -1542,7 +1548,7 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): create_video({ 'edx_video_id': self.descriptor.edx_video_id, 'client_video_id': 'test_client_video_id', - 'duration': 111, + 'duration': 111.0, 'status': 'dummy', 'encoded_videos': [{ 'profile': 'mobile', @@ -1643,7 +1649,7 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): self.assertEqual(video.edx_video_id, 'test_edx_video_id') video_data = get_video_info(video.edx_video_id) self.assertEqual(video_data['client_video_id'], 'test_client_video_id') - self.assertEqual(video_data['duration'], 111) + self.assertEqual(video_data['duration'], 111.0) self.assertEqual(video_data['status'], 'imported') self.assertEqual(video_data['courses'], [{id_generator.target_course_id: None}]) self.assertEqual(video_data['encoded_videos'][0]['profile'], 'mobile') @@ -1788,6 +1794,7 @@ class TestVideoWithBumper(TestVideo): 'sub': 'a_sub_file.srt.sjson', 'sources': sources, 'poster': None, + 'duration': None, 'captionDataDir': None, 'showCaptions': 'true', 'generalSpeed': 1.0,