Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
8c26fc68
Commit
8c26fc68
authored
Apr 16, 2014
by
polesye
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor video volume control.
parent
d01af063
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
645 additions
and
342 deletions
+645
-342
common/lib/xmodule/xmodule/css/video/display.scss
+2
-2
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+5
-14
common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
+203
-99
common/lib/xmodule/xmodule/js/src/video/00_i18n.js
+31
-0
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+5
-6
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
+4
-2
common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js
+3
-3
common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
+388
-214
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+1
-0
common/lib/xmodule/xmodule/video_module/video_module.py
+1
-0
lms/templates/video.html
+2
-2
No files found.
common/lib/xmodule/xmodule/css/video/display.scss
View file @
8c26fc68
...
...
@@ -458,14 +458,14 @@ div.video {
float
:
left
;
position
:
relative
;
&
.
open
{
&
.
is-opened
{
.volume-slider-container
{
display
:
block
;
opacity
:
1
;
}
}
&
.muted
{
&
.
is-
muted
{
&
>
a
{
background-image
:
url('../images/mute.png')
;
}
...
...
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
View file @
8c26fc68
...
...
@@ -141,8 +141,7 @@ function (VideoPlayer) {
state
.
videoEl
=
$
(
'video, iframe'
);
expect
(
state
.
videoVolumeControl
).
toBeUndefined
();
expect
(
state
.
el
.
find
(
'div.volume'
)).
not
.
toExist
();
expect
(
state
.
el
.
find
(
'.volume'
)).
not
.
toExist
();
});
});
});
...
...
@@ -450,7 +449,6 @@ function (VideoPlayer) {
},
'currentTime got updated'
,
10000
);
});
});
});
describe
(
'when the video is not playing'
,
function
()
{
beforeEach
(
function
()
{
...
...
@@ -472,20 +470,13 @@ function (VideoPlayer) {
expect
(
state
.
videoPlayer
.
setPlaybackRate
.
calls
.
length
)
.
toEqual
(
1
);
});
it
(
'video has a correct volume'
,
function
()
{
spyOn
(
state
.
videoPlayer
.
player
,
'setVolume'
);
state
.
currentVolume
=
'0.26'
;
state
.
videoPlayer
.
onPlay
();
expect
(
state
.
videoPlayer
.
player
.
setVolume
)
.
toHaveBeenCalledWith
(
'0.26'
);
});
});
describe
(
'onVolumeChange'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
state
.
videoPlayer
.
onReady
();
state
.
videoEl
=
$
(
'video, iframe'
);
});
...
...
@@ -502,10 +493,10 @@ function (VideoPlayer) {
it
(
'video has a correct volume'
,
function
()
{
spyOn
(
state
.
videoPlayer
.
player
,
'setVolume'
);
state
.
currentVolume
=
'0.26'
;
state
.
videoPlayer
.
onPlay
(
);
state
.
videoVolumeControl
.
volume
=
26
;
state
.
el
.
trigger
(
'play'
);
expect
(
state
.
videoPlayer
.
player
.
setVolume
)
.
toHaveBeenCalledWith
(
'0.26'
);
.
toHaveBeenCalledWith
(
26
);
});
});
});
...
...
common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
View file @
8c26fc68
(
function
(
undefined
)
{
describe
(
'VideoVolumeControl'
,
function
()
{
var
state
,
oldOTBD
;
(
function
()
{
'use strict'
;
describe
(
'VideoVolumeControl'
,
function
()
{
var
state
,
oldOTBD
,
volumeControl
;
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
...
...
@@ -14,56 +15,57 @@
state
.
storage
.
clear
();
});
it
(
'Volume level has correct value even if cookie is broken'
,
function
()
{
$
.
cookie
.
andReturn
(
'broken_cookie'
);
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
expect
(
volumeControl
.
volume
).
toEqual
(
100
);
});
describe
(
'constructor'
,
function
()
{
beforeEach
(
function
()
{
spyOn
(
$
.
fn
,
'slider'
).
andCallThrough
();
$
.
cookie
.
andReturn
(
'75'
);
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
it
(
'initialize currentV
olume to 75%'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
currentV
olume
).
toEqual
(
75
);
it
(
'initialize v
olume to 75%'
,
function
()
{
expect
(
volumeControl
.
v
olume
).
toEqual
(
75
);
});
it
(
'render the volume control'
,
function
()
{
expect
(
state
.
videoControl
.
secondaryControlsEl
.
html
())
.
toContain
(
"<div class=
\"
volume
\"
>
\
n"
);
.
toContain
(
'<div class="volume">
\
n'
);
});
it
(
'create the slider'
,
function
()
{
expect
(
$
.
fn
.
slider
).
toHaveBeenCalledWith
(
{
orientation
:
"vertical"
,
range
:
"min"
,
expect
(
$
.
fn
.
slider
.
calls
[
2
].
args
).
toEqual
([
{
orientation
:
'vertical'
,
range
:
'min'
,
min
:
0
,
max
:
100
,
value
:
state
.
videoVolumeControl
.
currentVolume
,
change
:
state
.
videoVolumeControl
.
onChange
,
slide
:
state
.
videoVolumeControl
.
onChange
});
slide
:
jasmine
.
any
(
Function
)
}]);
expect
(
$
.
fn
.
slider
).
toHaveBeenCalledWith
(
'value'
,
volumeControl
.
volume
);
});
it
(
'add ARIA attributes to slider handle'
,
function
()
{
var
sliderHandle
=
$
(
'div.volume-slider>a.ui-slider-handle'
),
arr
=
[
'Muted'
,
'Very low'
,
'Low'
,
'Average'
,
'Loud'
,
'Very loud'
,
'Maximum'
];
it
(
'add ARIA attributes to live region'
,
function
()
{
var
liveRegion
=
$
(
'.video-live-region'
);
expect
(
sliderHandle
).
toHaveAttrs
({
'role'
:
'slider'
,
'title'
:
'Volume'
,
'aria-disabled'
:
'false'
,
'aria-valuemin'
:
'0'
,
'aria-valuemax'
:
'100'
expect
(
liveRegion
).
toHaveAttrs
({
'role'
:
'status'
,
'aria-live'
:
'polite'
,
'aria-atomic'
:
'false'
});
expect
(
sliderHandle
.
attr
(
'aria-valuenow'
)).
toBeInRange
(
0
,
100
);
expect
(
sliderHandle
.
attr
(
'aria-valuetext'
)).
toBeInArray
(
arr
);
});
it
(
'add ARIA attributes to volume control'
,
function
()
{
var
volumeControl
=
$
(
'div.volume>
a'
);
var
button
=
$
(
'.volume >
a'
);
expect
(
volumeControl
).
toHaveAttrs
({
expect
(
button
).
toHaveAttrs
({
'role'
:
'button'
,
'title'
:
'Volume'
,
'aria-disabled'
:
'false'
...
...
@@ -71,139 +73,241 @@
});
it
(
'bind the volume control'
,
function
()
{
expect
(
$
(
'.volume>a'
)).
toHandleWith
(
'click'
,
state
.
videoVolumeControl
.
toggleMute
);
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'open'
);
var
button
=
$
(
'.volume > a'
);
expect
(
button
).
toHandle
(
'keydown'
);
expect
(
button
).
toHandle
(
'mousedown'
);
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'is-opened'
);
$
(
'.volume'
).
mouseenter
();
expect
(
$
(
'.volume'
)).
toHaveClass
(
'open
'
);
expect
(
$
(
'.volume'
)).
toHaveClass
(
'is-opened
'
);
$
(
'.volume'
).
mouseleave
();
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'open'
);
});
});
describe
(
'onChange'
,
function
()
{
var
initialData
=
[{
range
:
'Muted'
,
value
:
0
,
expectation
:
'Muted'
},
{
range
:
'in ]0,20]'
,
value
:
10
,
expectation
:
'Very low'
},
{
range
:
'in ]20,40]'
,
value
:
30
,
expectation
:
'Low'
},
{
range
:
'in ]40,60]'
,
value
:
50
,
expectation
:
'Average'
},
{
range
:
'in ]60,80]'
,
value
:
70
,
expectation
:
'Loud'
},
{
range
:
'in ]80,100['
,
value
:
90
,
expectation
:
'Very loud'
},
{
range
:
'Maximum'
,
value
:
100
,
expectation
:
'Maximum'
}];
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'is-opened'
);
});
});
describe
(
'setVolume'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
this
.
addMatchers
({
assertLiveRegionState
:
function
(
volume
,
expectation
)
{
var
region
=
$
(
'.video-live-region'
);
var
getExpectedText
=
function
(
text
)
{
return
text
+
' Volume.'
;
};
this
.
actual
.
setVolume
(
volume
,
true
,
true
);
return
region
.
text
()
===
getExpectedText
(
expectation
);
}
});
});
it
(
'update is not called, if new volume equals current'
,
function
()
{
volumeControl
.
volume
=
60
;
spyOn
(
volumeControl
,
'updateSliderView'
);
volumeControl
.
setVolume
(
60
,
false
,
true
);
expect
(
volumeControl
.
updateSliderView
).
not
.
toHaveBeenCalled
();
});
it
(
'volume is changed on sliding'
,
function
()
{
volumeControl
.
onSlideHandler
(
null
,
{
value
:
99
});
expect
(
volumeControl
.
volume
).
toBe
(
99
);
});
describe
(
'when the new volume is more than 0'
,
function
()
{
beforeEach
(
function
()
{
state
.
videoVolumeControl
.
onChange
(
void
0
,
{
value
:
60
});
volumeControl
.
setVolume
(
60
,
false
,
true
);
});
it
(
'set the player volume'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
currentVolume
).
toEqual
(
60
);
expect
(
volumeControl
.
volume
).
toEqual
(
60
);
});
it
(
'remove muted class'
,
function
()
{
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'is-muted'
);
});
});
it
(
'remote muted class'
,
function
()
{
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'muted'
);
describe
(
'when the new volume is more than 0, but was 0'
,
function
()
{
it
(
'remove muted class'
,
function
()
{
volumeControl
.
setVolume
(
0
,
false
,
true
);
expect
(
$
(
'.volume'
)).
toHaveClass
(
'is-muted'
);
state
.
el
.
trigger
(
'volumechange'
,
[
20
]);
expect
(
$
(
'.volume'
)).
not
.
toHaveClass
(
'is-muted'
);
});
});
describe
(
'when the new volume is 0'
,
function
()
{
beforeEach
(
function
()
{
state
.
videoVolumeControl
.
onChange
(
void
0
,
{
value
:
0
});
volumeControl
.
setVolume
(
0
,
false
,
true
);
});
it
(
'set the player volume'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
currentV
olume
).
toEqual
(
0
);
expect
(
volumeControl
.
v
olume
).
toEqual
(
0
);
});
it
(
'add muted class'
,
function
()
{
expect
(
$
(
'.volume'
)).
toHaveClass
(
'
muted'
);
expect
(
$
(
'.volume'
)).
toHaveClass
(
'is-
muted'
);
});
});
$
.
each
(
initialData
,
function
(
index
,
data
)
{
describe
(
'when the new volume is '
+
data
.
range
,
function
()
{
beforeEach
(
function
()
{
state
.
videoVolumeControl
.
onChange
(
void
0
,
{
value
:
data
.
value
it
(
'when the new volume is Muted'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
0
,
'Muted'
);
});
it
(
'when the new volume is in ]0,20]'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
10
,
'Very low'
);
});
it
(
'when the new volume is in ]20,40]'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
30
,
'Low'
);
});
it
(
'changes ARIA attributes'
,
function
()
{
var
sliderHandle
=
$
(
'div.volume-slider>a.ui-slider-handle'
);
it
(
'when the new volume is in ]40,60]'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
50
,
'Average'
);
});
it
(
'when the new volume is in ]60,80]'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
70
,
'Loud'
);
});
it
(
'when the new volume is in ]80,100['
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
90
,
'Very loud'
);
});
expect
(
sliderHandle
).
toHaveAttrs
({
'aria-valuenow'
:
data
.
value
.
toString
(
10
),
'aria-valuetext'
:
data
.
expectation
it
(
'when the new volume is Maximum'
,
function
()
{
expect
(
volumeControl
).
assertLiveRegionState
(
100
,
'Maximum'
);
});
});
describe
(
'increaseVolume'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
it
(
'volume is increased correctly'
,
function
()
{
volumeControl
.
volume
=
60
;
state
.
el
.
trigger
(
jQuery
.
Event
(
"keydown"
,
{
keyCode
:
$
.
ui
.
keyCode
.
UP
}));
expect
(
volumeControl
.
volume
).
toEqual
(
80
);
});
it
(
'volume level is not changed if it is already max'
,
function
()
{
volumeControl
.
volume
=
100
;
volumeControl
.
increaseVolume
();
expect
(
volumeControl
.
volume
).
toEqual
(
100
);
});
});
describe
(
'decreaseVolume'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
it
(
'volume is decreased correctly'
,
function
()
{
volumeControl
.
volume
=
60
;
state
.
el
.
trigger
(
jQuery
.
Event
(
"keydown"
,
{
keyCode
:
$
.
ui
.
keyCode
.
DOWN
}));
expect
(
volumeControl
.
volume
).
toEqual
(
40
);
});
it
(
'volume level is not changed if it is already min'
,
function
()
{
volumeControl
.
volume
=
0
;
volumeControl
.
decreaseVolume
();
expect
(
volumeControl
.
volume
).
toEqual
(
0
);
});
});
describe
(
'toggleMute'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
describe
(
'when the current volume is more than 0'
,
function
()
{
beforeEach
(
function
()
{
state
.
videoVolumeControl
.
currentV
olume
=
60
;
state
.
videoVolumeControl
.
buttonEl
.
trigger
(
'click
'
);
volumeControl
.
v
olume
=
60
;
volumeControl
.
button
.
trigger
(
'mousedown
'
);
});
it
(
'save the previous volume'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
previous
Volume
).
toEqual
(
60
);
expect
(
volumeControl
.
stored
Volume
).
toEqual
(
60
);
});
it
(
'set the player volume'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
currentV
olume
).
toEqual
(
0
);
expect
(
volumeControl
.
v
olume
).
toEqual
(
0
);
});
});
describe
(
'when the current volume is 0'
,
function
()
{
beforeEach
(
function
()
{
state
.
videoVolumeControl
.
currentV
olume
=
0
;
state
.
videoVolumeControl
.
previous
Volume
=
60
;
state
.
videoVolumeControl
.
buttonEl
.
trigger
(
'click
'
);
volumeControl
.
v
olume
=
0
;
volumeControl
.
stored
Volume
=
60
;
volumeControl
.
button
.
trigger
(
'mousedown
'
);
});
it
(
'set the player volume to previous volume'
,
function
()
{
expect
(
state
.
videoVolumeControl
.
currentV
olume
).
toEqual
(
60
);
expect
(
volumeControl
.
v
olume
).
toEqual
(
60
);
});
});
});
describe
(
'keyDownHandler'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
var
assertVolumeIsNotChanged
=
function
(
eventObject
)
{
volumeControl
.
volume
=
60
;
state
.
el
.
trigger
(
jQuery
.
Event
(
"keydown"
,
eventObject
));
expect
(
volumeControl
.
volume
).
toEqual
(
60
);
};
it
(
'nothing happens if ALT+keyUp are pushed down'
,
function
()
{
assertVolumeIsNotChanged
({
keyCode
:
$
.
ui
.
keyCode
.
UP
,
altKey
:
true
});
});
it
(
'nothing happens if SHIFT+keyUp are pushed down'
,
function
()
{
assertVolumeIsNotChanged
({
keyCode
:
$
.
ui
.
keyCode
.
UP
,
shiftKey
:
true
});
});
it
(
'nothing happens if SHIFT+keyDown are pushed down'
,
function
()
{
assertVolumeIsNotChanged
({
keyCode
:
$
.
ui
.
keyCode
.
DOWN
,
shiftKey
:
true
});
});
})
describe
(
'keyDownButtonHandler'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
volumeControl
=
state
.
videoVolumeControl
;
});
it
(
'nothing happens if ALT+ENTER are pushed down'
,
function
()
{
var
isMuted
=
volumeControl
.
getMuteStatus
();
$
(
'.volume > a'
).
trigger
(
jQuery
.
Event
(
"keydown"
,
{
keyCode
:
$
.
ui
.
keyCode
.
ENTER
,
altKey
:
true
}));
expect
(
volumeControl
.
getMuteStatus
()).
toEqual
(
isMuted
);
});
})
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/src/video/00_i18n.js
0 → 100644
View file @
8c26fc68
(
function
(
define
)
{
'use strict'
;
define
(
'video/00_i18n.js'
,
[],
function
()
{
/**
* i18n module.
* @exports video/00_i18n.js
* @return {object}
*/
return
{
'Volume'
:
gettext
(
'Volume'
),
// Translators: Volume level equals 0%.
'Muted'
:
gettext
(
'Muted'
),
// Translators: Volume level in range ]0,20]%
'Very low'
:
gettext
(
'Very low'
),
// Translators: Volume level in range ]20,40]%
'Low'
:
gettext
(
'Low'
),
// Translators: Volume level in range ]40,60]%
'Average'
:
gettext
(
'Average'
),
// Translators: Volume level in range ]60,80]%
'Loud'
:
gettext
(
'Loud'
),
// Translators: Volume level in range ]80,99]%
'Very loud'
:
gettext
(
'Very loud'
),
// Translators: Volume level equals 100%.
'Maximum'
:
gettext
(
'Maximum'
)
};
});
}(
RequireJS
.
define
));
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
View file @
8c26fc68
...
...
@@ -14,8 +14,8 @@
define
(
'video/01_initialize.js'
,
[
'video/03_video_player.js'
,
'video/00_video_storage.js'
],
function
(
VideoPlayer
,
VideoStorage
)
{
[
'video/03_video_player.js'
,
'video/00_video_storage.js'
,
'video/00_i18n.js'
],
function
(
VideoPlayer
,
VideoStorage
,
i18n
)
{
/**
* @function
*
...
...
@@ -39,7 +39,7 @@ function (VideoPlayer, VideoStorage) {
return
false
;
}
_initializeModules
(
state
)
_initializeModules
(
state
,
i18n
)
.
done
(
function
()
{
// On iPad ready state occurs just after start playing.
// We hide controls before video starts playing.
...
...
@@ -341,11 +341,11 @@ function (VideoPlayer, VideoStorage) {
state
.
captionHideTimeout
=
null
;
}
function
_initializeModules
(
state
)
{
function
_initializeModules
(
state
,
i18n
)
{
var
dfd
=
$
.
Deferred
(),
modulesList
=
$
.
map
(
state
.
modules
,
function
(
module
)
{
if
(
$
.
isFunction
(
module
))
{
return
module
(
state
);
return
module
(
state
,
i18n
);
}
else
if
(
$
.
isPlainObject
(
module
))
{
return
module
;
}
...
...
@@ -494,7 +494,6 @@ function (VideoPlayer, VideoStorage) {
__dfd__
:
__dfd__
,
el
:
el
,
container
:
container
,
currentVolume
:
100
,
id
:
id
,
isFullScreen
:
false
,
isTouch
:
isTouch
,
...
...
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
View file @
8c26fc68
...
...
@@ -98,7 +98,6 @@ function (HTML5Video, Resizer) {
if
(
!
state
.
isFlashMode
()
&&
state
.
speed
!=
'1.0'
)
{
state
.
videoPlayer
.
setPlaybackRate
(
state
.
speed
);
}
state
.
videoPlayer
.
player
.
setVolume
(
state
.
currentVolume
);
});
if
(
state
.
isYoutubeType
())
{
...
...
@@ -584,6 +583,10 @@ function (HTML5Video, Resizer) {
_this
.
videoPlayer
.
onSpeedChange
(
speed
);
});
this
.
el
.
on
(
'volumechange volumechange:silent'
,
function
(
event
,
volume
)
{
_this
.
videoPlayer
.
onVolumeChange
(
volume
);
});
this
.
videoPlayer
.
log
(
'load_video'
);
availablePlaybackRates
=
this
.
videoPlayer
.
player
...
...
@@ -919,7 +922,6 @@ function (HTML5Video, Resizer) {
function
onVolumeChange
(
volume
)
{
this
.
videoPlayer
.
player
.
setVolume
(
volume
);
this
.
el
.
trigger
(
'volumechange'
,
arguments
);
}
});
...
...
common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js
View file @
8c26fc68
...
...
@@ -64,9 +64,9 @@ function () {
state
.
videoQualityControl
.
el
.
on
(
'click'
,
state
.
videoQualityControl
.
toggleQuality
);
state
.
el
.
on
(
'play'
,
_
.
once
(
state
.
videoQualityControl
.
fetchAvailableQualities
)
);
state
.
el
.
on
(
'play'
,
_
.
once
(
function
()
{
state
.
videoQualityControl
.
fetchAvailableQualities
();
})
);
}
// ***************************************************************
...
...
common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
View file @
8c26fc68
(
function
(
requirejs
,
require
,
define
)
{
(
function
(
define
)
{
'use strict'
;
// VideoVolumeControl module.
define
(
'video/07_video_volume_control.js'
,
[],
function
()
{
// VideoVolumeControl() function - what this module "exports".
return
function
(
state
)
{
var
dfd
=
$
.
Deferred
();
if
(
state
.
isTouch
)
{
// iOS doesn't support volume change
state
.
el
.
find
(
'div.volume'
).
remove
();
dfd
.
resolve
();
return
dfd
.
promise
(
);
'video/07_video_volume_control.js'
,
[],
function
()
{
/**
* Video volume control module.
* @exports video/07_video_volume_control.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @return {jquery Promise}
*/
var
VolumeControl
=
function
(
state
,
i18n
)
{
if
(
!
(
this
instanceof
VolumeControl
))
{
return
new
VolumeControl
(
state
,
i18n
);
}
state
.
videoVolumeControl
=
{};
this
.
state
=
state
;
this
.
state
.
videoVolumeControl
=
this
;
this
.
i18n
=
i18n
;
this
.
initialize
();
_makeFunctionsPublic
(
state
);
_renderElements
(
state
);
_bindHandlers
(
state
);
dfd
.
resolve
();
return
dfd
.
promise
();
return
$
.
Deferred
().
resolve
().
promise
();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called, these functions will
// get the 'state' object as a context.
function
_makeFunctionsPublic
(
state
)
{
var
methodsDict
=
{
onChange
:
onChange
,
toggleMute
:
toggleMute
};
VolumeControl
.
prototype
=
{
/** Minimum value for the volume slider. */
min
:
0
,
/** Maximum value for the volume slider. */
max
:
100
,
/** Step to increase/decrease volume level via keyboard. */
step
:
20
,
state
.
bindTo
(
methodsDict
,
state
.
videoVolumeControl
,
state
);
}
/** Initializes the module. */
initialize
:
function
()
{
var
volume
;
this
.
el
=
this
.
state
.
el
.
find
(
'.volume'
);
// function _renderElements(state)
//
// Create any necessary DOM elements, attach them, and set their 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.
function
_renderElements
(
state
)
{
var
volumeControl
=
state
.
videoVolumeControl
,
element
=
state
.
el
.
find
(
'div.volume'
),
button
=
element
.
find
(
'a'
),
volumeSlider
=
element
.
find
(
'.volume-slider'
),
// Figure out what the current volume is. If no information about
// volume level could be retrieved, then we will use the default 100
// level (full volume).
currentVolume
=
parseInt
(
$
.
cookie
(
'video_player_volume_level'
),
10
),
// Set it up so that muting/unmuting works correctly.
previousVolume
=
100
,
slider
,
buttonStr
,
volumeSliderHandleEl
;
if
(
!
isFinite
(
currentVolume
))
{
currentVolume
=
100
;
if
(
this
.
state
.
isTouch
)
{
// iOS doesn't support volume change
this
.
el
.
remove
();
return
false
;
}
// Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe.
this
.
state
.
el
.
find
(
'iframe'
).
attr
(
'tabindex'
,
-
1
);
this
.
button
=
this
.
el
.
children
(
'a'
);
this
.
cookie
=
new
CookieManager
(
this
.
min
,
this
.
max
);
this
.
a11y
=
new
Accessibility
(
this
.
button
,
this
.
min
,
this
.
max
,
this
.
i18n
);
volume
=
this
.
cookie
.
getVolume
();
this
.
storedVolume
=
this
.
max
;
this
.
render
();
this
.
bindHandlers
();
this
.
setVolume
(
volume
,
true
,
false
);
this
.
checkMuteButtonStatus
(
volume
);
},
slider
=
volumeSlider
.
slider
({
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render
:
function
()
{
var
container
=
this
.
el
.
find
(
'.volume-slider'
);
this
.
volumeSlider
=
container
.
slider
({
orientation
:
'vertical'
,
range
:
'min'
,
min
:
0
,
max
:
100
,
value
:
currentVolume
,
change
:
volumeControl
.
onChange
,
slide
:
volumeControl
.
onChange
min
:
this
.
min
,
max
:
this
.
max
,
slide
:
this
.
onSlideHandler
.
bind
(
this
)
});
element
.
toggleClass
(
'muted'
,
currentVolume
===
0
);
// ARIA
// Let screen readers know that:
// This anchor behaves as a button named 'Volume'.
buttonStr
=
(
currentVolume
===
0
)
?
'Volume muted'
:
'Volume'
;
// We add the aria-label attribute because the title attribute cannot be
// read.
button
.
attr
(
'aria-label'
,
gettext
(
buttonStr
));
// Let screen readers know that this anchor, representing the slider
// handle, behaves as a slider named 'volume'.
volumeSliderHandleEl
=
slider
.
find
(
'.ui-slider-handle'
);
volumeSliderHandleEl
.
attr
({
'role'
:
'slider'
,
'title'
:
gettext
(
'Volume'
),
'aria-disabled'
:
false
,
'aria-valuemin'
:
slider
.
slider
(
'option'
,
'min'
),
'aria-valuemax'
:
slider
.
slider
(
'option'
,
'max'
),
'aria-valuenow'
:
slider
.
slider
(
'option'
,
'value'
),
'aria-valuetext'
:
getVolumeDescription
(
slider
.
slider
(
'option'
,
'value'
))
// We provide an independent behavior to adjust volume level.
// Therefore, we do not need redundant focusing on slider in TAB
// order.
container
.
find
(
'a'
).
attr
(
'tabindex'
,
-
1
);
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers
:
function
()
{
this
.
state
.
el
.
on
({
'keydown'
:
this
.
keyDownHandler
.
bind
(
this
),
'play'
:
_
.
once
(
this
.
updateVolumeSilently
.
bind
(
this
)),
'volumechange'
:
this
.
onVolumeChangeHandler
.
bind
(
this
)
});
this
.
el
.
on
({
'mouseenter'
:
this
.
openMenu
.
bind
(
this
),
'mouseleave'
:
this
.
closeMenu
.
bind
(
this
)
});
this
.
button
.
on
({
'click'
:
false
,
'mousedown'
:
this
.
toggleMuteHandler
.
bind
(
this
),
'keydown'
:
this
.
keyDownButtonHandler
.
bind
(
this
),
'focus'
:
this
.
openMenu
.
bind
(
this
),
'blur'
:
this
.
closeMenu
.
bind
(
this
)
});
},
/**
* Updates volume level without updating view and triggering
* `volumechange` event.
*/
updateVolumeSilently
:
function
()
{
this
.
state
.
el
.
trigger
(
'volumechange:silent'
,
[
this
.
getVolume
()]
);
},
state
.
currentVolume
=
currentVolume
;
$
.
extend
(
state
.
videoVolumeControl
,
{
el
:
element
,
buttonEl
:
button
,
volumeSliderEl
:
volumeSlider
,
currentVolume
:
currentVolume
,
previousVolume
:
previousVolume
,
slider
:
slider
,
volumeSliderHandleEl
:
volumeSliderHandleEl
});
/**
* Returns current volume level.
* @return {Number}
*/
getVolume
:
function
()
{
return
this
.
volume
;
},
/**
* Sets current volume level.
* @param {Number} volume Suggested volume level
* @param {Boolean} [silent] Sets the new volume level without
* triggering `volumechange` event and updating the cookie.
* @param {Boolean} [withoutSlider] Disables updating the slider.
*/
setVolume
:
function
(
volume
,
silent
,
withoutSlider
)
{
if
(
volume
===
this
.
getVolume
())
{
return
false
;
}
this
.
volume
=
volume
;
this
.
a11y
.
update
(
this
.
getVolume
());
if
(
!
withoutSlider
)
{
this
.
updateSliderView
(
this
.
getVolume
());
}
if
(
!
silent
)
{
this
.
cookie
.
setVolume
(
this
.
getVolume
());
this
.
state
.
el
.
trigger
(
'volumechange'
,
[
this
.
getVolume
()]);
}
},
/** Increases current volume level using previously defined step. */
increaseVolume
:
function
()
{
var
volume
=
Math
.
min
(
this
.
getVolume
()
+
this
.
step
,
this
.
max
);
this
.
setVolume
(
volume
,
false
,
false
);
},
/** Decreases current volume level using previously defined step. */
decreaseVolume
:
function
()
{
var
volume
=
Math
.
max
(
this
.
getVolume
()
-
this
.
step
,
this
.
min
);
this
.
setVolume
(
volume
,
false
,
false
);
},
/** Updates volume slider view. */
updateSliderView
:
function
(
volume
)
{
this
.
volumeSlider
.
slider
(
'value'
,
volume
);
},
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
* @type {function}
* @access private
*
* @param {object} state The object containg the state of the video player.
* All other modules, their parameters, public variables, etc. are
* available via this object.
*
* @this {object} The global window object.
*
* @returns {undefined}
* Mutes or unmutes volume.
* @param {Number} muteStatus Flag to mute/unmute volume.
*/
function
_bindHandlers
(
state
)
{
state
.
videoVolumeControl
.
buttonEl
.
on
(
'click'
,
state
.
videoVolumeControl
.
toggleMute
);
mute
:
function
(
muteStatus
)
{
var
volume
;
state
.
videoVolumeControl
.
el
.
on
(
'mouseenter'
,
function
()
{
state
.
videoVolumeControl
.
el
.
addClass
(
'open'
);
});
this
.
updateMuteButtonView
(
muteStatus
);
state
.
videoVolumeControl
.
el
.
on
(
'mouseleave'
,
function
()
{
state
.
videoVolumeControl
.
el
.
removeClass
(
'open'
);
});
if
(
muteStatus
)
{
this
.
storedVolume
=
this
.
getVolume
()
||
this
.
max
;
}
volume
=
muteStatus
?
0
:
this
.
storedVolume
;
this
.
setVolume
(
volume
,
false
,
false
);
},
// Attach a focus event to the volume button.
state
.
videoVolumeControl
.
buttonEl
.
on
(
'blur'
,
function
()
{
// If the focus is being trasnfered from the volume slider, then we
// don't do anything except for unsetting the special flag.
if
(
state
.
volumeBlur
===
true
)
{
state
.
volumeBlur
=
false
;
/**
* Returns current volume state (is it muted or not?).
* @return {Boolean}
*/
getMuteStatus
:
function
()
{
return
this
.
getVolume
()
===
0
;
},
/**
* Updates the volume button view.
* @param {Boolean} isMuted Flag to use muted or unmuted view.
*/
updateMuteButtonView
:
function
(
isMuted
)
{
var
action
=
isMuted
?
'addClass'
:
'removeClass'
;
this
.
el
[
action
](
'is-muted'
);
},
/** Toggles the state of the volume button. */
toggleMute
:
function
()
{
this
.
mute
(
!
this
.
getMuteStatus
());
},
/**
* Checks and updates the state of the volume button relatively to
* volume level.
* @param {Number} volume Volume level.
*/
checkMuteButtonStatus
:
function
(
volume
)
{
if
(
volume
<=
this
.
min
)
{
this
.
updateMuteButtonView
(
true
);
this
.
state
.
el
.
off
(
'volumechange.is-muted'
);
this
.
state
.
el
.
on
(
'volumechange.is-muted'
,
_
.
once
(
function
()
{
this
.
updateMuteButtonView
(
false
);
}.
bind
(
this
)));
}
},
/** Opens volume menu. */
openMenu
:
function
()
{
this
.
el
.
addClass
(
'is-opened'
);
},
//If the focus is comming from elsewhere, then we must show the
// volume slider and set focus to it.
else
{
state
.
videoVolumeControl
.
el
.
addClass
(
'open'
);
state
.
videoVolumeControl
.
volumeSliderEl
.
find
(
'a'
).
focus
();
/** Closes speed menu. */
closeMenu
:
function
()
{
this
.
el
.
removeClass
(
'is-opened'
);
},
/**
* Keydown event handler for the video container.
* @param {jquery Event} event
*/
keyDownHandler
:
function
(
event
)
{
// ALT key is used to change (alternate) the function of
// other pressed keys. In this case, do nothing.
if
(
event
.
altKey
)
{
return
true
;
}
});
// Attach a blur event handler (loss of focus) to the volume slider
// element. More specifically, we are attaching to the handle on
// the slider with which you can change the volume.
state
.
videoVolumeControl
.
volumeSliderEl
.
find
(
'a'
)
.
on
(
'blur'
,
function
()
{
// Hide the volume slider. This is done so that we can
// continue to the next (or previous) element by tabbing.
// Otherwise, after next tab we would come back to the volume
// slider because it is the next element visible element that
// we can tab to after the volume button.
state
.
videoVolumeControl
.
el
.
removeClass
(
'open'
);
// Set focus to the volume button.
state
.
videoVolumeControl
.
buttonEl
.
focus
();
// We store the fact that previous element that lost focus was
// the volume clontrol.
state
.
volumeBlur
=
true
;
// The following field is used in video_speed_control to track
// the element that had the focus before it.
state
.
previousFocus
=
'volume'
;
});
if
(
$
(
event
.
target
).
hasClass
(
'ui-slider-handle'
))
{
return
true
;
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
var
KEY
=
$
.
ui
.
keyCode
,
keyCode
=
event
.
keyCode
;
function
onChange
(
event
,
ui
)
{
var
currentVolume
=
ui
.
value
,
ariaLabelText
=
(
currentVolume
===
0
)
?
'Volume muted'
:
'Volume'
;
switch
(
keyCode
)
{
case
KEY
.
UP
:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if
(
event
.
shiftKey
)
{
return
true
;
}
this
.
videoVolumeControl
.
currentVolume
=
currentVolume
;
this
.
videoVolumeControl
.
el
.
toggleClass
(
'muted'
,
currentVolume
===
0
);
this
.
increaseVolume
();
return
false
;
case
KEY
.
DOWN
:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if
(
event
.
shiftKey
)
{
return
true
;
}
$
.
cookie
(
'video_player_volume_level'
,
ui
.
value
,
{
expires
:
3650
,
path
:
'/'
});
this
.
decreaseVolume
();
return
false
;
}
this
.
trigger
(
'videoPlayer.onVolumeChange'
,
ui
.
value
);
return
true
;
},
// ARIA
this
.
videoVolumeControl
.
volumeSliderHandleEl
.
attr
({
'aria-valuenow'
:
ui
.
value
,
'aria-valuetext'
:
getVolumeDescription
(
ui
.
value
)
});
/**
* Keydown event handler for the volume button.
* @param {jquery Event} event
*/
keyDownButtonHandler
:
function
(
event
)
{
// ALT key is used to change (alternate) the function of
// other pressed keys. In this case, do nothing.
if
(
event
.
altKey
)
{
return
true
;
}
this
.
videoVolumeControl
.
buttonEl
.
attr
(
'aria-label'
,
gettext
(
ariaLabelText
)
);
var
KEY
=
$
.
ui
.
keyCode
,
keyCode
=
event
.
keyCode
;
switch
(
keyCode
)
{
case
KEY
.
ENTER
:
case
KEY
.
SPACE
:
this
.
toggleMute
();
return
false
;
}
function
toggleMute
(
event
)
{
return
true
;
},
/**
* onSlide callback for the video slider.
* @param {jquery Event} event
* @param {jqueryuiSlider ui} ui
*/
onSlideHandler
:
function
(
event
,
ui
)
{
this
.
setVolume
(
ui
.
value
,
false
,
true
);
},
/**
* Mousedown event handler for the volume button.
* @param {jquery Event} event
*/
toggleMuteHandler
:
function
(
event
)
{
this
.
toggleMute
();
event
.
preventDefault
();
},
if
(
this
.
videoVolumeControl
.
currentVolume
>
0
)
{
this
.
videoVolumeControl
.
previousVolume
=
this
.
videoVolumeControl
.
currentVolume
;
this
.
videoVolumeControl
.
slider
.
slider
(
'option'
,
'value'
,
0
);
// ARIA
this
.
videoVolumeControl
.
volumeSliderHandleEl
.
attr
({
'aria-valuenow'
:
0
,
'aria-valuetext'
:
getVolumeDescription
(
0
),
});
}
else
{
this
.
videoVolumeControl
.
slider
.
slider
(
'option'
,
'value'
,
this
.
videoVolumeControl
.
previousVolume
);
// ARIA
this
.
videoVolumeControl
.
volumeSliderHandleEl
.
attr
({
'aria-valuenow'
:
this
.
videoVolumeControl
.
previousVolume
,
'aria-valuetext'
:
getVolumeDescription
(
this
.
videoVolumeControl
.
previousVolume
)
/**
* Volumechange event handler.
* @param {jquery Event} event
* @param {Number} volume Volume level.
*/
onVolumeChangeHandler
:
function
(
event
,
volume
)
{
this
.
checkMuteButtonStatus
(
volume
);
}
};
/**
* Module responsible for the accessibility of volume controls.
* @constructor
* @private
* @param {jquery $} button The volume button.
* @param {Number} min Minimum value for the volume slider.
* @param {Number} max Maximum value for the volume slider.
* @param {Object} i18n The object containing strings with translations.
*/
var
Accessibility
=
function
(
button
,
min
,
max
,
i18n
)
{
this
.
min
=
min
;
this
.
max
=
max
;
this
.
button
=
button
;
this
.
i18n
=
i18n
;
this
.
initialize
();
};
Accessibility
.
prototype
=
{
/** Initializes the module. */
initialize
:
function
()
{
this
.
liveRegion
=
$
(
'<div />'
,
{
'class'
:
'sr video-live-region'
,
'role'
:
'status'
,
'aria-hidden'
:
'false'
,
'aria-live'
:
'polite'
,
'aria-atomic'
:
'false'
});
this
.
button
.
after
(
this
.
liveRegion
);
},
/**
* Updates text of the live region.
* @param {Number} volume Volume level.
*/
update
:
function
(
volume
)
{
this
.
liveRegion
.
text
([
this
.
getVolumeDescription
(
volume
),
this
.
i18n
[
'Volume'
]
+
'.'
].
join
(
' '
));
},
/**
* Returns a string describing the level of volume.
* @param {Number} volume Volume level.
*/
getVolumeDescription
:
function
(
volume
)
{
if
(
volume
===
0
)
{
return
this
.
i18n
[
'Muted'
];
}
else
if
(
volume
<=
20
)
{
return
this
.
i18n
[
'Very low'
];
}
else
if
(
volume
<=
40
)
{
return
this
.
i18n
[
'Low'
];
}
else
if
(
volume
<=
60
)
{
return
this
.
i18n
[
'Average'
];
}
else
if
(
volume
<=
80
)
{
return
this
.
i18n
[
'Loud'
];
}
else
if
(
volume
<=
99
)
{
return
this
.
i18n
[
'Very loud'
];
}
return
this
.
i18n
[
'Maximum'
];
}
};
// ARIA
// Returns a string describing the level of volume.
function
getVolumeDescription
(
vol
)
{
if
(
vol
===
0
)
{
// Translators: Volume level equals 0%.
return
gettext
(
'Muted'
);
}
else
if
(
vol
<=
20
)
{
// Translators: Volume level in range (0,20]%
return
gettext
(
'Very low'
);
}
else
if
(
vol
<=
40
)
{
// Translators: Volume level in range (20,40]%
return
gettext
(
'Low'
);
}
else
if
(
vol
<=
60
)
{
// Translators: Volume level in range (40,60]%
return
gettext
(
'Average'
);
}
else
if
(
vol
<=
80
)
{
// Translators: Volume level in range (60,80]%
return
gettext
(
'Loud'
);
}
else
if
(
vol
<=
99
)
{
// Translators: Volume level in range (80,100)%
return
gettext
(
'Very loud'
);
/**
* Module responsible for the work with volume cookie.
* @constructor
* @private
* @param {Number} min Minimum value for the volume slider.
* @param {Number} max Maximum value for the volume slider.
*/
var
CookieManager
=
function
(
min
,
max
)
{
this
.
min
=
min
;
this
.
max
=
max
;
this
.
cookieName
=
'video_player_volume_level'
;
};
CookieManager
.
prototype
=
{
/**
* Returns volume level from the cookie.
* @return {Number} Volume level.
*/
getVolume
:
function
()
{
var
volume
=
parseInt
(
$
.
cookie
(
this
.
cookieName
),
10
);
if
(
_
.
isFinite
(
volume
))
{
volume
=
Math
.
max
(
volume
,
this
.
min
);
volume
=
Math
.
min
(
volume
,
this
.
max
);
}
else
{
volume
=
this
.
max
;
}
// Translators: Volume level equals 100%.
return
gettext
(
'Maximum'
);
return
volume
;
},
/**
* Updates volume cookie.
* @param {Number} volume Volume level.
*/
setVolume
:
function
(
value
)
{
$
.
cookie
(
this
.
cookieName
,
value
,
{
expires
:
3650
,
path
:
'/'
});
}
};
return
VolumeControl
;
});
}(
RequireJS
.
requirejs
,
RequireJS
.
require
,
RequireJS
.
define
));
}(
RequireJS
.
define
));
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
View file @
8c26fc68
...
...
@@ -9,6 +9,7 @@ function (Iterator) {
* @exports video/08_video_speed_control.js
* @constructor
* @param {object} state The object containing the state of the video player.
* @return {jquery Promise}
*/
var
SpeedControl
=
function
(
state
)
{
if
(
!
(
this
instanceof
SpeedControl
))
{
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
8c26fc68
...
...
@@ -71,6 +71,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
resource_string
(
module
,
'js/src/video/00_video_storage.js'
),
resource_string
(
module
,
'js/src/video/00_resizer.js'
),
resource_string
(
module
,
'js/src/video/00_async_process.js'
),
resource_string
(
module
,
'js/src/video/00_i18n.js'
),
resource_string
(
module
,
'js/src/video/00_sjson.js'
),
resource_string
(
module
,
'js/src/video/00_iterator.js'
),
resource_string
(
module
,
'js/src/video/01_initialize.js'
),
...
...
lms/templates/video.html
View file @
8c26fc68
...
...
@@ -79,8 +79,8 @@
<ol
class=
"video-speeds menu"
role=
"menu"
></ol>
</div>
<div
class=
"volume"
>
<a
href=
"#"
title=
"${_('Volume')}"
role=
"button"
aria-disabled=
"false
"
></a>
<div
class=
"volume-slider-container"
>
<a
href=
"#"
role=
"button"
aria-disabled=
"false"
title=
"${_('Volume')}"
aria-label=
"${_('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.')}
"
></a>
<div
role=
"presentation"
class=
"volume-slider-container"
>
<div
class=
"volume-slider"
></div>
</div>
</div>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment