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
ec6388b8
Commit
ec6388b8
authored
Apr 04, 2014
by
Anton Stupak
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3153 from edx/anton/caption_refactor
Refactor video caption module.
parents
dd7bae43
2f572a86
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
801 additions
and
712 deletions
+801
-712
common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+7
-19
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+12
-18
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
+5
-21
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
+0
-1
common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
+777
-653
No files found.
common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
View file @
ec6388b8
...
...
@@ -123,28 +123,16 @@
it
(
'bind the hide caption button'
,
function
()
{
state
=
jasmine
.
initializePlayer
();
expect
(
$
(
'.hide-subtitles'
)).
toHandleWith
(
'click'
,
state
.
videoCaption
.
toggle
);
expect
(
$
(
'.hide-subtitles'
)).
toHandle
(
'click'
);
});
it
(
'bind the mouse movement'
,
function
()
{
state
=
jasmine
.
initializePlayer
();
expect
(
$
(
'.subtitles'
)).
toHandleWith
(
'mouseover'
,
state
.
videoCaption
.
onMouseEnter
);
expect
(
$
(
'.subtitles'
)).
toHandleWith
(
'mouseout'
,
state
.
videoCaption
.
onMouseLeave
);
expect
(
$
(
'.subtitles'
)).
toHandleWith
(
'mousemove'
,
state
.
videoCaption
.
onMovement
);
expect
(
$
(
'.subtitles'
)).
toHandleWith
(
'mousewheel'
,
state
.
videoCaption
.
onMovement
);
expect
(
$
(
'.subtitles'
)).
toHandleWith
(
'DOMMouseScroll'
,
state
.
videoCaption
.
onMovement
);
expect
(
$
(
'.subtitles'
)).
toHandle
(
'mouseover'
);
expect
(
$
(
'.subtitles'
)).
toHandle
(
'mouseout'
);
expect
(
$
(
'.subtitles'
)).
toHandle
(
'mousemove'
);
expect
(
$
(
'.subtitles'
)).
toHandle
(
'mousewheel'
);
expect
(
$
(
'.subtitles'
)).
toHandle
(
'DOMMouseScroll'
);
});
it
(
'bind the scroll'
,
function
()
{
...
...
@@ -859,7 +847,7 @@
runs
(
function
()
{
videoControl
=
state
.
videoControl
;
$
(
'.subtitles li[data-index=1]'
).
addClass
(
'current'
);
state
.
videoCaption
.
r
esize
();
state
.
videoCaption
.
onR
esize
();
});
});
...
...
common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
View file @
ec6388b8
...
...
@@ -26,7 +26,6 @@ function (VideoPlayer) {
describe
(
'always'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
state
.
videoEl
=
$
(
'video, iframe'
);
});
...
...
@@ -211,7 +210,7 @@ function (VideoPlayer) {
state
.
videoEl
=
$
(
'video, iframe'
);
spyOn
(
state
.
videoControl
,
'pause'
).
andCallThrough
();
spyOn
(
state
.
videoCaption
,
'pause
'
).
andCallThrough
();
spyOn
(
$
.
fn
,
'trigger
'
).
andCallThrough
();
state
.
videoPlayer
.
onStateChange
({
data
:
YT
.
PlayerState
.
PAUSED
...
...
@@ -223,7 +222,7 @@ function (VideoPlayer) {
});
it
(
'pause the video caption'
,
function
()
{
expect
(
state
.
videoCaption
.
pause
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'pause'
,
{}
);
});
});
...
...
@@ -245,7 +244,7 @@ function (VideoPlayer) {
spyOn
(
state
.
videoPlayer
,
'log'
).
andCallThrough
();
spyOn
(
window
,
'setInterval'
).
andReturn
(
100
);
spyOn
(
state
.
videoControl
,
'play'
);
spyOn
(
state
.
videoCaption
,
'play'
);
spyOn
(
$
.
fn
,
'trigger'
).
andCallThrough
(
);
state
.
videoPlayer
.
onStateChange
({
data
:
YT
.
PlayerState
.
PLAYING
...
...
@@ -281,7 +280,7 @@ function (VideoPlayer) {
});
it
(
'play the video caption'
,
function
()
{
expect
(
state
.
videoCaption
.
play
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'play'
,
{}
);
});
});
...
...
@@ -295,7 +294,7 @@ function (VideoPlayer) {
spyOn
(
state
.
videoPlayer
,
'log'
).
andCallThrough
();
spyOn
(
state
.
videoControl
,
'pause'
).
andCallThrough
();
spyOn
(
state
.
videoCaption
,
'pause
'
).
andCallThrough
();
spyOn
(
$
.
fn
,
'trigger
'
).
andCallThrough
();
state
.
videoPlayer
.
onStateChange
({
data
:
YT
.
PlayerState
.
PLAYING
...
...
@@ -323,7 +322,7 @@ function (VideoPlayer) {
});
it
(
'pause the video caption'
,
function
()
{
expect
(
state
.
videoCaption
.
pause
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'pause'
,
{}
);
});
});
...
...
@@ -334,7 +333,7 @@ function (VideoPlayer) {
state
.
videoEl
=
$
(
'video, iframe'
);
spyOn
(
state
.
videoControl
,
'pause'
).
andCallThrough
();
spyOn
(
state
.
videoCaption
,
'pause
'
).
andCallThrough
();
spyOn
(
$
.
fn
,
'trigger
'
).
andCallThrough
();
state
.
videoPlayer
.
onStateChange
({
data
:
YT
.
PlayerState
.
ENDED
...
...
@@ -346,7 +345,7 @@ function (VideoPlayer) {
});
it
(
'pause the video caption'
,
function
()
{
expect
(
state
.
videoCaption
.
pause
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'ended'
,
{}
);
});
});
});
...
...
@@ -709,6 +708,7 @@ function (VideoPlayer) {
describe
(
'updatePlayTime with invalid endTime'
,
function
()
{
beforeEach
(
function
()
{
state
=
{
el
:
$
(
'#video_id'
),
videoPlayer
:
{
duration
:
function
()
{
// The video will be 60 seconds long.
...
...
@@ -756,10 +756,7 @@ function (VideoPlayer) {
describe
(
'when the video player is not full screen'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
state
.
videoEl
=
$
(
'video, iframe'
);
spyOn
(
state
.
videoCaption
,
'resize'
).
andCallThrough
();
spyOn
(
$
.
fn
,
'trigger'
).
andCallThrough
();
state
.
videoControl
.
toggleFullScreen
(
jQuery
.
Event
(
'click'
));
});
...
...
@@ -774,7 +771,7 @@ function (VideoPlayer) {
});
it
(
'tell VideoCaption to resize'
,
function
()
{
expect
(
state
.
videoCaption
.
resize
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'fullscreen'
,
[
true
]
);
expect
(
state
.
resizer
.
setMode
).
toHaveBeenCalledWith
(
'both'
);
expect
(
state
.
resizer
.
delta
.
substract
).
toHaveBeenCalled
();
});
...
...
@@ -783,11 +780,8 @@ function (VideoPlayer) {
describe
(
'when the video player already full screen'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
state
.
videoEl
=
$
(
'video, iframe'
);
spyOn
(
state
.
videoCaption
,
'resize'
).
andCallThrough
();
spyOn
(
$
.
fn
,
'trigger'
).
andCallThrough
();
state
.
el
.
addClass
(
'video-fullscreen'
);
state
.
videoControl
.
fullScreenState
=
true
;
state
.
videoControl
.
isFullScreen
=
true
;
...
...
@@ -806,7 +800,7 @@ function (VideoPlayer) {
});
it
(
'tell VideoCaption to resize'
,
function
()
{
expect
(
state
.
videoCaption
.
resize
).
toHaveBeenCalled
(
);
expect
(
$
.
fn
.
trigger
).
toHaveBeenCalledWith
(
'fullscreen'
,
[
false
]
);
expect
(
state
.
resizer
.
setMode
)
.
toHaveBeenCalledWith
(
'width'
);
expect
(
state
.
resizer
.
delta
.
reset
).
toHaveBeenCalled
();
...
...
common/lib/xmodule/xmodule/js/src/video/03_video_player.js
View file @
ec6388b8
...
...
@@ -223,20 +223,20 @@ function (HTML5Video, Resizer) {
container
:
state
.
container
})
.
callbacks
.
once
(
function
()
{
state
.
trigger
(
'videoCaption.resize'
,
null
);
state
.
el
.
trigger
(
'caption:resize'
);
})
.
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
);
state
.
el
.
trigger
(
'caption:resize'
);
});
}
$
(
window
).
on
(
'resize'
,
_
.
debounce
(
function
()
{
state
.
trigger
(
'videoControl.updateControlsHeight'
,
null
);
state
.
trigger
(
'videoCaption.resize'
,
null
);
state
.
el
.
trigger
(
'caption:resize'
);
state
.
resizer
.
align
();
},
100
));
}
...
...
@@ -271,7 +271,7 @@ function (HTML5Video, Resizer) {
});
_updateVcrAndRegion
(
state
,
true
);
state
.
trigger
(
'videoCaption.fetchCaption'
,
null
);
state
.
el
.
trigger
(
'caption:fetch'
);
state
.
resizer
.
setElement
(
state
.
el
.
find
(
'iframe'
)).
align
();
}
...
...
@@ -447,10 +447,6 @@ function (HTML5Video, Resizer) {
end
:
true
});
if
(
this
.
config
.
showCaptions
)
{
this
.
trigger
(
'videoCaption.pause'
,
null
);
}
if
(
this
.
videoPlayer
.
skipOnEndedStartEndReset
)
{
this
.
videoPlayer
.
skipOnEndedStartEndReset
=
undefined
;
}
...
...
@@ -475,11 +471,6 @@ function (HTML5Video, Resizer) {
delete
this
.
videoPlayer
.
updateInterval
;
this
.
trigger
(
'videoControl.pause'
,
null
);
if
(
this
.
config
.
showCaptions
)
{
this
.
trigger
(
'videoCaption.pause'
,
null
);
}
this
.
saveState
(
true
);
this
.
el
.
trigger
(
'pause'
,
arguments
);
}
...
...
@@ -501,17 +492,10 @@ function (HTML5Video, Resizer) {
}
this
.
trigger
(
'videoControl.play'
,
null
);
this
.
trigger
(
'videoProgressSlider.notifyThroughHandleEnd'
,
{
end
:
false
});
if
(
this
.
config
.
showCaptions
)
{
this
.
trigger
(
'videoCaption.play'
,
null
);
}
this
.
videoPlayer
.
ready
();
this
.
el
.
trigger
(
'play'
,
arguments
);
}
...
...
@@ -803,7 +787,7 @@ function (HTML5Video, Resizer) {
}
);
this
.
trigger
(
'videoCaption.updatePlayTime'
,
time
);
this
.
el
.
trigger
(
'caption:update'
,
[
time
]
);
}
function
isEnded
()
{
...
...
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
View file @
ec6388b8
...
...
@@ -277,7 +277,6 @@ function () {
.
attr
(
'title'
,
text
)
.
text
(
text
);
this
.
trigger
(
'videoCaption.resize'
,
null
);
this
.
el
.
trigger
(
'fullscreen'
,
[
this
.
isFullScreen
]);
}
...
...
common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
View file @
ec6388b8
...
...
@@ -5,734 +5,858 @@ define(
'video/09_video_caption.js'
,
[
'video/00_sjson.js'
,
'video/00_async_process.js'
],
function
(
Sjson
,
AsyncProcess
)
{
/**
* @desc VideoCaption module exports a function.
*
* @type {function}
* @access public
*
* @param {object} state - The object containg the state of the video
* @param {object} state - The object contain
in
g 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
}
* @returns {
jquery Promise
}
*/
return
function
(
state
)
{
state
.
videoCaption
=
{};
_makeFunctionsPublic
(
state
);
state
.
videoCaption
.
renderElements
();
var
VideoCaption
=
function
(
state
)
{
if
(
!
(
this
instanceof
VideoCaption
))
{
return
new
VideoCaption
(
state
);
}
this
.
state
=
state
;
this
.
state
.
videoCaption
=
this
;
this
.
renderElements
();
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
=
{
addPaddings
:
addPaddings
,
bindHandlers
:
bindHandlers
,
bottomSpacingHeight
:
bottomSpacingHeight
,
calculateOffset
:
calculateOffset
,
captionBlur
:
captionBlur
,
captionClick
:
captionClick
,
captionFocus
:
captionFocus
,
captionHeight
:
captionHeight
,
captionKeyDown
:
captionKeyDown
,
captionMouseDown
:
captionMouseDown
,
captionMouseOverOut
:
captionMouseOverOut
,
fetchCaption
:
fetchCaption
,
fetchAvailableTranslations
:
fetchAvailableTranslations
,
hideCaptions
:
hideCaptions
,
onMouseEnter
:
onMouseEnter
,
onMouseLeave
:
onMouseLeave
,
onMovement
:
onMovement
,
pause
:
pause
,
play
:
play
,
renderCaption
:
renderCaption
,
renderElements
:
renderElements
,
renderLanguageMenu
:
renderLanguageMenu
,
resize
:
resize
,
scrollCaption
:
scrollCaption
,
seekPlayer
:
seekPlayer
,
setSubtitlesHeight
:
setSubtitlesHeight
,
toggle
:
toggle
,
topSpacingHeight
:
topSpacingHeight
,
updatePlayTime
:
updatePlayTime
};
state
.
bindTo
(
methodsDict
,
state
.
videoCaption
,
state
);
}
// ***************************************************************
// 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().
// ***************************************************************
/**
* @desc 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.
*
* @type {function}
* @access public
*
* @this {object} - The object containg the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @returns {boolean}
* true: The function fethched captions successfully, and compltely
* rendered everything related to captions.
* false: The captions were not fetched. Nothing will be rendered,
* and the CC button will be hidden.
*/
function
renderElements
()
{
var
Caption
=
this
.
videoCaption
,
languages
=
this
.
config
.
transcriptLanguages
;
Caption
.
loaded
=
false
;
Caption
.
subtitlesEl
=
this
.
el
.
find
(
'ol.subtitles'
);
Caption
.
container
=
this
.
el
.
find
(
'.lang'
);
Caption
.
hideSubtitlesEl
=
this
.
el
.
find
(
'a.hide-subtitles'
);
if
(
_
.
keys
(
languages
).
length
)
{
Caption
.
renderLanguageMenu
(
languages
);
if
(
!
Caption
.
fetchCaption
())
{
Caption
.
hideCaptions
(
true
);
Caption
.
hideSubtitlesEl
.
hide
();
}
}
else
{
Caption
.
hideCaptions
(
true
,
false
);
Caption
.
hideSubtitlesEl
.
hide
();
}
}
// function bindHandlers()
//
// Bind any necessary function callbacks to DOM events (click,
// mousemove, etc.).
function
bindHandlers
()
{
var
self
=
this
,
Caption
=
this
.
videoCaption
,
events
=
[
'mouseover'
,
'mouseout'
,
'mousedown'
,
'click'
,
'focus'
,
'blur'
,
'keydown'
].
join
(
' '
);
Caption
.
hideSubtitlesEl
.
on
({
'click'
:
Caption
.
toggle
});
Caption
.
subtitlesEl
.
on
({
mouseenter
:
Caption
.
onMouseEnter
,
mouseleave
:
Caption
.
onMouseLeave
,
mousemove
:
Caption
.
onMovement
,
mousewheel
:
Caption
.
onMovement
,
DOMMouseScroll
:
Caption
.
onMovement
})
.
on
(
events
,
'li[data-index]'
,
function
(
event
)
{
switch
(
event
.
type
)
{
case
'mouseover'
:
case
'mouseout'
:
Caption
.
captionMouseOverOut
(
event
);
break
;
case
'mousedown'
:
Caption
.
captionMouseDown
(
event
);
break
;
case
'click'
:
Caption
.
captionClick
(
event
);
break
;
case
'focusin'
:
Caption
.
captionFocus
(
event
);
break
;
case
'focusout'
:
Caption
.
captionBlur
(
event
);
break
;
case
'keydown'
:
Caption
.
captionKeyDown
(
event
);
break
;
VideoCaption
.
prototype
=
{
/**
* @desc Initiate rendering of elements, and set their initial configuration.
*
*/
renderElements
:
function
()
{
var
state
=
this
.
state
,
languages
=
this
.
state
.
config
.
transcriptLanguages
;
this
.
loaded
=
false
;
this
.
subtitlesEl
=
state
.
el
.
find
(
'ol.subtitles'
);
this
.
container
=
state
.
el
.
find
(
'.lang'
);
this
.
hideSubtitlesEl
=
state
.
el
.
find
(
'a.hide-subtitles'
);
if
(
_
.
keys
(
languages
).
length
)
{
this
.
renderLanguageMenu
(
languages
);
if
(
!
this
.
fetchCaption
())
{
this
.
hideCaptions
(
true
);
this
.
hideSubtitlesEl
.
hide
();
}
});
if
(
Caption
.
showLanguageMenu
)
{
Caption
.
container
.
on
({
mouseenter
:
onContainerMouseEnter
,
mouseleave
:
onContainerMouseLeave
});
}
if
((
this
.
videoType
===
'html5'
)
&&
(
this
.
config
.
autohideHtml5
))
{
Caption
.
subtitlesEl
.
on
(
'scroll'
,
this
.
videoControl
.
showControls
);
}
}
function
onContainerMouseEnter
(
event
)
{
event
.
preventDefault
();
$
(
event
.
currentTarget
).
addClass
(
'open'
);
}
function
onContainerMouseLeave
(
event
)
{
event
.
preventDefault
();
$
(
event
.
currentTarget
).
removeClass
(
'open'
);
}
}
else
{
this
.
hideCaptions
(
true
,
false
);
this
.
hideSubtitlesEl
.
hide
();
}
},
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
*/
bindHandlers
:
function
()
{
var
self
=
this
,
state
=
this
.
state
,
events
=
[
'mouseover'
,
'mouseout'
,
'mousedown'
,
'click'
,
'focus'
,
'blur'
,
'keydown'
].
join
(
' '
);
// Change context to VideoCaption of event handlers using `bind`.
this
.
hideSubtitlesEl
.
on
(
'click'
,
this
.
toggle
.
bind
(
this
));
this
.
subtitlesEl
.
on
({
mouseenter
:
this
.
onMouseEnter
.
bind
(
this
),
mouseleave
:
this
.
onMouseLeave
.
bind
(
this
),
mousemove
:
this
.
onMovement
.
bind
(
this
),
mousewheel
:
this
.
onMovement
.
bind
(
this
),
DOMMouseScroll
:
this
.
onMovement
.
bind
(
this
)
})
.
on
(
events
,
'li[data-index]'
,
function
(
event
)
{
switch
(
event
.
type
)
{
case
'mouseover'
:
case
'mouseout'
:
self
.
captionMouseOverOut
(
event
);
break
;
case
'mousedown'
:
self
.
captionMouseDown
(
event
);
break
;
case
'click'
:
self
.
captionClick
(
event
);
break
;
case
'focusin'
:
self
.
captionFocus
(
event
);
break
;
case
'focusout'
:
self
.
captionBlur
(
event
);
break
;
case
'keydown'
:
self
.
captionKeyDown
(
event
);
break
;
}
});
function
onMouseEnter
()
{
if
(
this
.
videoCaption
.
frozen
)
{
clearTimeout
(
this
.
videoCaption
.
frozen
);
}
if
(
this
.
showLanguageMenu
)
{
this
.
container
.
on
({
mouseenter
:
this
.
onContainerMouseEnter
,
mouseleave
:
this
.
onContainerMouseLeave
});
}
this
.
videoCaption
.
frozen
=
setTimeout
(
this
.
videoCaption
.
onMouseLeave
,
this
.
config
.
captionsFreezeTime
);
}
state
.
el
.
on
({
'caption:fetch'
:
this
.
fetchCaption
.
bind
(
this
),
'caption:resize'
:
this
.
onResize
.
bind
(
this
),
'caption:update'
:
function
(
event
,
time
)
{
self
.
updatePlayTime
(
time
);
},
'ended'
:
this
.
pause
,
'fullscreen'
:
this
.
onResize
.
bind
(
this
),
'pause'
:
this
.
pause
,
'play'
:
this
.
play
,
});
if
((
state
.
videoType
===
'html5'
)
&&
(
state
.
config
.
autohideHtml5
))
{
this
.
subtitlesEl
.
on
(
'scroll'
,
state
.
videoControl
.
showControls
);
}
},
/**
* @desc Opens language menu.
*
* @param {jquery Event} event
*/
onContainerMouseEnter
:
function
(
event
)
{
event
.
preventDefault
();
$
(
event
.
currentTarget
).
addClass
(
'open'
);
},
/**
* @desc Closes language menu.
*
* @param {jquery Event} event
*/
onContainerMouseLeave
:
function
(
event
)
{
event
.
preventDefault
();
$
(
event
.
currentTarget
).
removeClass
(
'open'
);
},
/**
* @desc Freezes moving of captions when mouse is over them.
*
* @param {jquery Event} event
*/
onMouseEnter
:
function
(
event
)
{
if
(
this
.
frozen
)
{
clearTimeout
(
this
.
frozen
);
}
function
onMouseLeave
()
{
if
(
this
.
videoCaption
.
frozen
)
{
clearTimeout
(
this
.
videoCaption
.
frozen
);
}
this
.
frozen
=
setTimeout
(
this
.
onMouseLeave
,
this
.
state
.
config
.
captionsFreezeTime
);
},
/**
* @desc Unfreezes moving of captions when mouse go out.
*
* @param {jquery Event} event
*/
onMouseLeave
:
function
(
event
)
{
if
(
this
.
frozen
)
{
clearTimeout
(
this
.
frozen
);
}
this
.
videoCaption
.
frozen
=
null
;
this
.
frozen
=
null
;
if
(
this
.
videoCaption
.
playing
)
{
this
.
videoCaption
.
scrollCaption
();
}
}
if
(
this
.
playing
)
{
this
.
scrollCaption
();
}
},
/**
* @desc Freezes moving of captions when mouse is moving over them.
*
* @param {jquery Event} event
*/
onMovement
:
function
(
event
)
{
this
.
onMouseEnter
();
},
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
*
* @returns {boolean}
* true: The user specified a caption file. NOTE: if an error happens
* while the specified file is being retrieved (for example the
* file is missing on the server), this function will still return
* true.
* false: No caption file was specified, or an empty string was
* specified for the Youtube type player.
*/
fetchCaption
:
function
()
{
var
self
=
this
,
state
=
this
.
state
,
language
=
state
.
getCurrentLanguage
(),
data
,
youtubeId
;
if
(
this
.
loaded
)
{
this
.
hideCaptions
(
false
);
}
else
{
this
.
hideCaptions
(
state
.
hide_captions
,
false
);
}
function
onMovement
(
)
{
this
.
videoCaption
.
onMouseEnter
();
}
if
(
this
.
fetchXHR
&&
this
.
fetchXHR
.
abort
)
{
this
.
fetchXHR
.
abort
();
}
/**
* @desc Fetch the caption file specified by the user. Upn successful
* receival of the file, the captions will be rendered.
*
* @type {function}
* @access public
*
* @this {object} - The object containg the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @returns {boolean}
* true: The user specified a caption file. NOTE: if an error happens
* while the specified file is being retrieved (for example the
* file is missing on the server), this function will still return
* true.
* false: No caption file was specified, or an empty string was
* specified.
*/
function
fetchCaption
()
{
var
self
=
this
,
Caption
=
self
.
videoCaption
,
language
=
this
.
getCurrentLanguage
(),
data
;
if
(
Caption
.
loaded
)
{
Caption
.
hideCaptions
(
false
);
}
else
{
Caption
.
hideCaptions
(
this
.
hide_captions
,
false
);
}
if
(
state
.
videoType
===
'youtube'
)
{
youtubeId
=
state
.
youtubeId
(
'1.0'
);
if
(
Caption
.
fetchXHR
&&
Caption
.
fetchXHR
.
abort
)
{
Caption
.
fetchXHR
.
abort
()
;
}
if
(
!
youtubeId
)
{
return
false
;
}
if
(
this
.
videoType
===
'youtube'
)
{
data
=
{
videoId
:
this
.
youtubeId
(
'1.0'
)
};
}
data
=
{
videoId
:
youtubeId
};
}
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
Caption
.
fetchXHR
=
$
.
ajaxWithPrefix
({
url
:
self
.
config
.
transcriptTranslationUrl
+
'/'
+
language
,
notifyOnError
:
false
,
data
:
data
,
success
:
function
(
response
)
{
Caption
.
sjson
=
new
Sjson
(
response
);
var
start
=
Caption
.
sjson
.
getStartTimes
(),
captions
=
Caption
.
sjson
.
getCaptions
();
if
(
Caption
.
loaded
)
{
if
(
Caption
.
rendered
)
{
Caption
.
renderCaption
(
start
,
captions
);
Caption
.
updatePlayTime
(
self
.
videoPlayer
.
currentTime
);
}
}
else
{
if
(
self
.
isTouch
)
{
Caption
.
subtitlesEl
.
find
(
'li'
).
html
(
gettext
(
'Caption will be displayed when '
+
'you start playing the video.'
)
);
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
this
.
fetchXHR
=
$
.
ajaxWithPrefix
({
url
:
state
.
config
.
transcriptTranslationUrl
+
'/'
+
language
,
notifyOnError
:
false
,
data
:
data
,
success
:
function
(
sjson
)
{
self
.
sjson
=
new
Sjson
(
sjson
);
var
start
=
self
.
sjson
.
getStartTimes
(),
captions
=
self
.
sjson
.
getCaptions
();
if
(
self
.
loaded
)
{
if
(
self
.
rendered
)
{
self
.
renderCaption
(
start
,
captions
);
self
.
updatePlayTime
(
state
.
videoPlayer
.
currentTime
);
}
}
else
{
Caption
.
renderCaption
(
start
,
captions
);
if
(
state
.
isTouch
)
{
self
.
subtitlesEl
.
find
(
'li'
).
html
(
gettext
(
'Caption will be displayed when '
+
'you start playing the video.'
)
);
}
else
{
self
.
renderCaption
(
start
,
captions
);
}
self
.
bindHandlers
();
}
Caption
.
bindHandlers
();
self
.
loaded
=
true
;
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
console
.
log
(
'[Video info]: ERROR while fetching captions.'
);
console
.
log
(
'[Video info]: STATUS:'
,
textStatus
+
', MESSAGE:'
,
''
+
errorThrown
);
// If initial list of languages has more than 1 item, check
// for availability other transcripts.
if
(
_
.
keys
(
state
.
config
.
transcriptLanguages
).
length
>
1
)
{
self
.
fetchAvailableTranslations
();
}
else
{
self
.
hideCaptions
(
true
,
false
);
self
.
hideSubtitlesEl
.
hide
();
}
}
});
Caption
.
loaded
=
true
;
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
console
.
log
(
'[Video info]: ERROR while fetching captions.'
);
console
.
log
(
'[Video info]: STATUS:'
,
textStatus
+
', MESSAGE:'
,
''
+
errorThrown
);
// If initial list of languages has more than 1 item, check
// for availability other transcripts.
if
(
_
.
keys
(
self
.
config
.
transcriptLanguages
).
length
>
1
)
{
Caption
.
fetchAvailableTranslations
();
}
else
{
Caption
.
hideCaptions
(
true
,
false
);
Caption
.
hideSubtitlesEl
.
hide
();
}
}
});
return
true
;
}
function
fetchAvailableTranslations
()
{
var
self
=
this
,
Caption
=
this
.
videoCaption
;
return
$
.
ajaxWithPrefix
({
url
:
self
.
config
.
transcriptAvailableTranslationsUrl
,
notifyOnError
:
false
,
success
:
function
(
response
)
{
var
currentLanguages
=
self
.
config
.
transcriptLanguages
,
newLanguages
=
_
.
pick
(
currentLanguages
,
response
);
// Update property with available currently translations.
self
.
config
.
transcriptLanguages
=
newLanguages
;
// Remove an old language menu.
Caption
.
container
.
find
(
'.langs-list'
).
remove
();
if
(
_
.
keys
(
newLanguages
).
length
)
{
// And try again to fetch transcript.
Caption
.
fetchCaption
();
Caption
.
renderLanguageMenu
(
newLanguages
);
return
true
;
},
/**
* @desc Fetch the list of available translations. Upon successful receipt,
* the list of available translations will be updated.
*
* @returns {jquery Promise}
*/
fetchAvailableTranslations
:
function
()
{
var
self
=
this
,
state
=
this
.
state
;
return
$
.
ajaxWithPrefix
({
url
:
state
.
config
.
transcriptAvailableTranslationsUrl
,
notifyOnError
:
false
,
success
:
function
(
response
)
{
var
currentLanguages
=
state
.
config
.
transcriptLanguages
,
newLanguages
=
_
.
pick
(
currentLanguages
,
response
);
// Update property with available currently translations.
state
.
config
.
transcriptLanguages
=
newLanguages
;
// Remove an old language menu.
self
.
container
.
find
(
'.langs-list'
).
remove
();
if
(
_
.
keys
(
newLanguages
).
length
)
{
// And try again to fetch transcript.
self
.
fetchCaption
();
self
.
renderLanguageMenu
(
newLanguages
);
}
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
self
.
hideCaptions
(
true
,
false
);
self
.
hideSubtitlesEl
.
hide
();
}
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
Caption
.
hideCaptions
(
true
,
false
);
Caption
.
hideSubtitlesEl
.
hide
();
});
},
/**
* @desc Recalculates and updates the height of the container of captions.
*
*/
onResize
:
function
()
{
this
.
subtitlesEl
.
find
(
'.spacing'
).
first
()
.
height
(
this
.
topSpacingHeight
()).
end
()
.
find
(
'.spacing'
).
last
()
.
height
(
this
.
bottomSpacingHeight
());
this
.
scrollCaption
();
this
.
setSubtitlesHeight
();
},
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration for the Language menu.
*
* @param {object} languages Dictionary where key is language code,
* value - language label
*
*/
renderLanguageMenu
:
function
(
languages
)
{
var
self
=
this
,
state
=
this
.
state
,
menu
=
$
(
'<ol class="langs-list menu">'
),
currentLang
=
state
.
getCurrentLanguage
();
if
(
_
.
keys
(
languages
).
length
<
2
)
{
return
false
;
}
});
}
function
resize
()
{
this
.
videoCaption
.
subtitlesEl
.
find
(
'.spacing:first'
)
.
height
(
this
.
videoCaption
.
topSpacingHeight
())
.
find
(
'.spacing:last'
)
.
height
(
this
.
videoCaption
.
bottomSpacingHeight
());
this
.
videoCaption
.
scrollCaption
();
this
.
videoCaption
.
setSubtitlesHeight
();
}
function
renderLanguageMenu
(
languages
)
{
var
self
=
this
,
menu
=
$
(
'<ol class="langs-list menu">'
),
currentLang
=
this
.
getCurrentLanguage
();
if
(
_
.
keys
(
languages
).
length
<
2
)
{
return
false
;
}
this
.
videoCaption
.
showLanguageMenu
=
true
;
this
.
showLanguageMenu
=
true
;
$
.
each
(
languages
,
function
(
code
,
label
)
{
var
li
=
$
(
'<li data-lang-code="'
+
code
+
'" />'
),
link
=
$
(
'<a href="javascript:void(0);">'
+
label
+
'</a>'
);
$
.
each
(
languages
,
function
(
code
,
label
)
{
var
li
=
$
(
'<li data-lang-code="'
+
code
+
'" />'
),
link
=
$
(
'<a href="javascript:void(0);">'
+
label
+
'</a>'
);
if
(
currentLang
===
code
)
{
li
.
addClass
(
'active'
);
}
if
(
currentLang
===
code
)
{
li
.
addClass
(
'active'
);
}
li
.
append
(
link
);
menu
.
append
(
li
);
});
li
.
append
(
link
);
menu
.
append
(
li
);
});
this
.
videoCaption
.
container
.
append
(
menu
);
this
.
container
.
append
(
menu
);
menu
.
on
(
'click'
,
'a'
,
function
(
e
)
{
var
el
=
$
(
e
.
currentTarget
).
parent
(),
Caption
=
self
.
videoCaption
,
langCode
=
el
.
data
(
'lang-code'
);
menu
.
on
(
'click'
,
'a'
,
function
(
e
)
{
var
el
=
$
(
e
.
currentTarget
).
parent
(),
state
=
self
.
state
,
langCode
=
el
.
data
(
'lang-code'
);
if
(
self
.
lang
!==
langCode
)
{
self
.
lang
=
langCode
;
self
.
storage
.
setItem
(
'language'
,
langCode
);
el
.
addClass
(
'active'
)
.
siblings
(
'li'
)
.
removeClass
(
'active'
);
if
(
state
.
lang
!==
langCode
)
{
state
.
lang
=
langCode
;
state
.
storage
.
setItem
(
'language'
,
langCode
);
el
.
addClass
(
'active'
)
.
siblings
(
'li'
)
.
removeClass
(
'active'
);
Caption
.
fetchCaption
();
}
});
}
function
buildCaptions
(
container
,
start
,
captions
)
{
var
process
=
function
(
text
,
index
)
{
var
liEl
=
$
(
'<li>'
,
{
'data-index'
:
index
,
'data-start'
:
start
[
index
],
'tabindex'
:
0
}).
html
(
text
);
return
liEl
[
0
];
self
.
fetchCaption
();
}
});
},
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration.
*
* @param {jQuery element} container Element in which captions will be
* inserted.
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
* @returns {object} jQuery's Promise object
*
*/
buildCaptions
:
function
(
container
,
start
,
captions
)
{
var
process
=
function
(
text
,
index
)
{
var
liEl
=
$
(
'<li>'
,
{
'data-index'
:
index
,
'data-start'
:
start
[
index
],
'tabindex'
:
0
}).
html
(
text
);
return
liEl
[
0
];
};
return
AsyncProcess
.
array
(
captions
,
process
).
done
(
function
(
list
)
{
container
.
append
(
list
);
});
},
/**
* @desc Initiates creating of captions and set their initial configuration.
*
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
*
*/
renderCaption
:
function
(
start
,
captions
)
{
var
self
=
this
;
var
onRender
=
function
()
{
self
.
addPaddings
();
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have
// this flag enabled as we assume mouse use. Then, if the first
// caption (through forward tabbing) or the last caption (through
// backwards tabbing) gets the focus, disable that feature.
// Re-enable it if tabbing then cycles out of the the captions.
self
.
autoScrolling
=
true
;
// Keeps track of where the focus is situated in the array of
// captions. Used to implement the automatic scrolling behavior and
// decide if the outline around a caption has to be hidden or shown
// on a mouseenter or mouseleave. Initially, no caption has the
// focus, set the index to -1.
self
.
currentCaptionIndex
=
-
1
;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
self
.
isMouseFocus
=
false
;
self
.
rendered
=
true
;
};
return
AsyncProcess
.
array
(
captions
,
process
).
done
(
function
(
list
)
{
container
.
append
(
list
);
});
}
function
renderCaption
(
start
,
captions
)
{
var
Caption
=
this
.
videoCaption
;
var
onRender
=
function
()
{
Caption
.
addPaddings
();
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have
// this flag enabled as we assume mouse use. Then, if the first
// caption (through forward tabbing) or the last caption (through
// backwards tabbing) gets the focus, disable that feature.
// Re-enable it if tabbing then cycles out of the the captions.
Caption
.
autoScrolling
=
true
;
// Keeps track of where the focus is situated in the array of
// captions. Used to implement the automatic scrolling behavior and
// decide if the outline around a caption has to be hidden or shown
// on a mouseenter or mouseleave. Initially, no caption has the
// focus, set the index to -1.
Caption
.
currentCaptionIndex
=
-
1
;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
Caption
.
isMouseFocus
=
false
;
Caption
.
rendered
=
true
;
};
Caption
.
rendered
=
false
;
Caption
.
subtitlesEl
.
empty
();
Caption
.
setSubtitlesHeight
();
buildCaptions
(
Caption
.
subtitlesEl
,
start
,
captions
).
done
(
onRender
);
}
function
addPaddings
()
{
// Set top and bottom spacing height and make sure they are taken out of
// the tabbing order.
this
.
videoCaption
.
subtitlesEl
.
prepend
(
$
(
'<li class="spacing">'
)
.
height
(
this
.
videoCaption
.
topSpacingHeight
())
.
attr
(
'tabindex'
,
-
1
)
)
.
append
(
$
(
'<li class="spacing">'
)
.
height
(
this
.
videoCaption
.
bottomSpacingHeight
())
.
attr
(
'tabindex'
,
-
1
)
);
}
// On mouseOver, hide the outline of a caption that has been tabbed to.
// On mouseOut, show the outline of a caption that has been tabbed to.
function
captionMouseOverOut
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
if
(
captionIndex
===
this
.
videoCaption
.
currentCaptionIndex
)
{
if
(
event
.
type
===
'mouseover'
)
{
caption
.
removeClass
(
'focused'
);
}
else
{
// mouseout
caption
.
addClass
(
'focused'
);
this
.
rendered
=
false
;
this
.
subtitlesEl
.
empty
();
this
.
setSubtitlesHeight
();
this
.
buildCaptions
(
this
.
subtitlesEl
,
start
,
captions
).
done
(
onRender
);
},
/**
* @desc Sets top and bottom spacing height and make sure they are taken
* out of the tabbing order.
*
*/
addPaddings
:
function
()
{
this
.
subtitlesEl
.
prepend
(
$
(
'<li class="spacing">'
)
.
height
(
this
.
topSpacingHeight
())
.
attr
(
'tabindex'
,
-
1
)
)
.
append
(
$
(
'<li class="spacing">'
)
.
height
(
this
.
bottomSpacingHeight
())
.
attr
(
'tabindex'
,
-
1
)
);
},
/**
* @desc
* On mouseOver: Hides the outline of a caption that has been tabbed to.
* On mouseOut: Shows the outline of a caption that has been tabbed to.
*
* @param {jquery Event} event
*
*/
captionMouseOverOut
:
function
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
if
(
captionIndex
===
this
.
currentCaptionIndex
)
{
if
(
event
.
type
===
'mouseover'
)
{
caption
.
removeClass
(
'focused'
);
}
else
{
// mouseout
caption
.
addClass
(
'focused'
);
}
}
}
}
function
captionMouseDown
(
event
)
{
var
caption
=
$
(
event
.
target
);
this
.
videoCaption
.
isMouseFocus
=
true
;
this
.
videoCaption
.
autoScrolling
=
true
;
caption
.
removeClass
(
'focused'
);
this
.
videoCaption
.
currentCaptionIndex
=
-
1
;
}
function
captionClick
(
event
)
{
this
.
videoCaption
.
seekPlayer
(
event
);
}
function
captionFocus
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
// If the focus comes from a mouse click, hide the outline, turn on
// automatic scrolling and set currentCaptionIndex to point outside of
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
if
(
this
.
videoCaption
.
isMouseFocus
)
{
this
.
videoCaption
.
autoScrolling
=
true
;
},
/**
* @desc Handles mousedown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionMouseDown
:
function
(
event
)
{
var
caption
=
$
(
event
.
target
);
this
.
isMouseFocus
=
true
;
this
.
autoScrolling
=
true
;
caption
.
removeClass
(
'focused'
);
this
.
videoCaption
.
currentCaptionIndex
=
-
1
;
}
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else
{
this
.
videoCaption
.
currentCaptionIndex
=
captionIndex
;
caption
.
addClass
(
'focused'
);
// The second and second to last elements turn automatic scrolling
// off again as it may have been enabled in captionBlur.
if
(
captionIndex
<=
1
||
captionIndex
>=
this
.
videoCaption
.
sjson
.
getSize
()
-
2
)
{
this
.
videoCaption
.
autoScrolling
=
false
;
this
.
currentCaptionIndex
=
-
1
;
},
/**
* @desc Handles click event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionClick
:
function
(
event
)
{
this
.
seekPlayer
(
event
);
},
/**
* @desc Handles focus event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionFocus
:
function
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
// If the focus comes from a mouse click, hide the outline, turn on
// automatic scrolling and set currentCaptionIndex to point outside of
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
if
(
this
.
isMouseFocus
)
{
this
.
autoScrolling
=
true
;
caption
.
removeClass
(
'focused'
);
this
.
currentCaptionIndex
=
-
1
;
}
}
}
function
captionBlur
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
caption
.
removeClass
(
'focused'
);
// If we are on first or last index, we have to turn automatic scroll
// on again when losing focus. There is no way to know in what
// direction we are tabbing. So we could be on the first element and
// tabbing back out of the captions or on the last element and tabbing
// forward out of the captions.
if
(
captionIndex
===
0
||
captionIndex
===
this
.
videoCaption
.
sjson
.
getSize
()
-
1
)
{
this
.
videoCaption
.
autoScrolling
=
true
;
}
}
function
captionKeyDown
(
event
)
{
this
.
videoCaption
.
isMouseFocus
=
false
;
if
(
event
.
which
===
13
)
{
//Enter key
this
.
videoCaption
.
seekPlayer
(
event
);
}
}
function
scrollCaption
()
{
var
el
=
this
.
videoCaption
.
subtitlesEl
.
find
(
'.current:first'
);
// Automatic scrolling gets disabled if one of the captions has
// received focus through tabbing.
if
(
!
this
.
videoCaption
.
frozen
&&
el
.
length
&&
this
.
videoCaption
.
autoScrolling
)
{
this
.
videoCaption
.
subtitlesEl
.
scrollTo
(
el
,
{
offset
:
-
this
.
videoCaption
.
calculateOffset
(
el
)
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else
{
this
.
currentCaptionIndex
=
captionIndex
;
caption
.
addClass
(
'focused'
);
// The second and second to last elements turn automatic scrolling
// off again as it may have been enabled in captionBlur.
if
(
captionIndex
<=
1
||
captionIndex
>=
this
.
sjson
.
getSize
()
-
2
)
{
this
.
autoScrolling
=
false
;
}
);
}
}
function
play
()
{
if
(
this
.
videoCaption
.
loaded
)
{
if
(
!
this
.
videoCaption
.
rendered
)
{
var
start
=
this
.
videoCaption
.
sjson
.
getStartTimes
(),
captions
=
this
.
videoCaption
.
sjson
.
getCaptions
();
this
.
videoCaption
.
renderCaption
(
start
,
captions
);
}
},
/**
* @desc Handles blur event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionBlur
:
function
(
event
)
{
var
caption
=
$
(
event
.
target
),
captionIndex
=
parseInt
(
caption
.
attr
(
'data-index'
),
10
);
this
.
videoCaption
.
playing
=
true
;
}
}
function
pause
()
{
if
(
this
.
videoCaption
.
loaded
)
{
this
.
videoCaption
.
playing
=
false
;
}
}
function
updatePlayTime
(
time
)
{
var
newIndex
;
if
(
this
.
videoCaption
.
loaded
)
{
if
(
this
.
isFlashMode
())
{
time
=
Time
.
convert
(
time
,
this
.
speed
,
'1.0'
);
caption
.
removeClass
(
'focused'
);
// If we are on first or last index, we have to turn automatic scroll
// on again when losing focus. There is no way to know in what
// direction we are tabbing. So we could be on the first element and
// tabbing back out of the captions or on the last element and tabbing
// forward out of the captions.
if
(
captionIndex
===
0
||
captionIndex
===
this
.
sjson
.
getSize
()
-
1
)
{
this
.
autoScrolling
=
true
;
}
},
/**
* @desc Handles keydown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionKeyDown
:
function
(
event
)
{
this
.
isMouseFocus
=
false
;
if
(
event
.
which
===
13
)
{
//Enter key
this
.
seekPlayer
(
event
);
}
},
time
=
Math
.
round
(
time
*
1000
+
100
);
newIndex
=
this
.
videoCaption
.
sjson
.
search
(
time
);
/**
* @desc Scrolls caption container to make active caption visible.
*
*/
scrollCaption
:
function
()
{
var
el
=
this
.
subtitlesEl
.
find
(
'.current:first'
);
// Automatic scrolling gets disabled if one of the captions has
// received focus through tabbing.
if
(
typeof
newIndex
!==
'undefined'
&&
newIndex
!==
-
1
&&
this
.
videoCaption
.
currentIndex
!==
newIndex
!
this
.
frozen
&&
el
.
length
&&
this
.
autoScrolling
)
{
if
(
typeof
this
.
videoCaption
.
currentIndex
!==
'undefined'
)
{
this
.
videoCaption
.
subtitlesEl
.
find
(
'li.current'
)
.
removeClass
(
'current'
);
this
.
subtitlesEl
.
scrollTo
(
el
,
{
offset
:
-
1
*
this
.
calculateOffset
(
el
)
}
);
}
},
/**
* @desc Updates flags on play
*
*/
play
:
function
()
{
if
(
this
.
loaded
)
{
if
(
!
this
.
rendered
)
{
var
start
=
this
.
sjson
.
getStartTimes
(),
captions
=
this
.
sjson
.
getCaptions
();
this
.
renderCaption
(
start
,
captions
);
}
this
.
videoCaption
.
subtitlesEl
.
find
(
"li[data-index='"
+
newIndex
+
"']"
)
.
addClass
(
'current'
);
this
.
videoCaption
.
currentIndex
=
newIndex
;
this
.
videoCaption
.
scrollCaption
();
this
.
playing
=
true
;
}
}
}
function
seekPlayer
(
event
)
{
var
time
=
parseInt
(
$
(
event
.
target
).
data
(
'start'
),
10
);
if
(
this
.
isFlashMode
())
{
time
=
Math
.
round
(
Time
.
convert
(
time
,
'1.0'
,
this
.
speed
));
}
this
.
trigger
(
'videoPlayer.onCaptionSeek'
,
{
'type'
:
'onCaptionSeek'
,
'time'
:
time
/
1000
},
/**
* @desc Updates flags on pause
*
*/
pause
:
function
()
{
if
(
this
.
loaded
)
{
this
.
playing
=
false
;
}
);
event
.
preventDefault
();
}
function
calculateOffset
(
element
)
{
return
this
.
videoCaption
.
captionHeight
()
/
2
-
element
.
height
()
/
2
;
}
function
topSpacingHeight
()
{
return
this
.
videoCaption
.
calculateOffset
(
this
.
videoCaption
.
subtitlesEl
.
find
(
'li:not(.spacing):first'
)
);
}
function
bottomSpacingHeight
()
{
return
this
.
videoCaption
.
calculateOffset
(
this
.
videoCaption
.
subtitlesEl
.
find
(
'li:not(.spacing):last'
)
);
}
function
toggle
(
event
)
{
event
.
preventDefault
();
if
(
this
.
el
.
hasClass
(
'closed'
))
{
this
.
videoCaption
.
hideCaptions
(
false
);
}
else
{
this
.
videoCaption
.
hideCaptions
(
true
);
}
}
},
/**
* @desc Updates captions UI on paying.
*
* @param {number} time Time in seconds.
*
*/
updatePlayTime
:
function
(
time
)
{
var
state
=
this
.
state
,
newIndex
;
if
(
this
.
loaded
)
{
if
(
state
.
isFlashMode
())
{
time
=
Time
.
convert
(
time
,
state
.
speed
,
'1.0'
);
}
function
hideCaptions
(
hide_captions
,
update_cookie
)
{
var
hideSubtitlesEl
=
this
.
videoCaption
.
hideSubtitlesEl
,
type
,
text
;
time
=
Math
.
round
(
time
*
1000
+
100
);
newIndex
=
this
.
sjson
.
search
(
time
);
if
(
typeof
newIndex
!==
'undefined'
&&
newIndex
!==
-
1
&&
this
.
currentIndex
!==
newIndex
)
{
if
(
typeof
this
.
currentIndex
!==
'undefined'
)
{
this
.
subtitlesEl
.
find
(
'li.current'
)
.
removeClass
(
'current'
);
}
if
(
typeof
update_cookie
===
'undefined'
)
{
update_cookie
=
true
;
}
this
.
subtitlesEl
.
find
(
"li[data-index='"
+
newIndex
+
"']"
)
.
addClass
(
'current'
);
if
(
hide_captions
)
{
type
=
'hide_transcript'
;
this
.
captionsHidden
=
true
;
this
.
currentIndex
=
newIndex
;
this
.
scrollCaption
();
}
}
},
/**
* @desc Sends log to the server on caption seek.
*
* @param {jquery Event} event
*
*/
seekPlayer
:
function
(
event
)
{
var
state
=
this
.
state
,
time
=
parseInt
(
$
(
event
.
target
).
data
(
'start'
),
10
);
if
(
state
.
isFlashMode
())
{
time
=
Math
.
round
(
Time
.
convert
(
time
,
'1.0'
,
state
.
speed
));
}
this
.
el
.
addClass
(
'closed'
);
state
.
trigger
(
'videoPlayer.onCaptionSeek'
,
{
'type'
:
'onCaptionSeek'
,
'time'
:
time
/
1000
}
);
text
=
gettext
(
'Turn on captions'
);
}
else
{
type
=
'show_transcript'
;
this
.
captionsHidden
=
false
;
event
.
preventDefault
();
},
/**
* @desc Calculates offset for paddings.
*
* @param {jquery element} element Top or bottom padding element.
* @returns {number} Offset for the passed padding element.
*
*/
calculateOffset
:
function
(
element
)
{
return
this
.
captionHeight
()
/
2
-
element
.
height
()
/
2
;
},
/**
* @desc Calculates offset for the top padding element.
*
* @returns {number} Offset for the passed top padding element.
*
*/
topSpacingHeight
:
function
()
{
return
this
.
calculateOffset
(
this
.
subtitlesEl
.
find
(
'li:not(.spacing)'
).
first
()
);
},
/**
* @desc Calculates offset for the bottom padding element.
*
* @returns {number} Offset for the passed bottom padding element.
*
*/
bottomSpacingHeight
:
function
()
{
return
this
.
calculateOffset
(
this
.
subtitlesEl
.
find
(
'li:not(.spacing)'
).
last
()
);
},
/**
* @desc Shows/Hides captions on click `CC` button
*
* @param {jquery Event} event
*
*/
toggle
:
function
(
event
)
{
event
.
preventDefault
();
if
(
this
.
state
.
el
.
hasClass
(
'closed'
))
{
this
.
hideCaptions
(
false
);
}
else
{
this
.
hideCaptions
(
true
);
}
},
/**
* @desc Shows/Hides captions and updates the cookie.
*
* @param {boolean} hide_captions if `true` hides the caption,
* otherwise - show.
* @param {boolean} update_cookie Flag to update or not the cookie.
*
*/
hideCaptions
:
function
(
hide_captions
,
update_cookie
)
{
var
hideSubtitlesEl
=
this
.
hideSubtitlesEl
,
state
=
this
.
state
,
type
,
text
;
if
(
typeof
update_cookie
===
'undefined'
)
{
update_cookie
=
true
;
}
this
.
el
.
removeClass
(
'closed'
);
this
.
videoCaption
.
scrollCaption
();
if
(
hide_captions
)
{
type
=
'hide_transcript'
;
state
.
captionsHidden
=
true
;
state
.
el
.
addClass
(
'closed'
);
text
=
gettext
(
'Turn on captions'
);
}
else
{
type
=
'show_transcript'
;
state
.
captionsHidden
=
false
;
state
.
el
.
removeClass
(
'closed'
);
this
.
scrollCaption
();
text
=
gettext
(
'Turn off captions'
);
}
text
=
gettext
(
'Turn off captions'
);
}
hideSubtitlesEl
.
attr
(
'title'
,
text
)
.
text
(
gettext
(
text
));
hideSubtitlesEl
.
attr
(
'title'
,
text
)
.
text
(
gettext
(
text
));
if
(
state
.
videoPlayer
)
{
state
.
videoPlayer
.
log
(
type
,
{
currentTime
:
state
.
videoPlayer
.
currentTime
});
}
if
(
this
.
videoPlayer
)
{
this
.
videoPlayer
.
log
(
type
,
{
currentTime
:
this
.
videoPlayer
.
currentTime
});
}
if
(
state
.
resizer
)
{
if
(
state
.
isFullScreen
)
{
state
.
resizer
.
setMode
(
'both'
);
}
else
{
state
.
resizer
.
alignByWidthOnly
();
}
}
if
(
this
.
resizer
)
{
if
(
this
.
isFullScreen
)
{
this
.
resizer
.
setMode
(
'both'
);
this
.
setSubtitlesHeight
();
if
(
update_cookie
)
{
$
.
cookie
(
'hide_captions'
,
hide_captions
,
{
expires
:
3650
,
path
:
'/'
});
}
},
/**
* @desc Return the caption container height.
*
* @returns {number} event Height of the container in pixels.
*
*/
captionHeight
:
function
()
{
var
state
=
this
.
state
;
if
(
state
.
isFullScreen
)
{
return
state
.
container
.
height
()
-
state
.
videoControl
.
height
;
}
else
{
this
.
resizer
.
alignByWidthOnly
();
return
state
.
container
.
height
();
}
},
/**
* @desc Sets the height of the caption container element.
*
*/
setSubtitlesHeight
:
function
()
{
var
height
=
0
,
state
=
this
.
state
;
// on page load captionHidden = undefined
if
((
state
.
captionsHidden
===
undefined
&&
state
.
hide_captions
)
||
state
.
captionsHidden
===
true
)
{
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height
=
state
.
videoControl
.
el
.
height
()
+
0.5
*
state
.
videoControl
.
sliderEl
.
height
();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
}
this
.
videoCaption
.
setSubtitlesHeight
();
if
(
update_cookie
)
{
$
.
cookie
(
'hide_captions'
,
hide_captions
,
{
expires
:
3650
,
path
:
'/'
this
.
subtitlesEl
.
css
({
maxHeight
:
this
.
captionHeight
()
-
height
});
}
}
function
captionHeight
()
{
if
(
this
.
isFullScreen
)
{
return
this
.
container
.
height
()
-
this
.
videoControl
.
height
;
}
else
{
return
this
.
container
.
height
();
}
}
function
setSubtitlesHeight
()
{
var
height
=
0
;
// on page load captionHidden = undefined
if
((
this
.
captionsHidden
===
undefined
&&
this
.
hide_captions
)
||
this
.
captionsHidden
===
true
)
{
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height
=
this
.
videoControl
.
el
.
height
()
+
0.5
*
this
.
videoControl
.
sliderEl
.
height
();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
};
this
.
videoCaption
.
subtitlesEl
.
css
({
maxHeight
:
this
.
videoCaption
.
captionHeight
()
-
height
});
}
return
VideoCaption
;
});
}(
RequireJS
.
define
));
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