Rewritten video module

This commit is contained in:
Prem Sichanugrist
2012-05-01 17:54:11 -04:00
parent 0b26609846
commit 8624927fc4
14 changed files with 530 additions and 188 deletions

View File

@@ -30,32 +30,19 @@ class Module(XModule):
def get_xml_tags(c):
'''Tags in the courseware file guaranteed to correspond to the module'''
return ["video"]
def video_list(self):
l = self.youtube.split(',')
l = [i.split(":") for i in l]
return json.dumps(dict(l))
def get_html(self):
return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position,
'name':self.name,
'position':self.position,
'name':self.name,
'annotations':self.annotations})
def get_init_js(self):
'''JavaScript code to be run when problem is shown. Be aware
that this may happen several times on the same page
(e.g. student switching tabs). Common functions should be put
in the main course .js files for now. '''
log.debug(u"INIT POSITION {0}".format(self.position))
return render_to_string('video_init.js',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position})+self.annotations_init
def get_destroy_js(self):
return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
@@ -69,5 +56,3 @@ class Module(XModule):
self.annotations=[(e.get("name"),self.render_function(e)) \
for e in xmltree]
self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]])
self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]])

View File

@@ -13,6 +13,8 @@ class window.Courseware
autoHeight: false
$('#open_close_accordion a').click navigation.toggle
$('#accordion').show()
log: (event, ui) ->
log_event 'accordion',
newheader: ui.newHeader.text()

View File

@@ -2,7 +2,11 @@ $ ->
$.ajaxSetup
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i
Calculator.bind()
Courseware.bind()
FeedbackForm.bind()
$("a[rel*=leanModal]").leanModal()

11
static/js/jquery.scrollTo-1.4.2-min.js vendored Normal file
View File

@@ -0,0 +1,11 @@
/**
* jQuery.ScrollTo - Easy element scrolling using jQuery.
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
* Dual licensed under MIT and GPL.
* Date: 5/25/2009
* @author Ariel Flesler
* @version 1.4.2
*
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
*/
;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery);

View File

