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
23dc10d0
Commit
23dc10d0
authored
Dec 24, 2013
by
Anton Stupak
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1998 from edx/anton/fix-video-in-ipad
Fix video controls on iPad.
parents
a68f5929
934b5198
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
755 additions
and
316 deletions
+755
-316
CHANGELOG.rst
+8
-0
cms/djangoapps/contentstore/features/video.py
+3
-2
cms/static/coffee/src/main.coffee
+1
-1
common/lib/xmodule/xmodule/css/video/display.scss
+40
-1
common/lib/xmodule/xmodule/js/fixtures/video.html
+4
-2
common/lib/xmodule/xmodule/js/fixtures/video_all.html
+4
-2
common/lib/xmodule/xmodule/js/fixtures/video_html5.html
+4
-2
common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
+4
-2
common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
+4
-2
common/lib/xmodule/xmodule/js/spec/video/events_spec.js
+164
-0
common/lib/xmodule/xmodule/js/spec/video/general_spec.js
+0
-1
common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
+0
-0
common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+11
-27
common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js
+144
-6
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+83
-48
common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js
+38
-108
common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js
+2
-2
common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+9
-13
common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
+12
-12
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+37
-8
common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
+42
-21
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
+57
-21
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
+53
-6
common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js
+1
-0
common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js
+3
-5
common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
+7
-0
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+8
-1
common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
+8
-20
lms/static/coffee/src/main.coffee
+1
-1
lms/templates/video.html
+3
-2
No files found.
CHANGELOG.rst
View file @
23dc10d0
...
...
@@ -5,6 +5,14 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Video player improvements:
- Disable edX controls on iPhone/iPod (native controls are used).
- Disable unsupported controls (volume, playback rate) on iPad/Android.
- Controls becomes visible after click on video or play placeholder to avoid
issues with YouTube API on iPad/Android.
- Captions becomes visible just after full initialization of video player.
- Fix blinking of captions after initialization of video player. BLD-206.
LMS: Fix answer distribution download for small courses. LMS-922, LMS-811
Blades: Add template for the zooming image in studio. BLD-206.
...
...
cms/djangoapps/contentstore/features/video.py
View file @
23dc10d0
...
...
@@ -141,12 +141,13 @@ def the_youtube_video_is_shown(_step):
@step
(
'Make sure captions are (.+)$'
)
def
set_captions_visibility_state
(
_step
,
captions_state
):
SELECTOR
=
'.closed .subtitles'
world
.
wait_for_visible
(
'.hide-subtitles'
)
if
captions_state
==
'closed'
:
if
not
world
.
is_css_present
(
SELECTOR
):
world
.
browser
.
find_by_css
(
'.hide-subtitles'
)
.
click
()
world
.
css_find
(
'.hide-subtitles'
)
.
click
()
else
:
if
world
.
is_css_present
(
SELECTOR
):
world
.
browser
.
find_by_css
(
'.hide-subtitles'
)
.
click
()
world
.
css_find
(
'.hide-subtitles'
)
.
click
()
@step
(
'I hover over button "([^"]*)"$'
)
...
...
cms/static/coffee/src/main.coffee
View file @
23dc10d0
...
...
@@ -9,7 +9,7 @@ define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
window
.
CMS
=
window
.
CMS
or
{}
CMS
.
URL
=
CMS
.
URL
or
{}
window
.
onTouchBasedDevice
=
->
navigator
.
userAgent
.
match
/iPhone|iPod|iPad/i
navigator
.
userAgent
.
match
/iPhone|iPod|iPad
|Android
/i
_
.
extend
CMS
,
Backbone
.
Events
Backbone
.
emulateHTTP
=
true
...
...
common/lib/xmodule/xmodule/css/video/display.scss
View file @
23dc10d0
...
...
@@ -2,6 +2,10 @@
margin-bottom
:
30px
;
}
.is-hidden
{
display
:
none
;
}
div
.video
{
@include
clearfix
();
background
:
#f3f3f3
;
...
...
@@ -97,12 +101,35 @@ div.video {
}
}
.btn-play
{
@include
transform
(
translate
(
-50%
,
-50%
));
position
:
absolute
;
z-index
:
1
;
background
:
rgba
(
0
,
0
,
0
,
0
.7
);
top
:
50%
;
left
:
50%
;
padding
:
30px
;
border-radius
:
25%
;
&
:after
{
content
:
''
;
display
:
block
;
width
:
0px
;
height
:
0px
;
border-style
:
solid
;
border-width
:
30px
0
30px
50px
;
border-color
:
transparent
transparent
transparent
#ffffff
;
position
:
relative
;
}
}
section
.video-player
{
overflow
:
hidden
;
min-height
:
300px
;
div
{
>
div
{
height
:
100%
;
&
.hidden
{
display
:
none
;
}
...
...
@@ -674,6 +701,7 @@ div.video {
width
:
275px
;
padding
:
0
20px
;
z-index
:
0
;
display
:
none
;
}
}
...
...
@@ -764,6 +792,17 @@ div.video {
}
}
}
&
.is-touch
{
div
.tc-wrapper
{
article
.video-wrapper
{
object
,
iframe
,
video
{
width
:
100%
;
height
:
100%
;
}
}
}
}
}
common/lib/xmodule/xmodule/js/fixtures/video.html
View file @
23dc10d0
...
...
@@ -3,7 +3,7 @@
<div
id=
"example"
>
<div
id=
"video_id"
class=
"video"
class=
"video
closed
"
data-streams=
"0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions=
"true"
data-start=
""
...
...
@@ -18,12 +18,14 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<div
class=
"video-player-pre"
></div>
<section
class=
"video-player"
>
<div
id=
"id"
></div>
</section>
<div
class=
"video-player-post"
></div>
<section
class=
"video-controls"
>
<section
class=
"video-controls
is-hidden
"
>
<div
class=
"slider"
></div>
<div>
<ul
class=
"vcr"
>
...
...
common/lib/xmodule/xmodule/js/fixtures/video_all.html
View file @
23dc10d0
...
...
@@ -3,7 +3,7 @@
<div
id=
"example"
>
<div
id=
"video_id"
class=
"video"
class=
"video
closed
"
data-show-captions=
"true"
data-start=
""
data-end=
""
...
...
@@ -21,12 +21,14 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<div
class=
"video-player-pre"
></div>
<section
class=
"video-player"
>
<div
id=
"id"
></div>
</section>
<div
class=
"video-player-post"
></div>
<section
class=
"video-controls"
>
<section
class=
"video-controls
is-hidden
"
>
<div
class=
"slider"
></div>
<div>
<ul
class=
"vcr"
>
...
...
common/lib/xmodule/xmodule/js/fixtures/video_html5.html
View file @
23dc10d0
...
...
@@ -3,7 +3,7 @@
<div
id=
"example"
>
<div
id=
"video_id"
class=
"video"
class=
"video
closed
"
data-show-captions=
"true"
data-start=
""
data-end=
""
...
...
@@ -21,10 +21,12 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<section
class=
"video-player"
>
<div
id=
"id"
></div>
</section>
<section
class=
"video-controls"
></section>
<section
class=
"video-controls
is-hidden
"
></section>
</article>
<ol
class=
"subtitles"
><li></li></ol>
...
...
common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
View file @
23dc10d0
...
...
@@ -3,7 +3,7 @@
<div
id=
"example"
>
<div
id=
"video_id"
class=
"video"
class=
"video
closed
"
data-streams=
"0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions=
"false"
data-start=
""
...
...
@@ -18,10 +18,12 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<section
class=
"video-player"
>
<div
id=
"id"
></div>
</section>
<section
class=
"video-controls"
></section>
<section
class=
"video-controls
is-hidden
"
></section>
</article>
</div>
...
...
common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
View file @
23dc10d0
...
...
@@ -3,7 +3,7 @@
<div
id=
"example1"
>
<div
id=
"video_id1"
class=
"video"
class=
"video
closed
"
data-streams=
"0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions=
"true"
data-start=
""
...
...
@@ -18,12 +18,14 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<div
class=
"video-player-pre"
></div>
<section
class=
"video-player"
>
<div
id=
"id1"
></div>
</section>
<div
class=
"video-player-post"
></div>
<section
class=
"video-controls"
>
<section
class=
"video-controls
is-hidden
"
>
<div
class=
"slider"
></div>
<div>
<ul
class=
"vcr"
>
...
...
common/lib/xmodule/xmodule/js/spec/video/events_spec.js
0 → 100644
View file @
23dc10d0
(
function
()
{
describe
(
'VideoPlayer Events'
,
function
()
{
var
state
,
videoPlayer
,
player
,
videoControl
,
videoCaption
,
videoProgressSlider
,
videoSpeedControl
,
videoVolumeControl
,
oldOTBD
;
function
initialize
(
fixture
,
params
)
{
if
(
_
.
isString
(
fixture
))
{
loadFixtures
(
fixture
);
}
else
{
if
(
_
.
isObject
(
fixture
))
{
params
=
fixture
;
}
loadFixtures
(
'video_all.html'
);
}
if
(
_
.
isObject
(
params
))
{
$
(
'#example'
)
.
find
(
'#video_id'
)
.
data
(
params
);
}
state
=
new
Video
(
'#example'
);
state
.
videoEl
=
$
(
'video, iframe'
);
videoPlayer
=
state
.
videoPlayer
;
player
=
videoPlayer
.
player
;
videoControl
=
state
.
videoControl
;
videoCaption
=
state
.
videoCaption
;
videoProgressSlider
=
state
.
videoProgressSlider
;
videoSpeedControl
=
state
.
videoSpeedControl
;
videoVolumeControl
=
state
.
videoVolumeControl
;
state
.
resizer
=
(
function
()
{
var
methods
=
[
'align'
,
'alignByWidthOnly'
,
'alignByHeightOnly'
,
'setParams'
,
'setMode'
],
obj
=
{};
$
.
each
(
methods
,
function
(
index
,
method
)
{
obj
[
method
]
=
jasmine
.
createSpy
(
method
).
andReturn
(
obj
);
});
return
obj
;
}());
}
function
initializeYouTube
()
{
initialize
(
'video.html'
);
}
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
andReturn
(
null
);
this
.
oldYT
=
window
.
YT
;
jasmine
.
stubRequests
();
window
.
YT
=
{
Player
:
function
()
{
return
{
getPlaybackQuality
:
function
()
{}
};
},
PlayerState
:
this
.
oldYT
.
PlayerState
,
ready
:
function
(
callback
)
{
callback
();
}
};
});
afterEach
(
function
()
{
$
(
'source'
).
remove
();
window
.
onTouchBasedDevice
=
oldOTBD
;
window
.
YT
=
this
.
oldYT
;
});
it
(
'initialize'
,
function
(){
runs
(
function
()
{
initialize
();
});
waitsFor
(
function
()
{
return
state
.
el
.
hasClass
(
'is-initialized'
);
},
'Player is not initialized.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
'initialize'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
});
it
(
'ready'
,
function
()
{
runs
(
function
()
{
initialize
();
});
waitsFor
(
function
()
{
return
state
.
el
.
hasClass
(
'is-initialized'
);
},
'Player is not initialized.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
'ready'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
});
it
(
'play'
,
function
()
{
initialize
();
videoPlayer
.
play
();
expect
(
'play'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'pause'
,
function
()
{
initialize
();
videoPlayer
.
play
();
videoPlayer
.
pause
();
expect
(
'pause'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'volumechange'
,
function
()
{
initialize
();
videoPlayer
.
onVolumeChange
(
60
);
expect
(
'volumechange'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'speedchange'
,
function
()
{
initialize
();
videoPlayer
.
onSpeedChange
(
'2.0'
);
expect
(
'speedchange'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'qualitychange'
,
function
()
{
initializeYouTube
();
videoPlayer
.
onPlaybackQualityChange
();
expect
(
'qualitychange'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'seek'
,
function
()
{
initialize
();
videoPlayer
.
onCaptionSeek
({
time
:
1
,
type
:
'any'
});
expect
(
'seek'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
it
(
'ended'
,
function
()
{
initialize
();
videoPlayer
.
onEnded
();
expect
(
'ended'
).
not
.
toHaveBeenTriggeredOn
(
'.video'
);
});
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/spec/video/general_spec.js
View file @
23dc10d0
...
...
@@ -60,7 +60,6 @@
beforeEach
(
function
()
{
loadFixtures
(
'video_html5.html'
);
this
.
stubVideoPlayer
=
jasmine
.
createSpy
(
'VideoPlayer'
);
$
.
cookie
.
andReturn
(
'0.75'
);
});
...
...
common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
View file @
23dc10d0
This diff is collapsed.
Click to expand it.
common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
View file @
23dc10d0
...
...
@@ -15,7 +15,7 @@
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
andReturn
(
false
);
.
andReturn
(
null
);
initialize
();
});
...
...
@@ -175,7 +175,7 @@
describe
(
'when on a touch-based device'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
window
.
onTouchBasedDevice
.
andReturn
(
[
'iPad'
]
);
initialize
();
});
...
...
@@ -209,34 +209,15 @@
});
describe
(
'mouse movement'
,
function
()
{
// We will store default window.setTimeout() function here.
var
oldSetTimeout
=
null
;
beforeEach
(
function
()
{
// Store original window.setTimeout() function. If we do not do
// this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
oldSetTimeout
=
window
.
setTimeout
;
// Redefine window.setTimeout() function as a spy.
window
.
setTimeout
=
jasmine
.
createSpy
().
andCallFake
(
function
(
callback
,
timeout
)
{
return
5
;
}
);
window
.
setTimeout
.
andReturn
(
100
);
jasmine
.
Clock
.
useMock
();
spyOn
(
window
,
'clearTimeout'
);
});
afterEach
(
function
()
{
// Reset the default window.setTimeout() function. If we do not
// do this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
window
.
setTimeout
=
oldSetTimeout
;
});
describe
(
'when cursor is outside of the caption box'
,
function
()
{
beforeEach
(
function
()
{
$
(
window
).
trigger
(
jQuery
.
Event
(
'mousemove'
));
jasmine
.
Clock
.
tick
(
state
.
config
.
captionsFreezeTime
);
});
it
(
'does not set freezing timeout'
,
function
()
{
...
...
@@ -246,11 +227,14 @@
describe
(
'when cursor is in the caption box'
,
function
()
{
beforeEach
(
function
()
{
spyOn
(
videoCaption
,
'onMouseLeave'
);
$
(
'.subtitles'
).
trigger
(
jQuery
.
Event
(
'mouseenter'
));
jasmine
.
Clock
.
tick
(
state
.
config
.
captionsFreezeTime
);
});
it
(
'set the freezing timeout'
,
function
()
{
expect
(
videoCaption
.
frozen
).
toEqual
(
100
);
expect
(
videoCaption
.
frozen
).
not
.
toBeFalsy
();
expect
(
videoCaption
.
onMouseLeave
).
toHaveBeenCalled
();
});
describe
(
'when the cursor is moving'
,
function
()
{
...
...
@@ -259,7 +243,7 @@
});
it
(
'reset the freezing timeout'
,
function
()
{
expect
(
window
.
clearTimeout
).
toHaveBeenCalled
With
(
100
);
expect
(
window
.
clearTimeout
).
toHaveBeenCalled
(
);
});
});
...
...
@@ -269,7 +253,7 @@
});
it
(
'reset the freezing timeout'
,
function
()
{
expect
(
window
.
clearTimeout
).
toHaveBeenCalled
With
(
100
);
expect
(
window
.
clearTimeout
).
toHaveBeenCalled
(
);
});
});
});
...
...
@@ -337,7 +321,7 @@
describe
(
'play'
,
function
()
{
describe
(
'when the caption was not rendered'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
window
.
onTouchBasedDevice
.
andReturn
(
[
'iPad'
]
);
initialize
();
videoCaption
.
play
();
});
...
...
common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js
View file @
23dc10d0
...
...
@@ -2,15 +2,23 @@
describe
(
'VideoControl'
,
function
()
{
var
state
,
videoControl
,
oldOTBD
;
function
initialize
()
{
loadFixtures
(
'video_all.html'
);
function
initialize
(
fixture
)
{
if
(
fixture
)
{
loadFixtures
(
fixture
);
}
else
{
loadFixtures
(
'video_all.html'
);
}
state
=
new
Video
(
'#example'
);
videoControl
=
state
.
videoControl
;
}
function
initializeYouTube
()
{
initialize
(
'video.html'
);
}
beforeEach
(
function
(){
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
false
);
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
null
);
});
afterEach
(
function
()
{
...
...
@@ -75,13 +83,13 @@
describe
(
'when on a touch based device'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
window
.
onTouchBasedDevice
.
andReturn
(
[
'iPad'
]
);
initialize
();
});
it
(
'does not add the play class to video control'
,
function
()
{
expect
(
$
(
'.video_control'
)).
not
.
toHaveClass
(
'play'
);
expect
(
$
(
'.video_control'
)).
not
.
toHaveAttr
(
'title'
,
'Play'
);
expect
(
$
(
'.video_control'
)).
toHaveClass
(
'play'
);
expect
(
$
(
'.video_control'
)).
toHaveAttr
(
'title'
,
'Play'
);
});
});
});
...
...
@@ -147,6 +155,136 @@
});
});
});
describe
(
'Play placeholder'
,
function
()
{
beforeEach
(
function
()
{
this
.
oldYT
=
window
.
YT
;
jasmine
.
stubRequests
();
window
.
YT
=
{
Player
:
function
()
{
},
PlayerState
:
this
.
oldYT
.
PlayerState
,
ready
:
function
(
callback
)
{
callback
();
}
};
spyOn
(
window
.
YT
,
'Player'
);
});
afterEach
(
function
()
{
window
.
YT
=
this
.
oldYT
;
});
it
(
'works correctly on calling proper methods'
,
function
()
{
initialize
();
var
btnPlay
=
state
.
el
.
find
(
'.btn-play'
);
videoControl
.
showPlayPlaceholder
();
expect
(
btnPlay
).
not
.
toHaveClass
(
'is-hidden'
);
expect
(
btnPlay
).
toHaveAttrs
({
'aria-hidden'
:
'false'
,
'tabindex'
:
0
});
videoControl
.
hidePlayPlaceholder
();
expect
(
btnPlay
).
toHaveClass
(
'is-hidden'
);
expect
(
btnPlay
).
toHaveAttrs
({
'aria-hidden'
:
'true'
,
'tabindex'
:
-
1
});
});
var
cases
=
[
{
name
:
'PC'
,
isShown
:
false
,
isTouch
:
null
},
{
name
:
'iPad'
,
isShown
:
true
,
isTouch
:
[
'iPad'
]
},
{
name
:
'Android'
,
isShown
:
true
,
isTouch
:
[
'Android'
]
},
{
name
:
'iPhone'
,
isShown
:
false
,
isTouch
:
[
'iPhone'
]
}
];
$
.
each
(
cases
,
function
(
index
,
data
)
{
var
message
=
[
(
data
.
isShown
)
?
'is'
:
'is not'
,
' shown on'
,
data
.
name
].
join
(
''
);
it
(
message
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
data
.
isTouch
);
initialize
();
var
btnPlay
=
state
.
el
.
find
(
'.btn-play'
);
if
(
data
.
isShown
)
{
expect
(
btnPlay
).
not
.
toHaveClass
(
'is-hidden'
);
}
else
{
expect
(
btnPlay
).
toHaveClass
(
'is-hidden'
);
}
});
});
$
.
each
([
'iPad'
,
'Android'
],
function
(
index
,
device
)
{
it
(
'is shown on paused video on '
+
device
+
' in HTML5 player'
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
var
btnPlay
=
state
.
el
.
find
(
'.btn-play'
);
videoControl
.
play
();
videoControl
.
pause
();
expect
(
btnPlay
).
not
.
toHaveClass
(
'is-hidden'
);
});
it
(
'is hidden on playing video on '
+
device
+
' in HTML5 player'
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
var
btnPlay
=
state
.
el
.
find
(
'.btn-play'
);
videoControl
.
play
();
expect
(
btnPlay
).
toHaveClass
(
'is-hidden'
);
});
it
(
'is hidden on paused video on '
+
device
+
' in YouTube player'
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initializeYouTube
();
var
btnPlay
=
state
.
el
.
find
(
'.btn-play'
);
videoControl
.
play
();
videoControl
.
pause
();
expect
(
btnPlay
).
toHaveClass
(
'is-hidden'
);
});
});
});
it
(
'show'
,
function
()
{
initialize
();
var
controls
=
state
.
el
.
find
(
'.video-controls'
);
controls
.
addClass
(
'is-hidden'
);
videoControl
.
show
();
expect
(
controls
).
not
.
toHaveClass
(
'is-hidden'
);
});
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
View file @
23dc10d0
...
...
@@ -57,7 +57,7 @@
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
andReturn
(
false
);
.
andReturn
(
null
);
});
afterEach
(
function
()
{
...
...
@@ -119,8 +119,8 @@
window
.
YT
=
{
Player
:
function
()
{
},
PlayerState
:
oldYT
.
PlayerState
,
ready
:
function
(
f
)
{
f
();
ready
:
function
(
callback
)
{
callback
();
}
};
...
...
@@ -156,19 +156,18 @@
// available globally. It is defined within the scope of Require
// JS.
describe
(
'when not on a touch based device'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
initialize
();
});
it
(
'create video volume control'
,
function
()
{
expect
(
videoVolumeControl
).
toBeDefined
();
expect
(
videoVolumeControl
.
el
).
toHaveClass
(
'volume'
);
describe
(
'when on a touch based device'
,
function
()
{
$
.
each
([
'iPad'
,
'Android'
],
function
(
index
,
device
)
{
it
(
'create video volume control on'
+
device
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
expect
(
videoVolumeControl
).
toBeUndefined
();
expect
(
state
.
el
.
find
(
'div.volume'
)).
not
.
toExist
();
});
});
});
describe
(
'when on a touch based device'
,
function
()
{
describe
(
'when
not
on a touch based device'
,
function
()
{
var
oldOTBD
;
beforeEach
(
function
()
{
...
...
@@ -343,16 +342,8 @@
state
.
videoPlayer
.
play
();
waitsFor
(
function
()
{
var
duration
=
videoPlayer
.
duration
(),
currentTime
=
videoPlayer
.
currentTime
;
return
(
isFinite
(
currentTime
)
&&
currentTime
>
0
&&
isFinite
(
duration
)
&&
duration
>
0
);
},
'video begins playing'
,
10000
);
return
videoPlayer
.
isPlaying
();
},
'video begins playing'
,
WAIT_TIMEOUT
);
});
it
(
'Slider event causes log update'
,
function
()
{
...
...
@@ -555,34 +546,24 @@
});
it
(
'video is paused on first endTime, start & end time are reset'
,
function
()
{
var
checkForStartEndTimeSet
=
true
;
var
duration
;
videoProgressSlider
.
notifyThroughHandleEnd
.
reset
();
videoPlayer
.
pause
.
reset
();
videoPlayer
.
play
();
waitsFor
(
function
()
{
if
(
!
isFinite
(
videoPlayer
.
currentTime
)
||
videoPlayer
.
currentTime
<=
0
)
{
return
false
;
}
if
(
checkForStartEndTimeSet
)
{
checkForStartEndTimeSet
=
false
;
expect
(
videoPlayer
.
startTime
).
toBe
(
START_TIME
);
expect
(
videoPlayer
.
endTime
).
toBe
(
END_TIME
);
}
duration
=
Math
.
round
(
videoPlayer
.
currentTime
);
return
videoPlayer
.
pause
.
calls
.
length
===
1
},
5000
,
'pause() has been called'
);
return
videoPlayer
.
pause
.
calls
.
length
===
1
;
},
'pause() has been called'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
videoPlayer
.
startTime
).
toBe
(
0
);
expect
(
videoPlayer
.
endTime
).
toBe
(
null
);
expect
(
duration
).
toBe
(
END_TIME
);
expect
(
videoProgressSlider
.
notifyThroughHandleEnd
)
.
toHaveBeenCalledWith
({
end
:
true
});
});
...
...
@@ -608,7 +589,7 @@
}
return
false
;
},
'Video is fully loaded.'
,
1000
);
},
'Video is fully loaded.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
var
htmlStr
;
...
...
@@ -637,7 +618,7 @@
it
(
'update the playback time on caption'
,
function
()
{
waitsFor
(
function
()
{
return
videoPlayer
.
duration
()
>
0
;
},
'Video is fully loaded.'
,
1000
);
},
'Video is fully loaded.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
videoPlayer
.
updatePlayTime
(
60
);
...
...
@@ -654,7 +635,7 @@
duration
=
videoPlayer
.
duration
();
return
duration
>
0
;
},
'Video is fully loaded.'
,
1000
);
},
'Video is fully loaded.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
videoPlayer
.
updatePlayTime
(
60
);
...
...
@@ -692,9 +673,9 @@
waitsFor
(
function
()
{
duration
=
videoPlayer
.
duration
();
return
duration
>
0
&&
return
videoPlayer
.
isPlaying
()
&&
videoPlayer
.
initialSeekToStartTime
===
false
;
},
'duration becomes available'
,
1000
);
},
'duration becomes available'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
videoPlayer
.
startTime
).
toBe
(
START_TIME
);
...
...
@@ -724,11 +705,9 @@
videoPlayer
.
play
();
waitsFor
(
function
()
{
duration
=
videoPlayer
.
duration
();
return
duration
>
0
&&
return
videoPlayer
.
isPlaying
()
&&
videoPlayer
.
initialSeekToStartTime
===
false
;
},
'updatePlayTime was invoked and duration is set'
,
5000
);
},
'updatePlayTime was invoked and duration is set'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
videoPlayer
.
endTime
).
toBe
(
null
);
...
...
@@ -896,6 +875,62 @@
expect
(
realValue
).
toEqual
(
expectedValue
);
});
});
describe
(
'on Touch devices'
,
function
()
{
it
(
'`is-touch` class name is added to container'
,
function
()
{
$
.
each
([
'iPad'
,
'Android'
,
'iPhone'
],
function
(
index
,
device
)
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
expect
(
state
.
el
).
toHaveClass
(
'is-touch'
);
});
});
it
(
'modules are not initialized on iPhone'
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
'iPhone'
]);
initialize
();
var
modules
=
[
videoControl
,
videoCaption
,
videoProgressSlider
,
videoSpeedControl
,
videoVolumeControl
];
$
.
each
(
modules
,
function
(
index
,
module
)
{
expect
(
module
).
toBeUndefined
();
});
});
$
.
each
([
'iPad'
,
'Android'
],
function
(
index
,
device
)
{
var
message
=
'controls become visible after playing starts on '
+
device
;
it
(
message
,
function
()
{
var
controls
;
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
runs
(
function
()
{
initialize
();
controls
=
state
.
el
.
find
(
'.video-controls'
);
});
waitsFor
(
function
()
{
return
state
.
el
.
hasClass
(
'is-initialized'
);
},
'Video is not initialized.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
controls
).
toHaveClass
(
'is-hidden'
);
videoPlayer
.
play
();
});
waitsFor
(
function
()
{
return
videoPlayer
.
isPlaying
();
},
'Video does not play.'
,
WAIT_TIMEOUT
);
runs
(
function
()
{
expect
(
controls
).
not
.
toHaveClass
(
'is-hidden'
);
});
});
});
});
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js
View file @
23dc10d0
...
...
@@ -12,7 +12,7 @@
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
andReturn
(
false
);
.
andReturn
(
null
);
});
afterEach
(
function
()
{
...
...
@@ -44,18 +44,23 @@
});
describe
(
'on a touch-based device'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
spyOn
(
$
.
fn
,
'slider'
).
andCallThrough
(
);
it
(
'does not build the slider on iPhone'
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
'iPhone'
]
);
initialize
();
});
it
(
'does not build the slider'
,
function
()
{
expect
(
videoProgressSlider
.
slider
).
toBeUndefined
();
expect
(
videoProgressSlider
).
toBeUndefined
();
// We can't expect $.fn.slider not to have been called,
// because sliders are used in other parts of Video.
});
$
.
each
([
'iPad'
,
'Android'
],
function
(
index
,
device
)
{
it
(
'build the slider on '
+
device
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
expect
(
videoProgressSlider
.
slider
).
toBeDefined
();
});
});
});
});
...
...
@@ -127,125 +132,58 @@
initialize
();
spyOn
(
$
.
fn
,
'slider'
).
andCallThrough
();
spyOn
(
videoPlayer
,
'onSlideSeek'
).
andCallThrough
();
state
.
videoPlayer
.
play
();
waitsFor
(
function
()
{
var
duration
=
videoPlayer
.
duration
(),
currentTime
=
videoPlayer
.
currentTime
;
return
(
isFinite
(
currentTime
)
&&
currentTime
>
0
&&
isFinite
(
duration
)
&&
duration
>
0
);
},
'video begins playing'
,
10000
);
});
it
(
'freeze the slider'
,
function
()
{
runs
(
function
()
{
videoProgressSlider
.
onSlide
(
jQuery
.
Event
(
'slide'
),
{
value
:
20
}
);
videoProgressSlider
.
onSlide
(
jQuery
.
Event
(
'slide'
),
{
value
:
20
}
);
expect
(
videoProgressSlider
.
frozen
).
toBeTruthy
();
});
expect
(
videoProgressSlider
.
frozen
).
toBeTruthy
();
});
// Turned off test due to flakiness (11/25/13)
xit
(
'trigger seek event'
,
function
()
{
runs
(
function
()
{
videoProgressSlider
.
onSlide
(
jQuery
.
Event
(
'slide'
),
{
value
:
20
}
);
expect
(
videoPlayer
.
onSlideSeek
).
toHaveBeenCalled
();
it
(
'trigger seek event'
,
function
()
{
videoProgressSlider
.
onSlide
(
jQuery
.
Event
(
'slide'
),
{
value
:
20
}
);
waitsFor
(
function
()
{
return
Math
.
round
(
videoPlayer
.
currentTime
)
===
20
;
},
'currentTime got updated'
,
10000
);
});
expect
(
videoPlayer
.
onSlideSeek
).
toHaveBeenCalled
();
});
});
describe
(
'onStop'
,
function
()
{
// We will store default window.setTimeout() function here.
var
oldSetTimeout
=
null
;
beforeEach
(
function
()
{
// Store original window.setTimeout() function. If we do not do
// this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
oldSetTimeout
=
window
.
setTimeout
;
// Redefine window.setTimeout() function as a spy.
window
.
setTimeout
=
jasmine
.
createSpy
()
.
andCallFake
(
function
(
callback
,
timeout
)
{
return
5
;
});
window
.
setTimeout
.
andReturn
(
100
);
jasmine
.
Clock
.
useMock
();
initialize
();
spyOn
(
videoPlayer
,
'onSlideSeek'
).
andCallThrough
();
videoPlayer
.
play
();
waitsFor
(
function
()
{
var
duration
=
videoPlayer
.
duration
(),
currentTime
=
videoPlayer
.
currentTime
;
return
(
isFinite
(
currentTime
)
&&
currentTime
>
0
&&
isFinite
(
duration
)
&&
duration
>
0
);
},
'video begins playing'
,
10000
);
});
afterEach
(
function
()
{
// Reset the default window.setTimeout() function. If we do not
// do this, then all other tests that rely on code which uses
// window.setTimeout() function might (and probably will) fail.
window
.
setTimeout
=
oldSetTimeout
;
});
it
(
'freeze the slider'
,
function
()
{
runs
(
function
()
{
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
expect
(
videoProgressSlider
.
frozen
).
toBeTruthy
();
});
expect
(
videoProgressSlider
.
frozen
).
toBeTruthy
();
});
// Turned off test due to flakiness (11/25/13)
xit
(
'trigger seek event'
,
function
()
{
runs
(
function
()
{
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
expect
(
videoPlayer
.
onSlideSeek
).
toHaveBeenCalled
();
it
(
'trigger seek event'
,
function
()
{
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
waitsFor
(
function
()
{
return
Math
.
round
(
videoPlayer
.
currentTime
)
===
20
;
},
'currentTime got updated'
,
10000
);
});
expect
(
videoPlayer
.
onSlideSeek
).
toHaveBeenCalled
();
});
it
(
'set timeout to unfreeze the slider'
,
function
()
{
runs
(
function
()
{
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
videoProgressSlider
.
onStop
(
jQuery
.
Event
(
'stop'
),
{
value
:
20
}
);
expect
(
window
.
setTimeout
).
toHaveBeenCalledWith
(
jasmine
.
any
(
Function
),
200
);
window
.
setTimeout
.
mostRecentCall
.
args
[
0
]();
expect
(
videoProgressSlider
.
frozen
).
toBeFalsy
();
});
jasmine
.
Clock
.
tick
(
200
);
expect
(
videoProgressSlider
.
frozen
).
toBeFalsy
();
});
});
...
...
@@ -317,15 +255,7 @@
videoPlayer
.
play
();
waitsFor
(
function
()
{
var
duration
=
videoPlayer
.
duration
(),
currentTime
=
videoPlayer
.
currentTime
;
return
(
isFinite
(
duration
)
&&
duration
>
0
&&
isFinite
(
currentTime
)
&&
currentTime
>
0
);
return
videoPlayer
.
isPlaying
();
},
'duration is set, video is playing'
,
5000
);
runs
(
function
()
{
...
...
common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js
View file @
23dc10d0
...
...
@@ -13,7 +13,7 @@
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
andReturn
(
false
);
.
andReturn
(
null
);
});
afterEach
(
function
()
{
...
...
@@ -49,7 +49,7 @@
'role'
:
'button'
,
'title'
:
'HD off'
,
'aria-disabled'
:
'false'
});
});
});
it
(
'bind the quality control'
,
function
()
{
...
...
common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
View file @
23dc10d0
...
...
@@ -12,7 +12,7 @@
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
false
);
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
null
);
});
...
...
@@ -48,7 +48,7 @@
'role'
:
'button'
,
'title'
:
'Speeds'
,
'aria-disabled'
:
'false'
});
});
});
it
(
'bind to change video speed link'
,
function
()
{
...
...
@@ -57,16 +57,12 @@
});
describe
(
'when running on touch based device'
,
function
()
{
beforeEach
(
function
()
{
window
.
onTouchBasedDevice
.
andReturn
(
true
);
initialize
();
});
it
(
'open the speed toggle on click'
,
function
()
{
$
(
'.speeds'
).
click
();
expect
(
$
(
'.speeds'
)).
toHaveClass
(
'open'
);
$
(
'.speeds'
).
click
();
expect
(
$
(
'.speeds'
)).
not
.
toHaveClass
(
'open'
);
$
.
each
([
'iPad'
,
'Android'
],
function
(
index
,
device
)
{
it
(
'is not rendered on'
+
device
,
function
()
{
window
.
onTouchBasedDevice
.
andReturn
([
device
]);
initialize
();
expect
(
state
.
el
.
find
(
'div.speeds'
)).
not
.
toExist
();
});
});
});
...
...
@@ -96,7 +92,7 @@
// 2. Speed anchor
// 3. A number of speed entry anchors
// 4. Volume anchor
// If an
other focusable element is inserted or if the order is changed, things will
// If an
other focusable element is inserted or if the order is changed, things will
// malfunction as a flag, state.previousFocus, is set in the 1,3,4 elements and is
// used to determine the behavior of foucus() and blur() for the speed anchor.
it
(
'checks for a certain order in focusable elements in video controls'
,
function
()
{
...
...
common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
View file @
23dc10d0
...
...
@@ -11,7 +11,7 @@
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
false
);
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
).
andReturn
(
null
);
});
afterEach
(
function
()
{
...
...
@@ -58,9 +58,9 @@
});
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'
);
expect
(
volumeControl
).
toHaveAttrs
({
...
...
@@ -121,38 +121,38 @@
{
range
:
'muted'
,
value
:
0
,
expectation
:
'muted'
expectation
:
'muted'
},
{
range
:
'in ]0,20]'
,
value
:
10
,
expectation
:
'very low'
expectation
:
'very low'
},
{
range
:
'in ]20,40]'
,
value
:
30
,
expectation
:
'low'
expectation
:
'low'
},
{
range
:
'in ]40,60]'
,
value
:
50
,
expectation
:
'average'
expectation
:
'average'
},
{
range
:
'in ]60,80]'
,
value
:
70
,
expectation
:
'loud'
expectation
:
'loud'
},
{
range
:
'in ]80,100['
,
value
:
90
,
expectation
:
'very loud'
expectation
:
'very loud'
},
{
range
:
'maximum'
,
value
:
100
,
expectation
:
'maximum'
}
expectation
:
'maximum'
}
];
$
.
each
(
initialData
,
function
(
index
,
data
)
{
...
...
@@ -162,7 +162,7 @@
value
:
data
.
value
});
});
it
(
'changes ARIA attributes'
,
function
()
{
var
sliderHandle
=
$
(
'div.volume-slider>a.ui-slider-handle'
);
expect
(
sliderHandle
).
toHaveAttrs
({
...
...
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
View file @
23dc10d0
...
...
@@ -44,15 +44,29 @@ function (VideoPlayer) {
state
.
initialize
(
element
)
.
done
(
function
()
{
// On iPhones and iPods native controls are used.
if
(
/iP
(
hone|od
)
/i
.
test
(
state
.
isTouch
[
0
]))
{
_hideWaitPlaceholder
(
state
);
state
.
el
.
trigger
(
'initialize'
,
arguments
);
return
false
;
}
_initializeModules
(
state
)
.
done
(
function
()
{
state
.
el
.
addClass
(
'is-initialized'
)
.
find
(
'.spinner'
)
.
attr
({
'aria-hidden'
:
'true'
,
'tabindex'
:
-
1
});
// On iPad ready state occurs just after start playing.
// We hide controls before video starts playing.
if
(
/iPad|Android/i
.
test
(
state
.
isTouch
[
0
]))
{
state
.
el
.
on
(
'play'
,
_
.
once
(
function
()
{
state
.
trigger
(
'videoControl.show'
,
null
);
}));
}
else
{
// On PC show controls immediately.
state
.
trigger
(
'videoControl.show'
,
null
);
}
_hideWaitPlaceholder
(
state
);
state
.
el
.
trigger
(
'initialize'
,
arguments
);
});
});
};
...
...
@@ -235,6 +249,16 @@ function (VideoPlayer) {
return
true
;
}
function
_hideWaitPlaceholder
(
state
)
{
state
.
el
.
addClass
(
'is-initialized'
)
.
find
(
'.spinner'
)
.
attr
({
'aria-hidden'
:
'true'
,
'tabindex'
:
-
1
});
}
function
_setConfigurations
(
state
)
{
_configureCaptions
(
state
);
_setPlayerMode
(
state
);
...
...
@@ -242,7 +266,7 @@ function (VideoPlayer) {
// Possible value are: 'visible', 'hiding', and 'invisible'.
state
.
controlState
=
'visible'
;
state
.
controlHideTimeout
=
null
;
state
.
captionState
=
'visible'
;
state
.
captionState
=
'
in
visible'
;
state
.
captionHideTimeout
=
null
;
}
...
...
@@ -299,12 +323,17 @@ function (VideoPlayer) {
// element has a CSS class 'fullscreen'.
this
.
__dfd__
=
$
.
Deferred
();
this
.
isFullScreen
=
false
;
this
.
isTouch
=
onTouchBasedDevice
()
||
''
;
// The parent element of the video, and the ID.
this
.
el
=
$
(
element
).
find
(
'.video'
);
this
.
elVideoWrapper
=
this
.
el
.
find
(
'.video-wrapper'
);
this
.
id
=
this
.
el
.
attr
(
'id'
).
replace
(
/video_/
,
''
);
if
(
this
.
isTouch
)
{
this
.
el
.
addClass
(
'is-touch'
);
}
// jQuery .data() return object with keys in lower camelCase format.
data
=
this
.
el
.
data
();
...
...
common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
View file @
23dc10d0
...
...
@@ -90,6 +90,10 @@ function () {
return
[
0.75
,
1.0
,
1.25
,
1.5
];
};
Player
.
prototype
.
_getLogs
=
function
()
{
return
this
.
logs
;
};
return
Player
;
/*
...
...
@@ -129,8 +133,10 @@ function () {
* }
*/
function
Player
(
el
,
config
)
{
var
sourceStr
,
_this
,
errorMessage
;
var
isTouch
=
onTouchBasedDevice
()
||
''
,
sourceStr
,
_this
,
errorMessage
;
this
.
logs
=
[];
// Initially we assume that el is a DOM element. If jQuery selector
// fails to select something, we assume that el is an ID of a DOM
// element. We try to select by ID. If jQuery fails this time, we
...
...
@@ -214,40 +220,51 @@ function () {
// determine what the video is currently doing.
this
.
videoEl
=
$
(
this
.
video
);
if
(
/iP
(
hone|od
)
/i
.
test
(
isTouch
[
0
]))
{
this
.
videoEl
.
prop
(
'controls'
,
true
);
}
this
.
playerState
=
HTML5Video
.
PlayerState
.
UNSTARTED
;
// Attach a 'click' event on the <video> element. It will cause the
// video to pause/play.
this
.
videoEl
.
on
(
'click'
,
function
(
event
)
{
if
(
_this
.
playerState
===
HTML5Video
.
PlayerState
.
PAUSED
)
{
_this
.
playVideo
();
_this
.
playerState
=
HTML5Video
.
PlayerState
.
PLAYING
;
_this
.
callStateChangeCallback
();
}
else
if
(
_this
.
playerState
===
HTML5Video
.
PlayerState
.
PLAYING
)
{
var
PlayerState
=
HTML5Video
.
PlayerState
;
if
(
_this
.
playerState
===
PlayerState
.
PLAYING
)
{
_this
.
pauseVideo
();
_this
.
playerState
=
HTML5Video
.
PlayerState
.
PAUSED
;
_this
.
playerState
=
PlayerState
.
PAUSED
;
_this
.
callStateChangeCallback
();
}
else
{
_this
.
playVideo
();
_this
.
playerState
=
PlayerState
.
PLAYING
;
_this
.
callStateChangeCallback
();
}
});
var
events
=
[
'loadstart'
,
'progress'
,
'suspend'
,
'abort'
,
'error'
,
'emptied'
,
'stalled'
,
'play'
,
'pause'
,
'loadedmetadata'
,
'loadeddata'
,
'waiting'
,
'playing'
,
'canplay'
,
'canplaythrough'
,
'seeking'
,
'seeked'
,
'timeupdate'
,
'ended'
,
'ratechange'
,
'durationchange'
,
'volumechange'
];
$
.
each
(
events
,
function
(
index
,
eventName
)
{
_this
.
video
.
addEventListener
(
eventName
,
function
()
{
_this
.
logs
.
push
({
'event name'
:
eventName
,
'state'
:
_this
.
playerState
});
el
.
trigger
(
'html5:'
+
eventName
,
arguments
);
});
});
// When the <video> tag has been processed by the browser, and it
// is ready for playback, notify other parts of the VideoPlayer,
// and initially pause the video.
this
.
video
.
addEventListener
(
'canplay'
,
function
()
{
// Because Firefox triggers 'canplay' event every time when
// 'currentTime' property changes, we must make sure that this
// block of code runs only once. Otherwise, this will be an
// endless loop ('currentTime' property is changed below).
//
// Chrome is immune to this behavior.
if
(
_this
.
playerState
!==
HTML5Video
.
PlayerState
.
UNSTARTED
)
{
return
;
}
this
.
video
.
addEventListener
(
'loadedmetadata'
,
function
()
{
_this
.
playerState
=
HTML5Video
.
PlayerState
.
PAUSED
;
if
(
$
.
isFunction
(
_this
.
config
.
events
.
onReady
))
{
_this
.
config
.
events
.
onReady
(
null
);
}
...
...
@@ -259,6 +276,10 @@ function () {
_this
.
callStateChangeCallback
();
},
false
);
this
.
video
.
addEventListener
(
'playing'
,
function
()
{
_this
.
playerState
=
HTML5Video
.
PlayerState
.
PLAYING
;
},
false
);
// Register the 'pause' event.
this
.
video
.
addEventListener
(
'pause'
,
function
()
{
_this
.
playerState
=
HTML5Video
.
PlayerState
.
PAUSED
;
...
...
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
View file @
23dc10d0
...
...
@@ -60,7 +60,7 @@ function (HTML5Video, Resizer) {
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
function
_initialize
(
state
)
{
var
youTubeId
,
player
,
videoWidth
,
videoHeight
;
var
youTubeId
,
player
;
// The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's
...
...
@@ -124,6 +124,24 @@ function (HTML5Video, Resizer) {
onStateChange
:
state
.
videoPlayer
.
onStateChange
}
});
player
=
state
.
videoEl
=
state
.
videoPlayer
.
player
.
videoEl
;
player
[
0
].
addEventListener
(
'loadedmetadata'
,
function
()
{
var
videoWidth
=
player
[
0
].
videoWidth
||
player
.
width
(),
videoHeight
=
player
[
0
].
videoHeight
||
player
.
height
();
_resize
(
state
,
videoWidth
,
videoHeight
);
state
.
trigger
(
'videoControl.updateVcrVidTime'
,
{
time
:
0
,
duration
:
state
.
videoPlayer
.
duration
()
}
);
},
false
);
}
else
{
// if (state.videoType === 'youtube') {
if
(
state
.
currentPlayerMode
===
'flash'
)
{
youTubeId
=
state
.
youtubeId
();
...
...
@@ -140,11 +158,18 @@ function (HTML5Video, Resizer) {
.
onPlaybackQualityChange
}
});
player
=
state
.
videoEl
=
state
.
el
.
find
(
'iframe'
);
videoWidth
=
player
.
attr
(
'width'
)
||
player
.
width
();
videoHeight
=
player
.
attr
(
'height'
)
||
player
.
height
();
_resize
(
state
,
videoWidth
,
videoHeight
);
state
.
el
.
on
(
'initialize'
,
function
()
{
var
player
=
state
.
videoEl
=
state
.
el
.
find
(
'iframe'
),
videoWidth
=
player
.
attr
(
'width'
)
||
player
.
width
(),
videoHeight
=
player
.
attr
(
'height'
)
||
player
.
height
();
_resize
(
state
,
videoWidth
,
videoHeight
);
});
}
if
(
state
.
isTouch
)
{
dfd
.
resolve
();
}
}
...
...
@@ -154,10 +179,17 @@ function (HTML5Video, Resizer) {
elementRatio
:
videoWidth
/
videoHeight
,
container
:
state
.
videoEl
.
parent
()
})
.
setMode
(
'width'
)
.
callbacks
.
once
(
function
()
{
state
.
trigger
(
'videoCaption.resize'
,
null
);
})
.
setMode
(
'width'
);
// Update captions size when controls becomes visible on iPad or Android
if
(
/iPad|Android/i
.
test
(
state
.
isTouch
[
0
]))
{
state
.
el
.
on
(
'controls:show'
,
function
()
{
state
.
trigger
(
'videoCaption.resize'
,
null
);
});
}
$
(
window
).
bind
(
'resize'
,
_
.
debounce
(
state
.
resizer
.
align
,
100
));
}
...
...
@@ -229,7 +261,7 @@ function (HTML5Video, Resizer) {
// video. `endTime` will be set to `null`, and this if statement
// will not be executed on next runs.
if
(
this
.
videoPlayer
.
endTime
!=
null
&&
this
.
videoPlayer
.
endTime
!=
=
null
&&
this
.
videoPlayer
.
endTime
<=
this
.
videoPlayer
.
currentTime
)
{
this
.
videoPlayer
.
pause
();
...
...
@@ -297,6 +329,8 @@ function (HTML5Video, Resizer) {
this
.
videoPlayer
.
player
[
methodName
](
youtubeId
,
time
);
this
.
videoPlayer
.
updatePlayTime
(
time
);
}
this
.
el
.
trigger
(
'speedchange'
,
arguments
);
}
// Every 200 ms, if the video is playing, we call the function update, via
...
...
@@ -343,6 +377,8 @@ function (HTML5Video, Resizer) {
}
this
.
videoPlayer
.
updatePlayTime
(
newTime
);
this
.
el
.
trigger
(
'seek'
,
arguments
);
}
function
onEnded
()
{
...
...
@@ -368,6 +404,8 @@ function (HTML5Video, Resizer) {
// `duration`. In this case, slider doesn't reach the end point of
// timeline.
this
.
videoPlayer
.
updatePlayTime
(
time
);
this
.
el
.
trigger
(
'ended'
,
arguments
);
}
function
onPause
()
{
...
...
@@ -386,6 +424,8 @@ function (HTML5Video, Resizer) {
if
(
this
.
config
.
show_captions
)
{
this
.
trigger
(
'videoCaption.pause'
,
null
);
}
this
.
el
.
trigger
(
'pause'
,
arguments
);
}
function
onPlay
()
{
...
...
@@ -415,6 +455,8 @@ function (HTML5Video, Resizer) {
}
this
.
videoPlayer
.
ready
();
this
.
el
.
trigger
(
'play'
,
arguments
);
}
function
onUnstarted
()
{
}
...
...
@@ -429,22 +471,17 @@ function (HTML5Video, Resizer) {
quality
=
this
.
videoPlayer
.
player
.
getPlaybackQuality
();
this
.
trigger
(
'videoQualityControl.onQualityChange'
,
quality
);
this
.
el
.
trigger
(
'qualitychange'
,
arguments
);
}
function
onReady
()
{
var
availablePlaybackRates
,
baseSpeedSubs
,
_this
,
var
_this
=
this
,
availablePlaybackRates
,
baseSpeedSubs
,
player
,
videoWidth
,
videoHeight
;
dfd
.
resolve
();
if
(
this
.
videoType
===
'html5'
)
{
player
=
this
.
videoEl
=
this
.
videoPlayer
.
player
.
videoEl
;
videoWidth
=
player
[
0
].
videoWidth
||
player
.
width
();
videoHeight
=
player
[
0
].
videoHeight
||
player
.
height
();
_resize
(
this
,
videoWidth
,
videoHeight
);
}
this
.
videoPlayer
.
log
(
'load_video'
);
availablePlaybackRates
=
this
.
videoPlayer
.
player
...
...
@@ -469,7 +506,7 @@ function (HTML5Video, Resizer) {
this
.
currentPlayerMode
===
'html5'
&&
this
.
videoType
===
'youtube'
)
{
if
(
availablePlaybackRates
.
length
===
1
)
{
if
(
availablePlaybackRates
.
length
===
1
&&
!
this
.
isTouch
)
{
// This condition is needed in cases when Firefox version is
// less than 20. In those versions HTML5 playback could only
// happen at 1 speed (no speed changing). Therefore, in this
...
...
@@ -479,14 +516,11 @@ function (HTML5Video, Resizer) {
// have 1 speed available, we fall back to Flash.
_restartUsingFlash
(
this
);
return
;
}
else
if
(
availablePlaybackRates
.
length
>
1
)
{
// We need to synchronize available frame rates with the ones
// that the user specified.
baseSpeedSubs
=
this
.
videos
[
'1.0'
];
_this
=
this
;
// this.videos is a dictionary containing various frame rates
// and their associated subs.
...
...
@@ -520,10 +554,11 @@ function (HTML5Video, Resizer) {
this
.
videoPlayer
.
player
.
setPlaybackRate
(
this
.
speed
);
}
this
.
el
.
trigger
(
'ready'
,
arguments
);
/* The following has been commented out to make sure autoplay is
disabled for students.
if (
!
onTouchBasedDevice()
&&
!
this.isTouch
&&
$('.video:first').data('autoplay') === 'True'
) {
this.videoPlayer.play();
...
...
@@ -735,6 +770,7 @@ function (HTML5Video, Resizer) {
function
onVolumeChange
(
volume
)
{
this
.
videoPlayer
.
player
.
setVolume
(
volume
);
this
.
el
.
trigger
(
'volumechange'
,
arguments
);
}
});
...
...
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
View file @
23dc10d0
...
...
@@ -32,9 +32,12 @@ function () {
var
methodsDict
=
{
exitFullScreen
:
exitFullScreen
,
hideControls
:
hideControls
,
hidePlayPlaceholder
:
hidePlayPlaceholder
,
pause
:
pause
,
play
:
play
,
show
:
show
,
showControls
:
showControls
,
showPlayPlaceholder
:
showPlayPlaceholder
,
toggleFullScreen
:
toggleFullScreen
,
togglePlayback
:
togglePlayback
,
updateVcrVidTime
:
updateVcrVidTime
...
...
@@ -54,16 +57,16 @@ function () {
state
.
videoControl
.
sliderEl
=
state
.
videoControl
.
el
.
find
(
'.slider'
);
state
.
videoControl
.
playPauseEl
=
state
.
videoControl
.
el
.
find
(
'.video_control'
);
state
.
videoControl
.
playPlaceholder
=
state
.
el
.
find
(
'.btn-play'
);
state
.
videoControl
.
secondaryControlsEl
=
state
.
videoControl
.
el
.
find
(
'.secondary-controls'
);
state
.
videoControl
.
fullScreenEl
=
state
.
videoControl
.
el
.
find
(
'.add-fullscreen'
);
state
.
videoControl
.
vidTimeEl
=
state
.
videoControl
.
el
.
find
(
'.vidtime'
);
state
.
videoControl
.
fullScreenState
=
false
;
state
.
videoControl
.
pause
();
if
(
!
onTouchBasedDevice
())
{
state
.
videoControl
.
pause
();
}
else
{
state
.
videoControl
.
play
();
if
(
state
.
isTouch
&&
state
.
videoType
===
'html5'
)
{
state
.
videoControl
.
showPlayPlaceholder
();
}
if
((
state
.
videoType
===
'html5'
)
&&
(
state
.
config
.
autohideHtml5
))
{
...
...
@@ -99,6 +102,13 @@ function () {
state
.
videoControl
.
playPauseEl
.
on
(
'blur'
,
function
()
{
state
.
previousFocus
=
'playPause'
;
});
if
(
/iPad|Android/i
.
test
(
state
.
isTouch
[
0
]))
{
state
.
videoControl
.
playPlaceholder
.
on
(
'click'
,
function
()
{
state
.
trigger
(
'videoPlayer.play'
,
null
);
});
}
}
// ***************************************************************
...
...
@@ -106,6 +116,11 @@ function () {
// 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().
// ***************************************************************
function
show
()
{
this
.
videoControl
.
el
.
removeClass
(
'is-hidden'
);
this
.
el
.
trigger
(
'controls:show'
,
arguments
);
}
function
showControls
(
event
)
{
if
(
!
this
.
controlShowLock
)
{
if
(
!
this
.
captionsHidden
)
{
...
...
@@ -157,14 +172,46 @@ function () {
});
}
function
showPlayPlaceholder
(
event
)
{
this
.
videoControl
.
playPlaceholder
.
removeClass
(
'is-hidden'
)
.
attr
({
'aria-hidden'
:
'false'
,
'tabindex'
:
0
});
}
function
hidePlayPlaceholder
(
event
)
{
this
.
videoControl
.
playPlaceholder
.
addClass
(
'is-hidden'
)
.
attr
({
'aria-hidden'
:
'true'
,
'tabindex'
:
-
1
});
}
function
play
()
{
this
.
videoControl
.
playPauseEl
.
removeClass
(
'play'
).
addClass
(
'pause'
).
attr
(
'title'
,
gettext
(
'Pause'
));
this
.
videoControl
.
isPlaying
=
true
;
this
.
videoControl
.
playPauseEl
.
removeClass
(
'play'
)
.
addClass
(
'pause'
)
.
attr
(
'title'
,
gettext
(
'Pause'
));
if
(
/iPad|Android/i
.
test
(
this
.
isTouch
[
0
])
&&
this
.
videoType
===
'html5'
)
{
this
.
videoControl
.
hidePlayPlaceholder
();
}
}
function
pause
()
{
this
.
videoControl
.
playPauseEl
.
removeClass
(
'pause'
).
addClass
(
'play'
).
attr
(
'title'
,
gettext
(
'Play'
));
this
.
videoControl
.
isPlaying
=
false
;
this
.
videoControl
.
playPauseEl
.
removeClass
(
'pause'
)
.
addClass
(
'play'
)
.
attr
(
'title'
,
gettext
(
'Play'
));
if
(
/iPad|Android/i
.
test
(
this
.
isTouch
[
0
])
&&
this
.
videoType
===
'html5'
)
{
this
.
videoControl
.
showPlayPlaceholder
();
}
}
function
togglePlayback
(
event
)
{
...
...
common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js
View file @
23dc10d0
...
...
@@ -12,6 +12,7 @@ function () {
// Changing quality for now only works for YouTube videos.
if
(
state
.
videoType
!==
'youtube'
)
{
state
.
el
.
find
(
'a.quality_control'
).
remove
();
return
;
}
...
...
common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js
View file @
23dc10d0
...
...
@@ -55,12 +55,10 @@ function () {
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
function
_renderElements
(
state
)
{
if
(
!
onTouchBasedDevice
())
{
state
.
videoProgressSlider
.
el
=
state
.
videoControl
.
sliderEl
;
state
.
videoProgressSlider
.
el
=
state
.
videoControl
.
sliderEl
;
buildSlider
(
state
);
_buildHandle
(
state
);
}
buildSlider
(
state
);
_buildHandle
(
state
);
}
function
_buildHandle
(
state
)
{
...
...
common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
View file @
23dc10d0
...
...
@@ -10,6 +10,13 @@ function () {
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
();
}
state
.
videoVolumeControl
=
{};
_makeFunctionsPublic
(
state
);
...
...
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
View file @
23dc10d0
...
...
@@ -10,6 +10,13 @@ function () {
return
function
(
state
)
{
var
dfd
=
$
.
Deferred
();
if
(
state
.
isTouch
)
{
// iOS doesn't support speed change
state
.
el
.
find
(
'div.speeds'
).
remove
();
dfd
.
resolve
();
return
dfd
.
promise
();
}
state
.
videoSpeedControl
=
{};
_initialize
(
state
);
...
...
@@ -131,7 +138,7 @@ function () {
state
.
videoSpeedControl
.
videoSpeedsEl
.
find
(
'a'
)
.
on
(
'click'
,
state
.
videoSpeedControl
.
changeVideoSpeed
);
if
(
onTouchBasedDevice
()
)
{
if
(
state
.
isTouch
)
{
state
.
videoSpeedControl
.
el
.
on
(
'click'
,
function
(
event
)
{
// So that you can't highlight this control via a drag
// operation, we disable the default browser actions on a
...
...
common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
View file @
23dc10d0
...
...
@@ -211,6 +211,8 @@ function () {
return
false
;
}
this
.
videoCaption
.
hideCaptions
(
this
.
hide_captions
);
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
$
.
ajaxWithPrefix
({
...
...
@@ -221,7 +223,7 @@ function () {
_this
.
videoCaption
.
start
=
captions
.
start
;
_this
.
videoCaption
.
loaded
=
true
;
if
(
onTouchBasedDevice
()
)
{
if
(
_this
.
isTouch
)
{
_this
.
videoCaption
.
subtitlesEl
.
find
(
'li'
).
html
(
gettext
(
'Caption will be displayed when '
+
...
...
@@ -231,6 +233,8 @@ function () {
}
else
{
_this
.
videoCaption
.
renderCaption
();
}
_this
.
videoCaption
.
bindHandlers
();
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
console
.
log
(
'[Video info]: ERROR while fetching captions.'
);
...
...
@@ -349,7 +353,8 @@ function () {
function
renderCaption
()
{
var
container
=
$
(
'<ol>'
),
_this
=
this
;
_this
=
this
,
autohideHtml5
=
this
.
config
.
autohideHtml5
;
this
.
elVideoWrapper
.
after
(
this
.
videoCaption
.
subtitlesEl
);
this
.
el
.
find
(
'.video-controls .secondary-controls'
)
...
...
@@ -357,28 +362,11 @@ function () {
this
.
videoCaption
.
setSubtitlesHeight
();
if
((
this
.
videoType
===
'html5'
)
&&
(
this
.
config
.
autohideHtml5
))
{
this
.
videoCaption
.
fadeOutTimeout
=
this
.
config
.
fadeOutTimeout
;
this
.
videoCaption
.
subtitlesEl
.
addClass
(
'html5'
);
this
.
captionHideTimeout
=
setTimeout
(
this
.
videoCaption
.
autoHideCaptions
,
this
.
videoCaption
.
fadeOutTimeout
);
}
else
if
(
!
this
.
config
.
autohideHtml5
)
{
if
((
this
.
videoType
===
'html5'
&&
autohideHtml5
)
||
!
autohideHtml5
)
{
this
.
videoCaption
.
fadeOutTimeout
=
this
.
config
.
fadeOutTimeout
;
this
.
videoCaption
.
subtitlesEl
.
addClass
(
'html5'
);
this
.
captionHideTimeout
=
setTimeout
(
this
.
videoCaption
.
autoHideCaptions
,
0
);
}
this
.
videoCaption
.
hideCaptions
(
this
.
hide_captions
);
this
.
videoCaption
.
bindHandlers
();
$
.
each
(
this
.
videoCaption
.
captions
,
function
(
index
,
text
)
{
var
liEl
=
$
(
'<li>'
);
...
...
lms/static/coffee/src/main.coffee
View file @
23dc10d0
...
...
@@ -6,7 +6,7 @@ $ ->
dataType
:
'json'
window
.
onTouchBasedDevice
=
->
navigator
.
userAgent
.
match
/iPhone|iPod|iPad/i
navigator
.
userAgent
.
match
/iPhone|iPod|iPad
|Android
/i
$
(
'body'
).
addClass
'touch-based-device'
if
onTouchBasedDevice
()
...
...
lms/templates/video.html
View file @
23dc10d0
...
...
@@ -6,7 +6,7 @@
<div
id=
"video_${id}"
class=
"video"
class=
"video
closed
"
data-streams=
"${youtube_streams}"
...
...
@@ -48,13 +48,14 @@
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"${_('Loading video player')}"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"${_('Play video')}"
></span>
<div
class=
"video-player-pre"
></div>
<section
class=
"video-player"
>
<div
id=
"${id}"
></div>
<h3
class=
"hidden"
>
${_('ERROR: No playable video sources found!')}
</h3>
</section>
<div
class=
"video-player-post"
></div>
<section
class=
"video-controls"
>
<section
class=
"video-controls
is-hidden
"
>
<div
class=
"slider"
title=
"Video position"
></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