Merge pull request #105 from MITx/ps-video-volume
Add video volume control to video player
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -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 $('<div class="hide-subtitles" />')
|
||||
@player = new VideoPlayer @video
|
||||
$('.video').append $('<div class="add-fullscreen" /><div class="hide-subtitles" />')
|
||||
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,8 +3,6 @@ describe 'VideoSpeedControl', ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
$('.speeds').remove()
|
||||
|
||||
afterEach ->
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'always', ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -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 """
|
||||
<div class="volume">
|
||||
<a href="#"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 """
|
||||
<div class="volume">
|
||||
<a href="#"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@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
|
||||
BIN
lms/static/images/mute.png
Normal file
BIN
lms/static/images/mute.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 B |
BIN
lms/static/images/volume.png
Normal file
BIN
lms/static/images/volume.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 B |
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user