158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
import json
|
|
import logging
|
|
|
|
from lxml import etree
|
|
from pkg_resources import resource_string, resource_listdir
|
|
|
|
from django.http import Http404
|
|
|
|
from xmodule.x_module import XModule
|
|
from xmodule.raw_module import RawDescriptor
|
|
from xmodule.modulestore.mongo import MongoModuleStore
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.contentstore.content import StaticContent
|
|
from xblock.core import Integer, Scope, String
|
|
|
|
import datetime
|
|
import time
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class VideoAlphaFields(object):
|
|
data = String(help="XML data for the problem", scope=Scope.content)
|
|
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
|
|
display_name = String(help="Display name for this module", scope=Scope.settings)
|
|
|
|
|
|
class VideoAlphaModule(VideoAlphaFields, XModule):
|
|
"""
|
|
XML source example:
|
|
|
|
<videoalpha show_captions="true"
|
|
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
|
|
url_name="lecture_21_3" display_name="S19V3: Vacancies"
|
|
>
|
|
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
|
|
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
|
|
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
|
|
</videoalpha>
|
|
"""
|
|
video_time = 0
|
|
icon_class = 'video'
|
|
|
|
js = {
|
|
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
|
|
'coffee':
|
|
[resource_string(__name__, 'js/src/time.coffee'),
|
|
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
|
|
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
|
|
for filename
|
|
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
|
|
if filename.endswith('.coffee')]}
|
|
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
|
|
js_module_name = "VideoAlpha"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
XModule.__init__(self, *args, **kwargs)
|
|
xmltree = etree.fromstring(self.data)
|
|
self.youtube_streams = xmltree.get('youtube')
|
|
self.sub = xmltree.get('sub')
|
|
self.position = 0
|
|
self.show_captions = xmltree.get('show_captions', 'true')
|
|
self.sources = {
|
|
'main': self._get_source(xmltree),
|
|
'mp4': self._get_source(xmltree, ['mp4']),
|
|
'webm': self._get_source(xmltree, ['webm']),
|
|
'ogv': self._get_source(xmltree, ['ogv']),
|
|
}
|
|
self.track = self._get_track(xmltree)
|
|
self.start_time, self.end_time = self._get_timeframe(xmltree)
|
|
|
|
def _get_source(self, xmltree, exts=None):
|
|
"""Find the first valid source, which ends with one of `exts`."""
|
|
exts = ['mp4', 'ogv', 'avi', 'webm'] if exts is None else exts
|
|
condition = lambda src: any([src.endswith(ext) for ext in exts])
|
|
return self._get_first_external(xmltree, 'source', condition)
|
|
|
|
def _get_track(self, xmltree):
|
|
# find the first valid track
|
|
return self._get_first_external(xmltree, 'track')
|
|
|
|
def _get_first_external(self, xmltree, tag, condition=bool):
|
|
"""Will return the first 'valid' element of the given tag.
|
|
'valid' means that `condition('src' attribute) == True`
|
|
"""
|
|
result = None
|
|
|
|
for element in xmltree.findall(tag):
|
|
src = element.get('src')
|
|
if condition(src):
|
|
result = src
|
|
break
|
|
return result
|
|
|
|
def _get_timeframe(self, xmltree):
|
|
""" Converts 'start_time' and 'end_time' parameters in video tag to seconds.
|
|
If there are no parameters, returns empty string. """
|
|
|
|
def parse_time(s):
|
|
"""Converts s in '12:34:45' format to seconds. If s is
|
|
None, returns empty string"""
|
|
if s is None:
|
|
return ''
|
|
else:
|
|
x = time.strptime(s, '%H:%M:%S')
|
|
return datetime.timedelta(
|
|
hours=x.tm_hour,
|
|
minutes=x.tm_min,
|
|
seconds=x.tm_sec
|
|
).total_seconds()
|
|
|
|
return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time'))
|
|
|
|
def handle_ajax(self, dispatch, get):
|
|
"""Handle ajax calls to this video.
|
|
TODO (vshnayder): This is not being called right now, so the
|
|
position is not being saved.
|
|
"""
|
|
log.debug(u"GET {0}".format(get))
|
|
log.debug(u"DISPATCH {0}".format(dispatch))
|
|
if dispatch == 'goto_position':
|
|
self.position = int(float(get['position']))
|
|
log.info(u"NEW POSITION {0}".format(self.position))
|
|
return json.dumps({'success': True})
|
|
raise Http404()
|
|
|
|
def get_instance_state(self):
|
|
return json.dumps({'position': self.position})
|
|
|
|
def get_html(self):
|
|
if isinstance(modulestore(), MongoModuleStore):
|
|
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
|
|
else:
|
|
# VS[compat]
|
|
# cdodge: filesystem static content support.
|
|
caption_asset_path = "/static/subs/"
|
|
|
|
return self.system.render_template('videoalpha.html', {
|
|
'youtube_streams': self.youtube_streams,
|
|
'id': self.location.html_id(),
|
|
'sub': self.sub,
|
|
'sources': self.sources,
|
|
'track': self.track,
|
|
'display_name': self.display_name_with_default,
|
|
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
|
'data_dir': getattr(self, 'data_dir', None),
|
|
'caption_asset_path': caption_asset_path,
|
|
'show_captions': self.show_captions,
|
|
'start': self.start_time,
|
|
'end': self.end_time
|
|
})
|
|
|
|
|
|
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
|
|
module_class = VideoAlphaModule
|
|
stores_state = True
|
|
template_dir_name = "videoalpha"
|