diff --git a/lms/static/coffee/spec/helper.coffee b/lms/static/coffee/spec/helper.coffee index 3d9537f872..1bb92160ce 100644 --- a/lms/static/coffee/spec/helper.coffee +++ b/lms/static/coffee/spec/helper.coffee @@ -30,16 +30,17 @@ jasmine.stubRequests = -> jasmine.stubYoutubePlayer = -> YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode', - 'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo'] + 'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById', + 'playVideo', 'pauseVideo', 'seekTo'] -jasmine.stubVideoPlayer = (context, enableParts) -> +jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> enableParts = [enableParts] unless $.isArray(enableParts) suite = context.suite currentPartName = suite.description while suite = suite.parentSuite enableParts.push currentPartName - for part in ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider'] + for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider'] unless $.inArray(part, enableParts) >= 0 spyOn window, part @@ -48,7 +49,8 @@ jasmine.stubVideoPlayer = (context, enableParts) -> YT.Player = undefined context.video = new Video 'example', '.75:abc123,1.0:def456' jasmine.stubYoutubePlayer() - return new VideoPlayer context.video + if createPlayer + return new VideoPlayer context.video spyOn(window, 'onunload') diff --git a/lms/static/coffee/spec/modules/video/video_player_spec.coffee b/lms/static/coffee/spec/modules/video/video_player_spec.coffee index cdfedcb4f9..ebcb2cc009 100644 --- a/lms/static/coffee/spec/modules/video/video_player_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_player_spec.coffee @@ -1,6 +1,6 @@ describe 'VideoPlayer', -> beforeEach -> - jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @, [], false afterEach -> YT.Player = undefined @@ -11,69 +11,94 @@ describe 'VideoPlayer', -> spyOn YT, 'Player' $.fn.qtip.andCallFake -> $(this).data('qtip', true) - $('.video').append $('
') - @player = new VideoPlayer @video + $('.video').append $('
') - it 'instanticate current time to zero', -> - expect(@player.currentTime).toEqual 0 + describe 'always', -> + beforeEach -> + @player = new VideoPlayer @video - it 'set the element', -> - expect(@player.element).toBe '#video_example' + it 'instanticate current time to zero', -> + expect(@player.currentTime).toEqual 0 - it 'create video control', -> - expect(window.VideoControl).toHaveBeenCalledWith @player + it 'set the element', -> + expect(@player.element).toBe '#video_example' - it 'create video caption', -> - expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456' + it 'create video control', -> + expect(window.VideoControl).toHaveBeenCalledWith @player - it 'create video speed control', -> - expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0'] + it 'create video caption', -> + expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456' - it 'create video progress slider', -> - expect(window.VideoProgressSlider).toHaveBeenCalledWith @player + it 'create video speed control', -> + expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0'] - it 'create Youtube player', -> - expect(YT.Player).toHaveBeenCalledWith 'example' - playerVars: - controls: 0 - wmode: 'transparent' - rel: 0 - showinfo: 0 - enablejsapi: 1 - videoId: 'def456' - events: - onReady: @player.onReady - onStateChange: @player.onStateChange + it 'create video progress slider', -> + expect(window.VideoProgressSlider).toHaveBeenCalledWith @player - it 'bind to seek event', -> - expect($(@player)).toHandleWith 'seek', @player.onSeek + it 'create Youtube player', -> + expect(YT.Player).toHaveBeenCalledWith 'example' + playerVars: + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + videoId: 'def456' + events: + onReady: @player.onReady + onStateChange: @player.onStateChange - it 'bind to updatePlayTime event', -> - expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime + it 'bind to seek event', -> + expect($(@player)).toHandleWith 'seek', @player.onSeek - it 'bidn to speedChange event', -> - expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange + it 'bind to updatePlayTime event', -> + expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime - it 'bind to play event', -> - expect($(@player)).toHandleWith 'play', @player.onPlay + it 'bidn to speedChange event', -> + expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange - it 'bind to paused event', -> - expect($(@player)).toHandleWith 'pause', @player.onPause + it 'bind to play event', -> + expect($(@player)).toHandleWith 'play', @player.onPlay - it 'bind to ended event', -> - expect($(@player)).toHandleWith 'ended', @player.onPause + it 'bind to paused event', -> + expect($(@player)).toHandleWith 'pause', @player.onPause - it 'bind to key press', -> - expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen + it 'bind to ended event', -> + expect($(@player)).toHandleWith 'ended', @player.onPause - it 'bind to fullscreen switching button', -> - expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen + it 'bind to key press', -> + expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen + + it 'bind to fullscreen switching button', -> + console.debug $('.add-fullscreen') + expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen describe 'when not on a touch based device', -> + beforeEach -> + spyOn(window, 'onTouchBasedDevice').andReturn false + $('.add-fullscreen, .hide-subtitles').removeData 'qtip' + @player = new VideoPlayer @video + it 'add the tooltip to fullscreen and subtitle button', -> expect($('.add-fullscreen')).toHaveData 'qtip' expect($('.hide-subtitles')).toHaveData 'qtip' + it 'create video volume control', -> + expect(window.VideoVolumeControl).toHaveBeenCalledWith @player + + describe 'when on a touch based device', -> + beforeEach -> + spyOn(window, 'onTouchBasedDevice').andReturn true + $('.add-fullscreen, .hide-subtitles').removeData 'qtip' + @player = new VideoPlayer @video + + it 'does not add the tooltip to fullscreen and subtitle button', -> + expect($('.add-fullscreen')).not.toHaveData 'qtip' + expect($('.hide-subtitles')).not.toHaveData 'qtip' + + it 'does not create video volume control', -> + expect(window.VideoVolumeControl).not.toHaveBeenCalled() + describe 'onReady', -> beforeEach -> @video.embed() @@ -387,3 +412,17 @@ describe 'VideoPlayer', -> it 'delegate to the video', -> expect(@player.currentSpeed()).toEqual '3.0' + + describe 'volume', -> + beforeEach -> + @player = new VideoPlayer @video + @player.player.getVolume.andReturn 42 + + describe 'without value', -> + it 'return current volume', -> + expect(@player.volume()).toEqual 42 + + describe 'with value', -> + it 'set player volume', -> + @player.volume(60) + expect(@player.player.setVolume).toHaveBeenCalledWith(60) diff --git a/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee b/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee index ba7d439a94..ae78048c55 100644 --- a/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee @@ -18,7 +18,7 @@ describe 'VideoProgressSlider', -> stop: @slider.onStop it 'build the seek handle', -> - expect(@slider.handle).toBe '.ui-slider-handle' + expect(@slider.handle).toBe '.slider .ui-slider-handle' expect($.fn.qtip).toHaveBeenCalledWith content: "0:00" position: diff --git a/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee b/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee index 9d52ac9b0d..737460d1ce 100644 --- a/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee @@ -3,8 +3,6 @@ describe 'VideoSpeedControl', -> @player = jasmine.stubVideoPlayer @ $('.speeds').remove() - afterEach -> - describe 'constructor', -> describe 'always', -> beforeEach -> diff --git a/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee b/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee new file mode 100644 index 0000000000..cbdef03ef0 --- /dev/null +++ b/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee @@ -0,0 +1,102 @@ +describe 'VideoVolumeControl', -> + beforeEach -> + @player = jasmine.stubVideoPlayer @ + $('.volume').remove() + + describe 'constructor', -> + beforeEach -> + spyOn($.fn, 'slider') + @volumeControl = new VideoVolumeControl @player + + it 'initialize previousVolume to 100', -> + expect(@volumeControl.previousVolume).toEqual 100 + + it 'render the volume control', -> + expect($('.secondary-controls').html()).toContain """ +
+ +
+
+
+
+ """ + + it 'create the slider', -> + expect($.fn.slider).toHaveBeenCalledWith + orientation: "vertical" + range: "min" + min: 0 + max: 100 + value: 100 + change: @volumeControl.onChange + slide: @volumeControl.onChange + + it 'bind the volume control', -> + expect($(@player)).toHandleWith 'ready', @volumeControl.onReady + expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute + + expect($('.volume')).not.toHaveClass 'open' + $('.volume').mouseenter() + expect($('.volume')).toHaveClass 'open' + $('.volume').mouseleave() + expect($('.volume')).not.toHaveClass 'open' + + describe 'onReady', -> + beforeEach -> + @volumeControl = new VideoVolumeControl @player + spyOn $.fn, 'slider' + spyOn(@player, 'volume').andReturn 60 + @volumeControl.onReady() + + it 'set the max value of the slider', -> + expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 60 + + describe 'onChange', -> + beforeEach -> + spyOn @player, 'volume' + @volumeControl = new VideoVolumeControl @player + + describe 'when the new volume is more than 0', -> + beforeEach -> + @volumeControl.onChange undefined, value: 60 + + it 'set the player volume', -> + expect(@player.volume).toHaveBeenCalledWith 60 + + it 'remote muted class', -> + expect($('.volume')).not.toHaveClass 'muted' + + describe 'when the new volume is 0', -> + beforeEach -> + @volumeControl.onChange undefined, value: 0 + + it 'set the player volume', -> + expect(@player.volume).toHaveBeenCalledWith 0 + + it 'add muted class', -> + expect($('.volume')).toHaveClass 'muted' + + describe 'toggleMute', -> + beforeEach -> + spyOn @player, 'volume' + @volumeControl = new VideoVolumeControl @player + + describe 'when the current volume is more than 0', -> + beforeEach -> + @player.volume.andReturn 60 + @volumeControl.toggleMute() + + it 'save the previous volume', -> + expect(@volumeControl.previousVolume).toEqual 60 + + it 'set the player volume', -> + expect(@player.volume).toHaveBeenCalledWith 0 + + describe 'when the current volume is 0', -> + beforeEach -> + @player.volume.andReturn 0 + @volumeControl.previousVolume = 60 + @volumeControl.toggleMute() + + it 'set the player volume to previous volume', -> + expect(@player.volume).toHaveBeenCalledWith 60 diff --git a/lms/static/coffee/src/modules/video/video_player.coffee b/lms/static/coffee/src/modules/video/video_player.coffee index 28f87b2d53..20df378267 100644 --- a/lms/static/coffee/src/modules/video/video_player.coffee +++ b/lms/static/coffee/src/modules/video/video_player.coffee @@ -30,6 +30,7 @@ class @VideoPlayer render: -> new VideoControl @ new VideoCaption @, @video.youtubeId('1.0') + new VideoVolumeControl @ unless onTouchBasedDevice() new VideoSpeedControl @, @video.speeds new VideoProgressSlider @ @player = new YT.Player @video.id, @@ -132,3 +133,9 @@ class @VideoPlayer currentSpeed: -> @video.speed + + volume: (value) -> + if value? + @player.setVolume value + else + @player.getVolume() diff --git a/lms/static/coffee/src/modules/video/video_progress_slider.coffee b/lms/static/coffee/src/modules/video/video_progress_slider.coffee index 923f06b0c3..af0bbb53a6 100644 --- a/lms/static/coffee/src/modules/video/video_progress_slider.coffee +++ b/lms/static/coffee/src/modules/video/video_progress_slider.coffee @@ -17,7 +17,7 @@ class @VideoProgressSlider @buildHandle() buildHandle: -> - @handle = @$('.ui-slider-handle') + @handle = @$('.slider .ui-slider-handle') @handle.qtip content: "#{Time.format(@slider.slider('value'))}" position: diff --git a/lms/static/coffee/src/modules/video/video_volume_control.coffee b/lms/static/coffee/src/modules/video/video_volume_control.coffee new file mode 100644 index 0000000000..1c0d3440d5 --- /dev/null +++ b/lms/static/coffee/src/modules/video/video_volume_control.coffee @@ -0,0 +1,48 @@ +class @VideoVolumeControl + constructor: (@player) -> + @previousVolume = 100 + @render() + @bind() + + $: (selector) -> + @player.$(selector) + + bind: -> + $(@player).bind('ready', @onReady) + @$('.volume').mouseenter -> + $(this).addClass('open') + @$('.volume').mouseleave -> + $(this).removeClass('open') + @$('.volume>a').click(@toggleMute) + + render: -> + @$('.secondary-controls').prepend """ +
+ +
+
+
+
+ """ + @slider = @$('.volume-slider').slider + orientation: "vertical" + range: "min" + min: 0 + max: 100 + value: 100 + change: @onChange + slide: @onChange + + onReady: => + @slider.slider 'option', 'max', @player.volume() + + onChange: (event, ui) => + @player.volume ui.value + @$('.secondary-controls .volume').toggleClass 'muted', ui.value == 0 + + toggleMute: => + if @player.volume() > 0 + @previousVolume = @player.volume() + @slider.slider 'option', 'value', 0 + else + @slider.slider 'option', 'value', @previousVolume diff --git a/lms/static/images/mute.png b/lms/static/images/mute.png new file mode 100644 index 0000000000..70a604965c Binary files /dev/null and b/lms/static/images/mute.png differ diff --git a/lms/static/images/volume.png b/lms/static/images/volume.png new file mode 100644 index 0000000000..0b02e4b9e7 Binary files /dev/null and b/lms/static/images/volume.png differ diff --git a/lms/static/sass/courseware/_video.scss b/lms/static/sass/courseware/_video.scss index 7130cc8b7b..87092fdc54 100644 --- a/lms/static/sass/courseware/_video.scss +++ b/lms/static/sass/courseware/_video.scss @@ -286,6 +286,87 @@ section.course-content { } } + div.volume { + float: left; + position: relative; + + &.open { + .volume-slider-container { + display: block; + opacity: 1; + } + } + + &.muted { + &>a { + background: url('../images/mute.png') 10px center no-repeat; + } + } + + > a { + background: url('../images/volume.png') 10px center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + height: 46px; + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover, &:active, &:focus { + background-color: #444; + } + } + + .volume-slider-container { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 45px; + height: 125px; + margin-left: -1px; + z-index: 10; + + .volume-slider { + height: 100px; + border: 0; + width: 5px; + margin: 14px auto; + background: #666; + border: 1px solid #000; + @include box-shadow(0 1px 0 #333); + + a.ui-slider-handle { + background: $mit-red url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($mit-red, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($mit-red, 10%)); + cursor: pointer; + height: 15px; + left: -6px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + } + + .ui-slider-range { + background: #ddd; + } + } + } + } + a.add-fullscreen { background: url(../images/fullscreen.png) center no-repeat; border-right: 1px solid #000;