@@ -14,38 +14,29 @@ section.course-content {
}
}
div.video-subtitles {
div.video {
@include clearfix();
background: #f3f3f3;
border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
@include clearfix();
display: block;
margin: 0 (-(lh()));
padding: 6px lh();
div.video-wrapper {
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
div.video-player {
section.video-player {
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
padding-top: 30px;
position: relative;
object {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
iframe#html5_player {
object, iframe {
border: none;
display: none;
height: 100%;
left: 0;
position: absolute;
@@ -68,7 +59,7 @@ section.course-content {
}
}
div#slider {
div.slider {
@extend .clearfix;
background: #c2c2c2;
border: none;
@@ -175,7 +166,8 @@ section.course-content {
}
}
div#vidtime {
div.vidtime {
padding-left: lh(.75);
font-weight: bold;
line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
@@ -190,6 +182,7 @@ section.course-content {
div.speeds {
float: left;
position: relative;
a {
background: url('../images/closed-arrow.png') 10px center no-repeat;
@@ -211,7 +204,7 @@ section.course-content {
&.open {
background: url('../images/open-arrow.png') 10px center no-repeat;
ol#video_speeds {
ol.video_speeds {
display: block;
opacity: 1;
}
@@ -234,47 +227,52 @@ section.course-content {
padding: 0 lh(.5) 0 0;
}
// fix for now
ol#video_speeds {
&:hover, &:active, &:focus {
opacity: 1;
background-color: #444;
border: 1px solid #000;
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
display: none;
left: -1px;
opacity: 0;
position: absolute;
top:0;
@include transition();
width: 100%;
z-index: 10;
}
}
li {
border-bottom: 1px solid #000;
@include box-shadow( 0 1px 0 #555);
// fix for now
ol.video_speeds {
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
@include transition();
background-color: #444;
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
position: absolute;
width: 125px;
z-index: 10;
li {
@include box-shadow( 0 1px 0 #555);
border-bottom: 1px solid #000;
color: #fff;
cursor: pointer;
a {
border: 0;
color: #fff;
cursor: pointer;
padding: 0 lh(.5);
&.active {
font-weight: bold;
}
&:last-child {
border-bottom: 0;
@include box-shadow(none);
margin-top: 0;
}
display: block;
padding: lh(.5);
&:hover {
background-color: #666;
color: #aaa;
}
}
}
&:hover {
background-color: #444;
opacity: 1;
&.active {
font-weight: bold;
}
&:last-child {
@include box-shadow(none);
border-bottom: 0;
margin-top: 0;
}
}
}
}
@@ -334,7 +332,7 @@ section.course-content {
opacity: 1;
}
div#slider {
div.slider {
height: 14px;
margin-top: -7px;
@@ -352,7 +350,7 @@ section.course-content {
ol.subtitles {
float: left;
max-height: 460px;
overflow: hidden;
overflow: auto;
padding-top: 10px;
width: flex-grid(3, 9);
@@ -360,7 +358,7 @@ section.course-content {
border: 0;
color: #666;
cursor: pointer;
margin-bottom: 0px;
margin-bottom: 8px;
padding: 0;
@include transition(all, .5s, ease-in);
@@ -373,11 +371,7 @@ section.course-content {
color: $mit-red;
}
div {
margin-bottom: 8px;
}
div:empty {
&:empty {
margin-bottom: 0px;
}
}
@@ -386,7 +380,7 @@ section.course-content {
&.closed {
@extend .trans;
div.video-wrapper {
article.video-wrapper {
width: flex-grid(9,9);
}
@@ -441,11 +435,11 @@ section.course-content {
}
div.tc-wrapper {
div.video-wrapper {
article.video-wrapper {
width: 100%;
}
object#myytplayer, iframe {
object, iframe {
bottom: 0;
height: 100%;
left: 0;
@@ -487,7 +481,7 @@ section.course-content {
}
}
div.course-wrapper.closed section.course-content div.video-subtitles {
div.course-wrapper.closed section.course-content div.video {
ol.subtitles {
max-height: 577px;
}

View File

@@ -0,0 +1,41 @@
class window.Video
constructor: (@id, videos) ->
@element = $("#video_#{@id}")
@parseVideos videos
@fetchMetadata()
@parseSpeed()
$("#video_#{@id}").data('video', this)
window.onYouTubePlayerAPIReady = =>
$('.course-content .video').each ->
$(this).data('video').embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseVideos: (videos) ->
@videos = {}
$.each videos, (speed, url) =>
speed = parseFloat(speed).toFixed(2).replace /\.00$/, '.0'
@videos[speed] = url
parseSpeed: ->
@setSpeed($.cookie('video_speed'))
@speeds = ($.map @videos, (url, speed) -> speed).sort()
setSpeed: (newSpeed) ->
if @videos[newSpeed] != undefined
@speed = newSpeed
$.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/')
else
@speed = '1.0'
embed: ->
@player = new VideoPlayer(this)
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "http://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration

View File

@@ -0,0 +1,116 @@
class VideoCaption
constructor: (@player, @youtubeId) ->
@index = []
@fetchCaption()
@bind()
$: (selector) ->
@player.$(selector)
bind: ->
$(window).bind('resize', @onWindowResize)
$(@player).bind('resize', @onWindowResize)
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
@$('.hide-subtitles').click @toggle
fetchCaption: ->
$.getJSON @captionURL(), (captions) =>
@captions = captions.text
@start = captions.start
for index in [0...captions.start.length]
for time in [captions.start[index]..captions.end[index]]
@index[time] ||= []
@index[time].push(index)
@render()
captionURL: ->
"/static/subs/#{@youtubeId}.srt.sjson"
render: ->
container = $('<ol class="subtitles">')
container.css maxHeight: @$('.video-wrapper').height() - 5
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').replaceWith(container)
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmatic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
onUpdatePlayTime: (event, time) =>
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250)
newIndex = @index[time]
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
for index in @currentIndex
@$(".subtitles li[data-index='#{index}']").removeClass('current')
for index in newIndex
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
onWindowResize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: (event) =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @player.isPlaying()
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @player.currentSpeed()) / 1000)
$(@player).trigger('seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing).first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing).last'))
toggle: (event) =>
event.preventDefault()
if @player.element.hasClass('closed')
@$('.hide-subtitles').attr('title', 'Turn off captions')
@player.element.removeClass('closed')
else
@$('.hide-subtitles').attr('title', 'Turn on captions')
@player.element.addClass('closed')
@scrollCaption()
captionHeight: ->
if @player.element.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()

View File

@@ -0,0 +1,145 @@
class VideoPlayer
constructor: (@video) ->
@currentTime = 0
@element = $("#video_#{@video.id}")
@buildPlayer()
@bind()
$: (selector) ->
$(selector, @element)
bind: ->
$(@).bind('seek', @onSeek)
$(@).bind('updatePlayTime', @onUpdatePlayTime)
$(@).bind('speedChange', @onSpeedChange)
$(document).keyup @bindExitFullScreen
@$('.video_control').click @togglePlayback
@$('.add-fullscreen').click @toggleFullScreen
@addToolTip unless onTouchBasedDevice()
bindExitFullScreen: (event) =>
if @element.hasClass('fullscreen') && event.keyCode == 27
@toggleFullScreen(event)
buildPlayer: ->
new VideoCaption(this, @video.youtubeId('1.0'))
new VideoSpeedControl(this, @video.speeds)
new VideoProgressSlider(this)
@player = new YT.Player @video.id,
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
videoId: @video.youtubeId()
events:
onReady: @onReady
onStateChange: @onStateChange
addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip
position:
my: 'top right'
at: 'top center'
onReady: =>
@setProgress(0, @duration())
$(@).trigger('ready')
unless true || onTouchBasedDevice()
$('.course-content .video:first').data('video').player.play()
onStateChange: (event) =>
switch event.data
when YT.PlayerState.PLAYING
if window.player && window.player != @player
window.player.pauseVideo()
window.player = @player
@onPlay()
when YT.PlayerState.PAUSED
if window.player == @player
window.player = null
@onPause()
when YT.PlayerState.ENDED
if window.player == @player
window.player = null
@onPause()
onPlay: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
unless @player.interval
@player.interval = setInterval(@update, 200)
onPause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
clearInterval(@player.interval)
@player.interval = null
onSeek: (event, time) ->
@player.seekTo(time, true)
if @isPlaying()
clearInterval(@player.interval)
@player.interval = setInterval(@update, 200)
else
@currentTime = time
$(@).trigger('updatePlayTime', time)
onSpeedChange: (event, newSpeed) =>
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
@video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0')
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
@setProgress(@currentTime, @duration())
$(@).trigger('updatePlayTime', @currentTime)
update: =>
if @currentTime = @player.getCurrentTime()
$(@).trigger('updatePlayTime', @currentTime)
onUpdatePlayTime: (event, time) =>
@setProgress(@currentTime) if time
setProgress: (time) =>
progress = Time.format(time) + ' / ' + Time.format(@duration())
if @progress != progress
@$(".vidtime").html(progress)
@progress = progress
togglePlayback: (event) =>
event.preventDefault()
if $(event.target).hasClass('play')
@play()
else
@pause()
toggleFullScreen: (event) =>
event.preventDefault()
if @element.hasClass('fullscreen')
@$('.exit').remove()
@$('.add-fullscreen').attr('title', 'Fill browser')
@element.removeClass('fullscreen')
else
@element.append('<a href="#" class="exit">Exit</a>').addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser')
@$('.exit').click @toggleFullScreen
$(@).trigger('resize')
# Delegates
play: ->
@player.playVideo() if @player.playVideo
isPlaying: ->
@player.getPlayerState() == YT.PlayerState.PLAYING
pause: ->
@player.pauseVideo()
duration: ->
@video.getDuration()
currentSpeed: ->
@video.speed

View File

@@ -0,0 +1,54 @@
class VideoProgressSlider
constructor: (@player) ->
@buildSlider()
@buildHandle()
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
$(@player).bind('ready', @onReady)
$: (selector) ->
@player.$(selector)
buildSlider: ->
@slider = @$('.slider').slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
buildHandle: ->
@handle = @$('.ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
onReady: =>
@slider.slider('option', 'max', @player.duration())
onUpdatePlayTime: (event, currentTime) =>
if !@frozen
@slider.slider('option', 'max', @player.duration())
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@player).trigger('seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@player).trigger('seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")

View File

@@ -0,0 +1,38 @@
class VideoSpeedControl
constructor: (@player, @speeds) ->
@build()
@bind()
$: (selector) ->
@player.$(selector)
bind: ->
$(@player).bind('speedChange', @onSpeedChange)
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click -> $(this).toggleClass('open')
else
@$('.speeds').mouseover -> $(this).addClass('open')
.mouseout -> $(this).removeClass('open')
.click (event) ->
event.preventDefault()
$(this).removeClass('open')
build: ->
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed(@player.currentSpeed())
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
$(@player).trigger 'speedChange', $(event.target).parent().data('speed')
onSpeedChange: (event, speed) =>
@setSpeed(parseFloat(speed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")

View File

@@ -0,0 +1,21 @@
class @Time
@format: (time) ->
seconds = Math.floor time
minutes = Math.floor seconds / 60
hours = Math.floor minutes / 60
seconds = seconds % 60
minutes = minutes % 60
if hours
"#{hours}:#{@pad(minutes)}:#{@pad(seconds % 60)}"
else
"#{minutes}:#{@pad(seconds % 60)}"
@pad: (number) ->
if number < 10
"0#{number}"
else
number
@convert: (time, oldSpeed, newSpeed) ->
(time * oldSpeed / newSpeed).toFixed(3)

View File

@@ -26,7 +26,7 @@
<a href="#">close</a>
</header>
<div id="accordion">
<div id="accordion" style="display: none">
<nav>
${accordion}
</nav>

View File

@@ -116,6 +116,11 @@
<script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
<script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
</script>
<%block name="js_extra"/>
</body>

View File

@@ -2,123 +2,49 @@
<h1> ${name} </h1>
% endif
<div class="video-subtitles">
<div class="tc-wrapper">
<div id="video_${id}" class="video">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="${id}"></div>
</section>
<div class="video-wrapper">
<div class="video-player">
<div id="ytapiplayer">
</div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control play">Play</a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<iframe id="html5_player" type="text/html" frameborder="0">
</iframe>
</div>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<section class="video-controls">
<div id="slider"></div>
<section>
<ul class="vcr">
<li><a id="video_control" class="pause">Pause</a></li>
<li>
<div id="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
<ol id="video_speeds"></ol>
</a>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</section>
</article>
<ol class="subtitles">
<li>Attempting to load captions...</li>
</ol>
</div>
<ol class="subtitles">
<!-- <li id="stt_n5"><div id="std_n7" onclick="title_seek(-7);"></div></li> -->
<li id="stt_n4"><div id="std_n6" onclick="title_seek(-6);"></div></li>
<li id="stt_n4"><div id="std_n5" onclick="title_seek(-5);"></div></li>
<li id="stt_n4"><div id="std_n4" onclick="title_seek(-4);"></div></li>
<li id="stt_n3"><div id="std_n3" onclick="title_seek(-3);"></div></li>
<li id="stt_n2"><div id="std_n2" onclick="title_seek(-2);"></div></li>
<li id="stt_n1"><div id="std_n1" onclick="title_seek(-1);"></div></li>
<li id="stt_0 "class="current"><div id="std_0" onclick="title_seek(0);"></div></li>
<li id="stt_p1"><div id="std_p1" onclick="title_seek( 1);"></div></li>
<li id="stt_p2"><div id="std_p2" onclick="title_seek( 2);"></div></li>
<li id="stt_p3"><div id="std_p3" onclick="title_seek( 3);"></div></li>
<li id="stt_p4"><div id="std_p4" onclick="title_seek( 4);"></div></li>
<li id="stt_p5"><div id="std_p5" onclick="title_seek( 5);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 6);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 7);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 8);"></div></li>
</ol>
</div>
</div>
<%block name="js_extra">
<script src="/static/js/jquery.ui.touch-punch.min.js"></script>
<script type="text/javascript" charset="utf-8">
$(function() {
// tooltips for full browser and closed caption
$('.add-fullscreen, .hide-subtitles ').qtip({
position: {
my: 'top right',
at: 'top center'
}
});
//full browser
$('.add-fullscreen').click(function() {
$('div.video-subtitles').toggleClass('fullscreen');
if ($('div.video-subtitles').hasClass('fullscreen')) {
$('div.video-subtitles').append('<a href="#" class="exit">Exit</a>');
} else {
$('a.exit').remove();
}
$('.exit').click(function() {
$('div.video-subtitles').removeClass('fullscreen');
$(this).remove();
return false;
});
var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Exit fill browser') ? 'Fill browser' : 'Exit fill browser');
return false;
});
//hide subtitles
$('.hide-subtitles').click(function() {
$('div.video-subtitles').toggleClass('closed');
var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Turn on captions') ? 'Turn off captions' : 'Turn on captions');
return false;
});
$("div.speeds a").hover(function() {
$(this).toggleClass("open");
});
$("div.speeds a").click(function() {
return false;
});
<script type="text/javascript">
$(function(){
new Video('${id}', ${streams});
});
</script>
</%block>