From 93d186511fabab6b3ad9f9fb55bd6a2e7c4b9620 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 12 Feb 2013 10:23:32 -0500 Subject: [PATCH 01/29] clean up templates that were removed from disk - we need to remove from DB as well --- common/lib/xmodule/xmodule/templates.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py index ce37df929f..eaf821155e 100644 --- a/common/lib/xmodule/xmodule/templates.py +++ b/common/lib/xmodule/xmodule/templates.py @@ -56,6 +56,10 @@ def update_templates(): available from the installed plugins """ + # cdodge: build up a list of all existing templates. This will be used to determine which + # templates have been removed from disk - and thus we need to remove from the DB + templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None]) + for category, templates in all_templates().items(): for template in templates: if 'display_name' not in template.metadata: @@ -85,3 +89,12 @@ def update_templates(): modulestore('direct').update_item(template_location, template.data) modulestore('direct').update_children(template_location, template.children) modulestore('direct').update_metadata(template_location, template.metadata) + + # remove template from list of templates to delete + templates_to_delete = [t for t in templates_to_delete if t.location != template_location] + + # now remove all templates which appear to have removed from disk + if len(templates_to_delete) > 0: + logging.debug('deleting dangling templates = {0}'.format(templates_to_delete)) + for template in templates_to_delete: + modulestore('direct').delete_item(template.location) From fbe3d29f195640edffa0e0b490faee34f7143efe Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 13 Feb 2013 09:25:09 -0500 Subject: [PATCH 02/29] added tests --- .../contentstore/tests/test_contentstore.py | 31 +++++++++++++++++++ cms/djangoapps/contentstore/tests/tests.py | 1 - 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index adecd392eb..4360b38f95 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -27,10 +27,12 @@ from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.templates import update_templates from xmodule.capa_module import CapaDescriptor from xmodule.course_module import CourseDescriptor from xmodule.seq_module import SequenceDescriptor +from xmodule.modulestore.exceptions import ItemNotFoundError TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') @@ -409,3 +411,32 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertIn('markdown', context, "markdown is missing from context") self.assertIn('markdown', problem.metadata, "markdown is missing from metadata") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + + +class TemplateTestCase(ModuleStoreTestCase): + + def test_template_cleanup(self): + ms = modulestore('direct') + + # insert a bogus template in the store + bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus') + source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty') + + ms.clone_item(source_template_location, bogus_template_location) + + verify_create = ms.get_item(bogus_template_location) + self.assertIsNotNone(verify_create) + + # now run cleanup + update_templates() + + # now try to find dangling template, it should not be in DB any longer + asserted = False + try: + verify_create = ms.get_item(bogus_template_location) + except ItemNotFoundError: + asserted = True + + self.assertTrue(asserted) + + diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 9af5b09276..393fd59f5c 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -86,7 +86,6 @@ class ContentStoreTestCase(ModuleStoreTestCase): # Now make sure that the user is now actually activated self.assertTrue(user(email).is_active) - class AuthTestCase(ContentStoreTestCase): """Check that various permissions-related things work""" From 857edaf6f38649bbbe834cb7437fcc0fbfade950 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 13 Feb 2013 12:23:08 -0500 Subject: [PATCH 03/29] Remove dogfood and quickedit. --- lms/envs/common.py | 1 - lms/envs/dev_edx4edx.py | 1 - lms/envs/edx4edx_aws.py | 1 - lms/lib/dogfood/README.md | 1 - lms/lib/dogfood/__init__.py | 1 - lms/lib/dogfood/check.py | 61 ------- lms/lib/dogfood/views.py | 325 ----------------------------------- lms/templates/dogfood.html | 144 ---------------- lms/templates/gitupdate.html | 32 ---- lms/templates/quickedit.html | 180 ------------------- lms/urls.py | 4 - 11 files changed, 751 deletions(-) delete mode 100644 lms/lib/dogfood/README.md delete mode 100644 lms/lib/dogfood/__init__.py delete mode 100644 lms/lib/dogfood/check.py delete mode 100644 lms/lib/dogfood/views.py delete mode 100644 lms/templates/dogfood.html delete mode 100644 lms/templates/gitupdate.html delete mode 100644 lms/templates/quickedit.html diff --git a/lms/envs/common.py b/lms/envs/common.py index f3bf223451..eb8c9989f0 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -200,7 +200,6 @@ COURSE_TITLE = "Circuits and Electronics" ### Dark code. Should be enabled in local settings for devel. ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome) -QUICKEDIT = False WIKI_ENABLED = False diff --git a/lms/envs/dev_edx4edx.py b/lms/envs/dev_edx4edx.py index c138ed81ae..2ebd24e68b 100644 --- a/lms/envs/dev_edx4edx.py +++ b/lms/envs/dev_edx4edx.py @@ -34,7 +34,6 @@ EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx" DEBUG = True ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome) -QUICKEDIT = True MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT] diff --git a/lms/envs/edx4edx_aws.py b/lms/envs/edx4edx_aws.py index de377c0b57..b82048824f 100644 --- a/lms/envs/edx4edx_aws.py +++ b/lms/envs/edx4edx_aws.py @@ -6,7 +6,6 @@ COURSE_TITLE = "edx4edx: edX Author Course" EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx" ### Dark code. Should be enabled in local settings for devel. -QUICKEDIT = True ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome) ### PIPELINE_CSS_COMPRESSOR = None diff --git a/lms/lib/dogfood/README.md b/lms/lib/dogfood/README.md deleted file mode 100644 index c6a7113049..0000000000 --- a/lms/lib/dogfood/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a library for edx4edx, allowing users to practice writing problems. diff --git a/lms/lib/dogfood/__init__.py b/lms/lib/dogfood/__init__.py deleted file mode 100644 index d00d8ea793..0000000000 --- a/lms/lib/dogfood/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from check import * diff --git a/lms/lib/dogfood/check.py b/lms/lib/dogfood/check.py deleted file mode 100644 index 070d3f9262..0000000000 --- a/lms/lib/dogfood/check.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python - -from random import choice -import string -import traceback - -from django.conf import settings -import capa.capa_problem as lcp -from dogfood.views import update_problem - - -def GenID(length=8, chars=string.letters + string.digits): - return ''.join([choice(chars) for i in range(length)]) - -randomid = GenID() - - -def check_problem_code(ans, the_lcp, correct_answers, false_answers): - """ - ans = student's answer - the_lcp = LoncapaProblem instance - - returns dict {'ok':is_ok,'msg': message with iframe} - """ - pfn = "dog%s" % randomid - pfn += the_lcp.problem_id.replace('filename', '') # add problem ID to dogfood problem name - update_problem(pfn, ans, filestore=the_lcp.system.filestore) - msg = '
' - msg += '' % (settings.MITX_ROOT_URL, pfn) - msg += '
' - - endmsg = """

Note: if the code text box disappears after clicking on "Check", - please type something in the box to make it refresh properly. This is a - bug with Chrome; it does not happen with Firefox. It is being fixed. -

""" - - is_ok = True - if (not correct_answers) or (not false_answers): - ret = {'ok': is_ok, - 'msg': msg + endmsg, - } - return ret - - try: - # check correctness - fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn) - test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system) - - if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1') == 'correct'): - is_ok = False - if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1') == 'correct'): - is_ok = False - except Exception, err: - is_ok = False - msg += "

Error: %s

" % str(err).replace('<', '<') - msg += "

%s

" % traceback.format_exc().replace('<', '<') - - ret = {'ok': is_ok, - 'msg': msg + endmsg, - } - return ret diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py deleted file mode 100644 index 6df881df98..0000000000 --- a/lms/lib/dogfood/views.py +++ /dev/null @@ -1,325 +0,0 @@ -''' -dogfood.py - -For using mitx / edX / i4x in checking itself. - -df_capa_problem: accepts an XML file for a problem, and renders it. -''' -import logging -import datetime -import re -import os # FIXME - use OSFS instead - -from fs.osfs import OSFS - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.context_processors import csrf -from django.core.mail import send_mail -from django.http import Http404 -from django.http import HttpResponse -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response, render_to_string - -import track.views -from lxml import etree - -from courseware.module_render import make_track_function, ModuleSystem, get_module -from courseware.models import StudentModule -from multicourse import multicourse_settings -from student.models import UserProfile -from util.cache import cache -from util.views import accepts - -import courseware.content_parser as content_parser -#import courseware.modules -import xmodule - -log = logging.getLogger("mitx.courseware") - -etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True)) - -DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings - - -def update_problem(pfn, pxml, coursename=None, overwrite=True, filestore=None): - ''' - update problem with filename pfn, and content (xml) pxml. - ''' - if not filestore: - if not coursename: coursename = DOGFOOD_COURSENAME - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn - fp = open(pfn2, 'w') - else: - pfn2 = 'problems/%s.xml' % pfn - fp = filestore.open(pfn2, 'w') - log.debug('[dogfood.update_problem] pfn2=%s' % pfn2) - - if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False - pxmls = pxml if type(pxml) in [str, unicode] else etree.tostring(pxml, pretty_print=True) - fp.write(pxmls) - fp.close() - - -def df_capa_problem(request, id=None): - ''' - dogfood capa problem. - - Accepts XML for a problem, inserts it into the dogfood course.xml. - Returns rendered problem. - ''' - # "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY." - - if settings.DEBUG: - log.debug('[lib.dogfood.df_capa_problem] id=%s' % id) - - if not 'coursename' in request.session: - coursename = DOGFOOD_COURSENAME - else: - coursename = request.session['coursename'] - - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - # Grab the XML corresponding to the request from course.xml - module = 'problem' - - try: - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - except Exception, err: - log.error("[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err) - xml = None - - # if problem of given ID does not exist, then create it - # do this only if course.xml has a section named "DogfoodProblems" - if not xml: - m = re.match('filename([A-Za-z0-9_]+)$', id) # extract problem filename from ID given - if not m: - raise Exception, '[lib.dogfood.df_capa_problem] Illegal problem id %s' % id - pfn = m.group(1) - log.debug('[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn) - - # add problem to course.xml - fn = settings.DATA_DIR + xp + 'course.xml' - xml = etree.parse(fn) - seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure! - if seq == None: - raise Exception, "[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!" - newprob = etree.Element('problem') - newprob.set('type', 'lecture') - newprob.set('showanswer', 'attempted') - newprob.set('rerandomize', 'never') - newprob.set('title', pfn) - newprob.set('filename', pfn) - newprob.set('name', pfn) - seq.append(newprob) - fp = open(fn, 'w') - fp.write(etree.tostring(xml, pretty_print=True)) # write new XML - fp.close() - - # now create new problem file - # update_problem(pfn,'\n\nThis is a new problem\n\n\n',coursename,overwrite=False) - - # reset cache entry - user = request.user - groups = content_parser.user_groups(user) - options = {'dev_content': settings.DEV_CONTENT, - 'groups': groups} - filename = xp + 'course.xml' - cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups)) - log.debug('[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key) - #cache.delete(cache_key) - tree = content_parser.course_xml_process(xml) # add ID tags - cache.set(cache_key, etree.tostring(tree), 60) - # settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache - - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - if not xml: - log.debug("[lib.dogfood.df_capa_problem] problem xml not found!") - - # add problem ID to list so that is_staff check can be bypassed - request.session['dogfood_id'] = id - - # hand over to quickedit to do the rest - return quickedit(request, id=id, qetemplate='dogfood.html', coursename=coursename) - - -def quickedit(request, id=None, qetemplate='quickedit.html', coursename=None): - ''' - quick-edit capa problem. - - Maybe this should be moved into capa/views.py - Or this should take a "module" argument, and the quickedit moved into capa_module. - - id is passed in from url resolution - qetemplate is used by dogfood.views.dj_capa_problem, to override normal template - ''' - print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY." - print "In deployed use, this will only edit on one server" - print "We need a setting to disable for production where there is" - print "a load balanacer" - - if not request.user.is_staff: - if not ('dogfood_id' in request.session and request.session['dogfood_id'] == id): - return redirect('/') - - if id == 'course.xml': - return quickedit_git_reload(request) - - # get coursename if stored - if not coursename: - coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - def get_lcp(coursename, id): - # Grab the XML corresponding to the request from course.xml - # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, - module_id=id) - student_module_cache = list(s) if s is not None else [] - #if len(s) == 0 or s is None: - # smod=StudentModule(student=request.user, - # module_type = 'problem', - # module_id=id, - # state=instance.get_state()) - # smod.save() - # student_module_cache = [smod] - module = 'problem' - module_xml = etree.XML(content_parser.module_xml(request.user, module, 'id', id, coursename)) - module_id = module_xml.get('id') - log.debug("module_id = %s" % module_id) - (instance, smod, module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None) - log.debug('[dogfood.views] instance=%s' % instance) - lcp = instance.lcp - log.debug('[dogfood.views] lcp=%s' % lcp) - pxml = lcp.tree - pxmls = etree.tostring(pxml, pretty_print=True) - return instance, pxmls - - def old_get_lcp(coursename, id): - # Grab the XML corresponding to the request from course.xml - module = 'problem' - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - - ajax_url = settings.MITX_ROOT_URL + '/modx/' + id + '/' - - # Create the module (instance of capa_module.Module) - system = ModuleSystem(track_function=make_track_function(request), - render_function=None, - render_template=render_to_string, - ajax_url=ajax_url, - filestore=OSFS(settings.DATA_DIR + xp), - ) - instance = xmodule.get_module_class(module)(system, - xml, - id, - state=None) - log.info('ajax_url = ' + instance.ajax_url) - - # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, - module_state_key=id) - if len(s) == 0 or s is None: - smod = StudentModule(student=request.user, - module_type='problem', - module_state_key=id, - state=instance.get_instance_state()) - smod.save() - - lcp = instance.lcp - pxml = lcp.tree - pxmls = etree.tostring(pxml, pretty_print=True) - - return instance, pxmls - - instance, pxmls = get_lcp(coursename, id) - - # if there was a POST, then process it - msg = '' - if 'qesubmit' in request.POST: - action = request.POST['qesubmit'] - if "Revert" in action: - msg = "Reverted to original" - elif action == 'Change Problem': - key = 'quickedit_%s' % id - if not key in request.POST: - msg = "oops, missing code key=%s" % key - else: - newcode = request.POST[key] - - # see if code changed - if str(newcode) == str(pxmls) or '\n' + str(newcode) == str(pxmls): - msg = "No changes" - else: - # check new code - isok = False - try: - newxml = etree.fromstring(newcode) - isok = True - except Exception, err: - msg = "Failed to change problem: XML error \"%s\"" % err - - if isok: - filename = instance.lcp.fileobject.name - fp = open(filename, 'w') # TODO - replace with filestore call? - fp.write(newcode) - fp.close() - msg = "Problem changed! (%s)" % filename - instance, pxmls = get_lcp(coursename, id) - - lcp = instance.lcp - - # get the rendered problem HTML - phtml = instance.get_html() - # phtml = instance.get_problem_html() - - context = {'id': id, - 'msg': msg, - 'lcp': lcp, - 'filename': lcp.fileobject.name, - 'pxmls': pxmls, - 'phtml': phtml, - "destroy_js": '', - 'init_js': '', - 'csrf': csrf(request)['csrf_token'], - } - - result = render_to_response(qetemplate, context) - return result - - -def quickedit_git_reload(request): - ''' - reload course.xml and all courseware files for this course, from the git repo. - assumes the git repo has already been setup. - staff only. - ''' - if not request.user.is_staff: - return redirect('/') - - # get coursename if stored - coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - msg = "" - if 'cancel' in request.POST: - return redirect("/courseware") - - if 'gitupdate' in request.POST: - import os # FIXME - put at top? - #cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/','')) - cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp, xp.replace('/', '')) - msg += '

cmd: %s

' % cmd - ret = os.popen(cmd).read() - msg += '

%s

' % ret.replace('<', '<') - msg += "

git update done!

" - - context = {'id': id, - 'msg': msg, - 'coursename': coursename, - 'csrf': csrf(request)['csrf_token'], - } - - result = render_to_response("gitupdate.html", context) - return result diff --git a/lms/templates/dogfood.html b/lms/templates/dogfood.html deleted file mode 100644 index 8460454f81..0000000000 --- a/lms/templates/dogfood.html +++ /dev/null @@ -1,144 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - -## ----------------------------------------------------------------------------- -## Template for lib.dogfood.views.dj_capa_problem -## -## Used for viewing assesment problems in "dogfood" self-evaluation mode -## ----------------------------------------------------------------------------- - - -## -## - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## <%static:css group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## -% endif - - - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - - -<%include file="mathjax_include.html" /> - - - - - - -
- -## ----------------------------------------------------------------------------- -## information - -##
-##

Rendition of your problem code

-##
- -## ----------------------------------------------------------------------------- -## rendered problem display - - - - - - - -
-
- ${phtml} -
-
- - - - - -## - - - -## image input: for clicking on images (see imageinput.html) - - - - - <%block name="js_extra"/> - - - diff --git a/lms/templates/gitupdate.html b/lms/templates/gitupdate.html deleted file mode 100644 index a0cedabeae..0000000000 --- a/lms/templates/gitupdate.html +++ /dev/null @@ -1,32 +0,0 @@ - - -edX gitupdate - - - -
-

edX gitupdate

-
- -

Coursename: ${coursename}

- -% if msg: - - ${msg} - -% else: -

-Do you REALLY want to overwrite all the course.xml + problems + html -files with version from the main git repository? -

- -
- -## - -
-% endif - -

Return to site

- - diff --git a/lms/templates/quickedit.html b/lms/templates/quickedit.html deleted file mode 100644 index bc8e74eb65..0000000000 --- a/lms/templates/quickedit.html +++ /dev/null @@ -1,180 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - -## ----------------------------------------------------------------------------- -## Template for courseware.views.quickedit -## -## Used for quick-edit link present when viewing capa-format assesment problems. -## ----------------------------------------------------------------------------- - - -## -## - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:css group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## -% endif - - - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - -## - - - -<%block name="headextra"/> - - - <%include file="mathjax_include.html" /> - - - - - - - -## ----------------------------------------------------------------------------- -## information and i4x PSL code - -
-

QuickEdit

-
-
    -
  • File = ${filename}
  • -
  • ID = ${id}
  • -
- -
- -
- - - -
- -${msg|n} - -## ----------------------------------------------------------------------------- -## rendered problem display - - - -
- - - - - - - -
-
-
- ${phtml} -
-
-
- - - - - -## - - - - - - - - <%block name="js_extra"/> - - - diff --git a/lms/urls.py b/lms/urls.py index b25c4d259e..a203d468e7 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.static_tab', name="static_tab"), ) -if settings.QUICKEDIT: - urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'dogfood.views.quickedit'),) - urlpatterns += (url(r'^dogfood/(?P[^/]*)$', 'dogfood.views.df_capa_problem'),) - if settings.ENABLE_JASMINE: urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) From 7c7e09500ac0d24fefd7fe74aa5190c66dd38deb Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 30 Jan 2013 11:47:17 +0200 Subject: [PATCH 04/29] Renamed videox to videoalpha, as per Piotr naming scheme request. --- common/lib/xmodule/setup.py | 1 + .../xmodule/css/videoalpha/display.scss | 559 ++++++++++++++++++ .../xmodule/js/src/videoalpha/display.coffee | 63 ++ .../js/src/videoalpha/display/_subview.coffee | 14 + .../videoalpha/display/video_caption.coffee | 152 +++++ .../videoalpha/display/video_control.coffee | 35 ++ .../videoalpha/display/video_player.coffee | 180 ++++++ .../display/video_progress_slider.coffee | 49 ++ .../display/video_quality_control.coffee | 26 + .../display/video_speed_control.coffee | 43 ++ .../display/video_volume_control.coffee | 40 ++ .../xmodule/templates/videoalpha/default.yaml | 7 + .../lib/xmodule/xmodule/videoalpha_module.py | 150 +++++ lms/templates/videoalpha.html | 31 + 14 files changed, 1350 insertions(+) create mode 100644 common/lib/xmodule/xmodule/css/videoalpha/display.scss create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee create mode 100644 common/lib/xmodule/xmodule/templates/videoalpha/default.yaml create mode 100644 common/lib/xmodule/xmodule/videoalpha_module.py create mode 100644 lms/templates/videoalpha.html diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 0a9c05f3ec..6b7114c439 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -37,6 +37,7 @@ setup( "timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss new file mode 100644 index 0000000000..bf575e74a3 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss @@ -0,0 +1,559 @@ +& { + margin-bottom: 30px; +} + +div.video { + @include clearfix(); + background: #f3f3f3; + display: block; + margin: 0 -12px; + padding: 12px; + border-radius: 5px; + + article.video-wrapper { + float: left; + margin-right: flex-gutter(9); + width: flex-grid(6, 9); + + section.video-player { + height: 0; + overflow: hidden; + padding-bottom: 56.25%; + position: relative; + + object, iframe { + border: none; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + section.video-controls { + @include clearfix(); + background: #333; + border: 1px solid #000; + border-top: 0; + color: #ccc; + position: relative; + + &:hover { + ul, div { + opacity: 1; + } + } + + div.slider { + @include clearfix(); + background: #c2c2c2; + border: 1px solid #000; + @include border-radius(0); + border-top: 1px solid #000; + @include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555); + height: 7px; + margin-left: -1px; + margin-right: -1px; + @include transition(height 2.0s ease-in-out); + + div.ui-widget-header { + background: #777; + @include box-shadow(inset 0 1px 0 #999); + } + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + margin-left: -7px; + top: -4px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + + &:focus, &:hover { + background-color: lighten($pink, 10%); + outline: none; + } + } + } + + ul.vcr { + @extend .dullify; + float: left; + list-style: none; + margin: 0 lh() 0 0; + padding: 0; + + li { + float: left; + margin-bottom: 0; + + a { + border-bottom: none; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555); + cursor: pointer; + display: block; + line-height: 46px; + padding: 0 lh(.75); + text-indent: -9999px; + @include transition(background-color, opacity); + width: 14px; + background: url('../images/vcr.png') 15px 15px no-repeat; + outline: 0; + + &:focus { + outline: 0; + } + + &:empty { + height: 46px; + background: url('../images/vcr.png') 15px 15px no-repeat; + } + + &.play { + background-position: 17px -114px; + + &:hover { + background-color: #444; + } + } + + &.pause { + background-position: 16px -50px; + + &:hover { + background-color: #444; + } + } + } + + div.vidtime { + padding-left: lh(.75); + font-weight: bold; + line-height: 46px; //height of play pause buttons + padding-left: lh(.75); + -webkit-font-smoothing: antialiased; + } + } + } + + div.secondary-controls { + @extend .dullify; + float: right; + + div.speeds { + float: left; + position: relative; + + &.open { + &>a { + background: url('../images/open-arrow.png') 10px center no-repeat; + } + + ol.video_speeds { + display: block; + opacity: 1; + padding: 0; + margin: 0; + list-style: none; + } + } + + &>a { + background: url('../images/closed-arrow.png') 10px center no-repeat; + border-left: 1px solid #000; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + line-height: 46px; //height of play pause buttons + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 116px; + outline: 0; + + &:focus { + outline: 0; + } + + h3 { + color: #999; + float: left; + font-size: em(14); + font-weight: normal; + letter-spacing: 1px; + padding: 0 lh(.25) 0 lh(.5); + line-height: 46px; + text-transform: uppercase; + } + + p.active { + float: left; + font-weight: bold; + margin-bottom: 0; + padding: 0 lh(.5) 0 0; + line-height: 46px; + color: #fff; + } + + &:hover, &:active, &:focus { + opacity: 1; + background-color: #444; + } + } + + // 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: 133px; + 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; + display: block; + padding: lh(.5); + + &:hover { + background-color: #666; + color: #aaa; + } + } + + &.active { + font-weight: bold; + } + + &:last-child { + @include box-shadow(none); + border-bottom: 0; + margin-top: 0; + } + } + } + } + + div.volume { + float: left; + position: relative; + + &.open { + .volume-slider-container { + display: block; + opacity: 1; + } + } + + &.muted { + &>a { + background: url('../images/mute.png') 10px center no-repeat; + } + } + + > a { + background: url('../images/volume.png') 10px center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + height: 46px; + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover, &:active, &:focus { + background-color: #444; + } + } + + .volume-slider-container { + @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: 45px; + height: 125px; + margin-left: -1px; + z-index: 10; + + .volume-slider { + height: 100px; + border: 0; + width: 5px; + margin: 14px auto; + background: #666; + border: 1px solid #000; + @include box-shadow(0 1px 0 #333); + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + left: -6px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + } + + .ui-slider-range { + background: #ddd; + } + } + } + } + + a.add-fullscreen { + background: url(../images/fullscreen.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + } + + a.quality_control { + background: url(../images/hd.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.active { + background-color: #F44; + color: #0ff; + text-decoration: none; + } + } + + + a.hide-subtitles { + background: url('../images/cc.png') center no-repeat; + color: #797979; + display: block; + float: left; + font-weight: 800; + line-height: 46px; //height of play pause buttons + margin-left: 0; + opacity: 1; + padding: 0 lh(.5); + position: relative; + text-indent: -9999px; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.off { + opacity: .7; + } + } + } + } + + &:hover section.video-controls { + ul, div { + opacity: 1; + } + + div.slider { + height: 14px; + margin-top: -7px; + + a.ui-slider-handle { + @include border-radius(20px); + height: 20px; + margin-left: -10px; + top: -4px; + width: 20px; + } + } + } + } + + ol.subtitles { + padding-left: 0; + float: left; + max-height: 460px; + overflow: auto; + width: flex-grid(3, 9); + margin: 0; + font-size: 14px; + list-style: none; + + li { + border: 0; + color: #666; + cursor: pointer; + margin-bottom: 8px; + padding: 0; + line-height: lh(); + + &.current { + color: #333; + font-weight: 700; + } + + &:hover { + color: $blue; + } + + &:empty { + margin-bottom: 0px; + } + } + } + + &.closed { + @extend .trans; + + article.video-wrapper { + width: flex-grid(9,9); + } + + ol.subtitles { + width: 0; + height: 0; + } + } + + &.fullscreen { + background: rgba(#000, .95); + border: 0; + bottom: 0; + height: 100%; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 999; + vertical-align: middle; + + &.closed { + ol.subtitles { + right: -(flex-grid(4)); + width: auto; + } + } + + div.tc-wrapper { + @include clearfix; + display: table; + width: 100%; + height: 100%; + + article.video-wrapper { + width: 100%; + display: table-cell; + vertical-align: middle; + float: none; + } + + object, iframe { + bottom: 0; + height: 100%; + left: 0; + overflow: hidden; + position: fixed; + top: 0; + } + + section.video-controls { + bottom: 0; + left: 0; + position: absolute; + width: 100%; + z-index: 9999; + } + } + + ol.subtitles { + background: rgba(#000, .8); + bottom: 0; + height: 100%; + max-height: 100%; + max-width: flex-grid(3); + padding: lh(); + position: fixed; + right: 0; + top: 0; + @include transition(); + + li { + color: #aaa; + + &.current { + color: #fff; + } + } + } + } +} diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee new file mode 100644 index 0000000000..1876330340 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -0,0 +1,63 @@ +class @Video + constructor: (element) -> + @el = $(element).find('.video') + @id = @el.attr('id').replace(/video_/, '') + @start = @el.data('start') + @end = @el.data('end') + @caption_data_dir = @el.data('caption-data-dir') + @caption_asset_path = @el.data('caption-asset-path') + @show_captions = @el.data('show-captions') == "true" + window.player = null + @el = $("#video_#{@id}") + @parseVideos @el.data('streams') + @fetchMetadata() + @parseSpeed() + $("#video_#{@id}").data('video', this).addClass('video-load-complete') + + @hide_captions = $.cookie('hide_captions') == 'true' + + if YT.Player + @embed() + else + window.onYouTubePlayerAPIReady = => + @el.each -> + $(this).data('video').embed() + + youtubeId: (speed)-> + @videos[speed || @speed] + + parseVideos: (videos) -> + @videos = {} + $.each videos.split(/,/), (index, video) => + video = video.split(/:/) + speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' + @videos[speed] = video[1] + + 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 video: this + + fetchMetadata: (url) -> + @metadata = {} + $.each @videos, (speed, url) => + $.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp' + + getDuration: -> + @metadata[@youtubeId()].duration + + log: (eventName) -> + Logger.log eventName, + id: @id + code: @youtubeId() + currentTime: @player.currentTime + speed: @speed diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee new file mode 100644 index 0000000000..2e14289843 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee @@ -0,0 +1,14 @@ +class @Subview + constructor: (options) -> + $.each options, (key, value) => + @[key] = value + @initialize() + @render() + @bind() + + $: (selector) -> + $(selector, @el) + + initialize: -> + render: -> + bind: -> diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee new file mode 100644 index 0000000000..e840cd2a77 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee @@ -0,0 +1,152 @@ +class @VideoCaption extends Subview + initialize: -> + @loaded = false + + bind: -> + $(window).bind('resize', @resize) + @$('.hide-subtitles').click @toggle + @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) + .mousemove(@onMovement).bind('mousewheel', @onMovement) + .bind('DOMMouseScroll', @onMovement) + + captionURL: -> + "#{@captionAssetPath}#{@youtubeId}.srt.sjson" + + render: -> + # TODO: make it so you can have a video with no captions. + #@$('.video-wrapper').after """ + #
  1. Attempting to load captions...
+ # """ + @$('.video-wrapper').after """ +
    + """ + @$('.video-controls .secondary-controls').append """ + Captions + """#" + @$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5 + @fetchCaption() + + fetchCaption: -> + $.getWithPrefix @captionURL(), (captions) => + @captions = captions.text + @start = captions.start + + @loaded = true + + if onTouchBasedDevice() + $('.subtitles li').html "Caption will be displayed when you start playing the video." + else + @renderCaption() + + renderCaption: -> + container = $('
      ') + + $.each @captions, (index, text) => + container.append $('
    1. ').html(text).attr + 'data-index': index + 'data-start': @start[index] + + @$('.subtitles').html(container.html()) + @$('.subtitles li[data-index]').click @seekPlayer + + # prepend and append an empty
    2. for cosmetic reason + @$('.subtitles').prepend($('
    3. ').height(@topSpacingHeight())) + .append($('
    4. ').height(@bottomSpacingHeight())) + + @rendered = true + + search: (time) -> + if @loaded + min = 0 + max = @start.length - 1 + + while min < max + index = Math.ceil((max + min) / 2) + if time < @start[index] + max = index - 1 + if time >= @start[index] + min = index + return min + + play: -> + if @loaded + @renderCaption() unless @rendered + @playing = true + + pause: -> + if @loaded + @playing = false + + updatePlayTime: (time) -> + if @loaded + # This 250ms offset is required to match the video speed + time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250) + newIndex = @search time + + if newIndex != undefined && @currentIndex != newIndex + if @currentIndex + @$(".subtitles li.current").removeClass('current') + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + + @currentIndex = newIndex + @scrollCaption() + + resize: => + @$('.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: => + @onMouseEnter() + + onMouseLeave: => + clearTimeout @frozen if @frozen + @frozen = null + @scrollCaption() if @playing + + 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', @currentSpeed) / 1000) + $(@).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 @el.hasClass('closed') # Captions are "closed" e.g. turned off + @hideCaptions(false) + else # Captions are on + @hideCaptions(true) + + hideCaptions: (hide_captions) => + if hide_captions + @$('.hide-subtitles').attr('title', 'Turn on captions') + @el.addClass('closed') + else + @$('.hide-subtitles').attr('title', 'Turn off captions') + @el.removeClass('closed') + @scrollCaption() + $.cookie('hide_captions', hide_captions, expires: 3650, path: '/') + + captionHeight: -> + if @el.hasClass('fullscreen') + $(window).height() - @$('.video-controls').height() + else + @$('.video-wrapper').height() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee new file mode 100644 index 0000000000..856549c3e2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee @@ -0,0 +1,35 @@ +class @VideoControl extends Subview + bind: -> + @$('.video_control').click @togglePlayback + + render: -> + @el.append """ +
      +
      +
        +
      • +
      • +
        0:00 / 0:00
        +
      • +
      + +
      + """#" + + unless onTouchBasedDevice() + @$('.video_control').addClass('play').html('Play') + + play: -> + @$('.video_control').removeClass('play').addClass('pause').html('Pause') + + pause: -> + @$('.video_control').removeClass('pause').addClass('play').html('Play') + + togglePlayback: (event) => + event.preventDefault() + if @$('.video_control').hasClass('play') + $(@).trigger('play') + else if @$('.video_control').hasClass('pause') + $(@).trigger('pause') diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee new file mode 100644 index 0000000000..22308a5568 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -0,0 +1,180 @@ +class @VideoPlayer extends Subview + initialize: -> + # Define a missing constant of Youtube API + YT.PlayerState.UNSTARTED = -1 + + @currentTime = 0 + @el = $("#video_#{@video.id}") + + bind: -> + $(@control).bind('play', @play) + .bind('pause', @pause) + $(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange) + $(@caption).bind('seek', @onSeek) + $(@speedControl).bind('speedChange', @onSpeedChange) + $(@progressSlider).bind('seek', @onSeek) + if @volumeControl + $(@volumeControl).bind('volumeChange', @onVolumeChange) + $(document).keyup @bindExitFullScreen + + @$('.add-fullscreen').click @toggleFullScreen + @addToolTip() unless onTouchBasedDevice() + + bindExitFullScreen: (event) => + if @el.hasClass('fullscreen') && event.keyCode == 27 + @toggleFullScreen(event) + + render: -> + @control = new VideoControl el: @$('.video-controls') + @qualityControl = new VideoQualityControl el: @$('.secondary-controls') + @caption = new VideoCaption + el: @el + youtubeId: @video.youtubeId('1.0') + currentSpeed: @currentSpeed() + captionAssetPath: @video.caption_asset_path + unless onTouchBasedDevice() + @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') + @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() + @progressSlider = new VideoProgressSlider el: @$('.slider') + @playerVars = + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + modestbranding: 1 + if @video.start + @playerVars.start = @video.start + @playerVars.wmode = 'window' + if @video.end + # work in AS3, not HMLT5. but iframe use AS3 + @playerVars.end = @video.end + + @player = new YT.Player @video.id, + playerVars: @playerVars + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange + @caption.hideCaptions(@['video'].hide_captions) + + addToolTip: -> + @$('.add-fullscreen, .hide-subtitles').qtip + position: + my: 'top right' + at: 'top center' + + onReady: (event) => + unless onTouchBasedDevice() + $('.video-load-complete:first').data('video').player.play() + + onStateChange: (event) => + switch event.data + when YT.PlayerState.UNSTARTED + @onUnstarted() + when YT.PlayerState.PLAYING + @onPlay() + when YT.PlayerState.PAUSED + @onPause() + when YT.PlayerState.ENDED + @onEnded() + + onPlaybackQualityChange: (event, value) => + quality = @player.getPlaybackQuality() + @qualityControl.onQualityChange(quality) + + handlePlaybackQualityChange: (event, value) => + @player.setPlaybackQuality(value) + + onUnstarted: => + @control.pause() + @caption.pause() + + onPlay: => + @video.log 'play_video' + window.player.pauseVideo() if window.player && window.player != @player + window.player = @player + unless @player.interval + @player.interval = setInterval(@update, 200) + @caption.play() + @control.play() + @progressSlider.play() + + onPause: => + @video.log 'pause_video' + window.player = null if window.player == @player + clearInterval(@player.interval) + @player.interval = null + @caption.pause() + @control.pause() + + onEnded: => + @control.pause() + @caption.pause() + + onSeek: (event, time) => + @player.seekTo(time, true) + if @isPlaying() + clearInterval(@player.interval) + @player.interval = setInterval(@update, 200) + else + @currentTime = time + @updatePlayTime time + + onSpeedChange: (event, newSpeed) => + @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) + newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' + @video.setSpeed(newSpeed) + @caption.currentSpeed = newSpeed + + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) + @updatePlayTime @currentTime + + onVolumeChange: (event, volume) => + @player.setVolume volume + + update: => + if @currentTime = @player.getCurrentTime() + @updatePlayTime @currentTime + + updatePlayTime: (time) -> + progress = Time.format(time) + ' / ' + Time.format(@duration()) + @$(".vidtime").html(progress) + @caption.updatePlayTime(time) + @progressSlider.updatePlayTime(time, @duration()) + + toggleFullScreen: (event) => + event.preventDefault() + if @el.hasClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Fill browser') + @el.removeClass('fullscreen') + else + @el.addClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Exit fill browser') + @caption.resize() + + # Delegates + play: => + @player.playVideo() if @player.playVideo + + isPlaying: -> + @player.getPlayerState() == YT.PlayerState.PLAYING + + pause: => + @player.pauseVideo() if @player.pauseVideo + + duration: -> + @video.getDuration() + + currentSpeed: -> + @video.speed + + volume: (value) -> + if value? + @player.setVolume value + else + @player.getVolume() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee new file mode 100644 index 0000000000..874756cb71 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee @@ -0,0 +1,49 @@ +class @VideoProgressSlider extends Subview + initialize: -> + @buildSlider() unless onTouchBasedDevice() + + buildSlider: -> + @slider = @el.slider + range: 'min' + change: @onChange + slide: @onSlide + stop: @onStop + @buildHandle() + + buildHandle: -> + @handle = @$('.slider .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 + + play: => + @buildSlider() unless @slider + + updatePlayTime: (currentTime, duration) -> + if @slider && !@frozen + @slider.slider('option', 'max', duration) + @slider.slider('value', currentTime) + + onSlide: (event, ui) => + @frozen = true + @updateTooltip(ui.value) + $(@).trigger('seek', ui.value) + + onChange: (event, ui) => + @updateTooltip(ui.value) + + onStop: (event, ui) => + @frozen = true + $(@).trigger('seek', ui.value) + setTimeout (=> @frozen = false), 200 + + updateTooltip: (value)-> + @handle.qtip('option', 'content.text', "#{Time.format(value)}") diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee new file mode 100644 index 0000000000..f8f6167075 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee @@ -0,0 +1,26 @@ +class @VideoQualityControl extends Subview + initialize: -> + @quality = null; + + bind: -> + @$('.quality_control').click @toggleQuality + + render: -> + @el.append """ + HD + """#" + + onQualityChange: (value) -> + @quality = value + if @quality in ['hd720', 'hd1080', 'highres'] + @el.addClass('active') + else + @el.removeClass('active') + + toggleQuality: (event) => + event.preventDefault() + if @quality in ['hd720', 'hd1080', 'highres'] + newQuality = 'large' + else + newQuality = 'hd720' + $(@).trigger('changeQuality', newQuality) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee new file mode 100644 index 0000000000..1d0d8b7d44 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee @@ -0,0 +1,43 @@ +class @VideoSpeedControl extends Subview + bind: -> + @$('.video_speeds a').click @changeVideoSpeed + if onTouchBasedDevice() + @$('.speeds').click (event) -> + event.preventDefault() + $(this).toggleClass('open') + else + @$('.speeds').mouseenter -> + $(this).addClass('open') + @$('.speeds').mouseleave -> + $(this).removeClass('open') + @$('.speeds').click (event) -> + event.preventDefault() + $(this).removeClass('open') + + render: -> + @el.prepend """ + + """ + + $.each @speeds, (index, speed) => + link = $('').attr(href: "#").html("#{speed}x") + @$('.video_speeds').prepend($('
    5. ').attr('data-speed', speed).html(link)) + @setSpeed(@currentSpeed) + + changeVideoSpeed: (event) => + event.preventDefault() + unless $(event.target).parent().hasClass('active') + @currentSpeed = $(event.target).parent().data('speed') + $(@).trigger 'speedChange', $(event.target).parent().data('speed') + @setSpeed(parseFloat(@currentSpeed).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") diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee new file mode 100644 index 0000000000..096b50042d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee @@ -0,0 +1,40 @@ +class @VideoVolumeControl extends Subview + initialize: -> + @currentVolume = 100 + + bind: -> + @$('.volume').mouseenter -> + $(this).addClass('open') + @$('.volume').mouseleave -> + $(this).removeClass('open') + @$('.volume>a').click(@toggleMute) + + render: -> + @el.prepend """ +
      + +
      +
      +
      +
      + """#" + @slider = @$('.volume-slider').slider + orientation: "vertical" + range: "min" + min: 0 + max: 100 + value: 100 + change: @onChange + slide: @onChange + + onChange: (event, ui) => + @currentVolume = ui.value + $(@).trigger 'volumeChange', @currentVolume + @$('.volume').toggleClass 'muted', @currentVolume == 0 + + toggleMute: => + if @currentVolume > 0 + @previousVolume = @currentVolume + @slider.slider 'option', 'value', 0 + else + @slider.slider 'option', 'value', @previousVolume diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml new file mode 100644 index 0000000000..69ed22cc1e --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -0,0 +1,7 @@ +--- +metadata: + display_name: default + data_dir: a_made_up_name +data: | + +children: [] diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py new file mode 100644 index 0000000000..e41f9783e4 --- /dev/null +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -0,0 +1,150 @@ +import json +import logging + +from lxml import etree +from pkg_resources import resource_string, resource_listdir + +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 + +import datetime +import time + +import datetime +import time + +log = logging.getLogger(__name__) + + +class VideoModule(XModule): + video_time = 0 + icon_class = '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 = "Video" + + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + xmltree = etree.fromstring(self.definition['data']) + self.youtube = xmltree.get('youtube') + self.position = 0 + self.show_captions = xmltree.get('show_captions', 'true') + self.source = self._get_source(xmltree) + self.track = self._get_track(xmltree) + self.start_time, self.end_time = self._get_timeframe(xmltree) + + if instance_state is not None: + state = json.loads(instance_state) + if 'position' in state: + self.position = int(float(state['position'])) + + def _get_source(self, xmltree): + # find the first valid source + return self._get_first_external(xmltree, 'source') + + def _get_track(self, xmltree): + # find the first valid track + return self._get_first_external(xmltree, 'track') + + def _get_first_external(self, xmltree, tag): + """ + Will return the first valid element + of the given tag. + 'valid' means has a non-empty 'src' attribute + """ + result = None + for element in xmltree.findall(tag): + src = element.get('src') + if src: + result = src + break + return result + + def _get_timeframe(self, xmltree): + """ Converts 'from' and 'to' 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('from')), parse_time(xmltree.get('to')) + + 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_progress(self): + ''' TODO (vshnayder): Get and save duration of youtube video, then return + fraction watched. + (Be careful to notice when video link changes and update) + + For now, we have no way of knowing if the video has even been watched, so + just return None. + ''' + return None + + def get_instance_state(self): + #log.debug(u"STATE POSITION {0}".format(self.position)) + return json.dumps({'position': self.position}) + + def videoalpha_list(self): + return self.youtube + + 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/{0}/subs/".format(self.metadata['data_dir']) + + return self.system.render_template('videoalpha.html', { + 'streams': self.videoalpha_list(), + 'id': self.location.html_id(), + 'position': self.position, + 'source': self.source, + 'track': self.track, + 'display_name': self.display_name, + # TODO (cpennington): This won't work when we move to data that isn't on the filesystem + 'data_dir': self.metadata['data_dir'], + 'caption_asset_path': caption_asset_path, + 'show_captions': self.show_captions, + 'start': self.start_time, + 'end': self.end_time + }) + + +class VideoAlphaDescriptor(RawDescriptor): + module_class = VideoModule + stores_state = True + template_dir_name = "videoalpha" diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html new file mode 100644 index 0000000000..6cee9ed39b --- /dev/null +++ b/lms/templates/videoalpha.html @@ -0,0 +1,31 @@ +% if display_name is not UNDEFINED and display_name is not None: +

      ${display_name}

      +% endif + + +%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: +
      +%else: +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +%endif + +% if source: +
      +

      Download video here.

      +
      +% endif + +% if track: +
      +

      Download subtitles here.

      +
      +% endif From ca83c3953a18cdd7a3d91fbe2138633d595649c2 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 30 Jan 2013 18:06:27 +0200 Subject: [PATCH 05/29] Adding HTML5Video class and modifying coffee sources to use it when video sources are provided instead of YouTube IDs. --- common/lib/xmodule/xmodule/js/src/.gitignore | 3 +- .../xmodule/js/src/videoalpha/display.coffee | 80 +++++-- .../js/src/videoalpha/display/html5_video.js | 196 ++++++++++++++++++ .../videoalpha/display/video_player.coffee | 32 ++- .../lib/xmodule/xmodule/videoalpha_module.py | 10 +- lms/templates/videoalpha.html | 15 +- 6 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index 03534687ca..bbd93c90e3 100644 --- a/common/lib/xmodule/xmodule/js/src/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/.gitignore @@ -1,2 +1 @@ -*.js - +# Please do not ignore *.js files. Some xmodules are written in JS. diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 1876330340..b97b02e68c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -1,4 +1,4 @@ -class @Video +class @VideoAlpha constructor: (element) -> @el = $(element).find('.video') @id = @el.attr('id').replace(/video_/, '') @@ -9,40 +9,77 @@ class @Video @show_captions = @el.data('show-captions') == "true" window.player = null @el = $("#video_#{@id}") - @parseVideos @el.data('streams') - @fetchMetadata() - @parseSpeed() + + if @parseVideos(@el.data("streams")) is true + @videoType = "youtube" + @fetchMetadata() + @parseSpeed() + else + @videoType = "html5" + @parseVideoSources @el.data("mp4-source"), @el.data("webm-source"), @el.data("ogg-source") + @speeds = ["0.75", "1.0", "1.25", "1.5"] + @setSpeed($.cookie('video_speed')) + $("#video_#{@id}").data('video', this).addClass('video-load-complete') @hide_captions = $.cookie('hide_captions') == 'true' - if YT.Player + if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) + console.log 'one' @embed() else - window.onYouTubePlayerAPIReady = => - @el.each -> - $(this).data('video').embed() + console.log 'two' + if @videoType is "youtube" + console.log 'three' + window.onYouTubePlayerAPIReady = -> + _this.embed() + else if @videoType is "html5" + console.log 'four' + console.log @videoType + console.log HTML5Video.Player + window.onHTML5PlayerAPIReady = -> + _this.embed() youtubeId: (speed)-> @videos[speed || @speed] - parseVideos: (videos) -> + VideoAlpha::parseVideos = (videos) -> + return false if (typeof videos isnt "string") or (videos.length is 0) + + console.log 'We got this far' + console.log videos + @videos = {} - $.each videos.split(/,/), (index, video) => + _this = this + $.each videos.split(/,/), (index, video) -> + speed = undefined video = video.split(/:/) - speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' - @videos[speed] = video[1] + speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0") + _this.videos[speed] = video[1] + true + + VideoAlpha::parseVideoSources = (mp4Source, webmSource, oggSource) -> + @html5Sources = + mp4: null + webm: null + ogg: null + + @html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0) + @html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0) + @html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0) parseSpeed: -> - @setSpeed($.cookie('video_speed')) @speeds = ($.map @videos, (url, speed) -> speed).sort() + @setSpeed($.cookie('video_speed')) - setSpeed: (newSpeed) -> - if @videos[newSpeed] != undefined + VideoAlpha::setSpeed = (newSpeed) -> + if @speeds.indexOf(newSpeed) isnt -1 @speed = newSpeed - $.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/') + $.cookie "video_speed", "" + newSpeed, + expires: 3650 + path: "/" else - @speed = '1.0' + @speed = "1.0" embed: -> @player = new VideoPlayer video: this @@ -55,9 +92,14 @@ class @Video getDuration: -> @metadata[@youtubeId()].duration - log: (eventName) -> - Logger.log eventName, + VideoAlpha::log = (eventName) -> + logInfo = id: @id code: @youtubeId() currentTime: @player.currentTime speed: @speed + + if @videoType is "youtube" + logInfo.code = @youtubeId() + else logInfo.code = "html5" if @videoType is "html5" + Logger.log eventName, logInfo diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js new file mode 100644 index 0000000000..3db8bb97ed --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -0,0 +1,196 @@ +console.log('We are in "html5_video.js" script.'); + +this.HTML5Video = (function () { + var HTML5Video = {}; + + HTML5Video.Player = (function () { + + /* + * Constructor function for HTML5 Video player. + * + * @el - A DOM element where the HTML5 player will be inserted (as returned by jQuery(selector) function), + * or a selector string which will be used to select an element. This is a required parameter. + * + * @config - An object whose properties will be used as configuration options for the HTML5 video + * player. This is an optional parameter. In the case if this parameter is missing, or some of the config + * object's properties are missing, defaults will be used. The available options (and their defaults) are as + * follows: + * + * config = { + * 'width': 640, + * + * 'height': 390, + * + * 'videoSources': null, // An object of with properties being video sources. The property name is the + * // video format of the source. Supported video formats are: 'mp4', 'webm', and + * // 'ogg'. By default videoSources property is null. This means that the + * // player will initialize, and not play anything. If you do not provide a + * // 'videoSource' option, you can later call loadVideoBySource() method to load + * // a video and start playing it. + * + * 'playerVars': { // Object's properties identify player parameters. + * + * 'controls': 1, // Possible values: 0, or 1. Value of 1 will enable the default browser video + * // controls. + * + * 'start': null, // Possible values: positive integer. Position from which to start playing the + * // video. Measured in seconds. If value is null, or 'start' property is not + * // specified, the video will start playing from the beginning. + * + * 'end': null // Possible values: positive integer. Position when to stop playing the + * // video. Measured in seconds. If value is null, or 'end' property is not + * // specified, the video will end playing at the end. + * + * }, + * + * 'events': { // Object's properties identify the events that the API fires, and the + * // functions (event listeners) that the API will call when those events occur. + * // If value is null, or property is not specified, then no callback will be + * // called for that event. + * + * 'onReady': null, + * 'onStateChange': null, + * 'onPlaybackQualityChange': null + * } + * } + */ + function Player(el, config) { + console.log('We are inside HTML5Video.Player constructor.'); + + if (typeof el === 'string') { + this.el = $(el); + } else if ($.isPlainObject(el) === true) { + this.el = el; + } else { + // Error. el parameter is required. + + // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype + // is called after we return. + + return; + } + + console.log('We got a proper DOM element.'); + + if ($.isPlainObject(config) === true) { + this.config = config; + } else { + this.config = { + 'width': 640, + 'height': 390, + 'videoSource': '', + 'playerVars': { + 'controls': 1, + 'start': null, + 'end': null + }, + 'events': { + 'onReady': null, + 'onStateChange': null, + 'onPlaybackQualityChange': null + } + }; + } + + console.log('The config is:'); + console.log(this.config); + } + + /* + * This function returns the quality of the video. Possible return values are (type String) + * + * highres + * hd1080 + * hd720 + * large + * medium + * small + * + * It returns undefined if there is no current video. + * + * If there is a current video, but it is impossible to determine it's quality, the function will return + * 'medium'. + */ + Player.prototype.getPlayBackQuality = function () { + if (this.config.videoSource === '') { + return undefined; + } + + // TODO: Figure out if we can get the quality of a video from a source (when it is loaded by the browser). + + return 'medium'; + }; + + /* + * The original YouTube API function player.setPlayBackQuality changed (if it was possible) the quality of the + * played video. In our case, this function will not do anything because we can't change the quality of HTML5 + * video since we only get one source of video with one quality. + */ + Player.prototype.setPlayBackQuality = function (value) { + + }; + + Player.prototype.pauseVideo = function () { + + }; + + Player.prototype.sekkTo = function () { + + }; + + // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this + // function accordingly. + Player.prototype.loadVideoBySource = function () { + + }; + + // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this + // function accordingly. + Player.prototype.cueVideoBySource = function () { + + }; + + Player.prototype.setVolume = function () { + + }; + + Player.prototype.getCurrentTime = function () { + + }; + + Player.prototype.playVideo = function () { + + }; + + Player.prototype.getPlayerState = function () { + + }; + + Player.prototype.pauseVideo = function () { + + }; + + Player.prototype.setVolume = function () { + + }; + + Player.prototype.getVolume = function () { + + }; + + return Player; + }()); + + HTML5Video.PlayerState = { + 'UNSTARTED': -1, + 'ENDED': 0, + 'PLAYING': 1, + 'PAUSED': 2, + 'BUFFERING': 3, + 'CUED': 5 + }; + + return HTML5Video; +}()); + +console.log(HTML5Video); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 22308a5568..3bee570bc8 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,7 +1,8 @@ class @VideoPlayer extends Subview initialize: -> - # Define a missing constant of Youtube API - YT.PlayerState.UNSTARTED = -1 + if @video.videoType is 'youtube' + # Define a missing constant of Youtube API + YT.PlayerState.UNSTARTED = -1 @currentTime = 0 @el = $("#video_#{@video.id}") @@ -25,6 +26,7 @@ class @VideoPlayer extends Subview @toggleFullScreen(event) render: -> + console.log '1.1' @control = new VideoControl el: @$('.video-controls') @qualityControl = new VideoQualityControl el: @$('.secondary-controls') @caption = new VideoCaption @@ -34,6 +36,7 @@ class @VideoPlayer extends Subview captionAssetPath: @video.caption_asset_path unless onTouchBasedDevice() @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') + console.log '1.2' @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() @progressSlider = new VideoProgressSlider el: @$('.slider') @playerVars = @@ -43,20 +46,31 @@ class @VideoPlayer extends Subview showinfo: 0 enablejsapi: 1 modestbranding: 1 + console.log '1.3' if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' if @video.end # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end + console.log '1.4' - @player = new YT.Player @video.id, - playerVars: @playerVars - videoId: @video.youtubeId() - events: - onReady: @onReady - onStateChange: @onStateChange - onPlaybackQualityChange: @onPlaybackQualityChange + if @video.videoType is 'html5' + @player = new HTML5Video.Player @video.id, + playerVars: @playerVars, + videoSources: @video.html5Sources, + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange + else if @video.videoType is 'youtube' + @player = new YT.Player @video.id, + playerVars: @playerVars + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange @caption.hideCaptions(@['video'].hide_captions) addToolTip: -> diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index e41f9783e4..912505d0a6 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -19,11 +19,13 @@ import time log = logging.getLogger(__name__) -class VideoModule(XModule): +class VideoAlphaModule(XModule): video_time = 0 icon_class = 'video' - js = {'coffee': + 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) @@ -31,7 +33,7 @@ class VideoModule(XModule): 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 = "Video" + js_module_name = "VideoAlpha" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -145,6 +147,6 @@ class VideoModule(XModule): class VideoAlphaDescriptor(RawDescriptor): - module_class = VideoModule + module_class = VideoAlphaModule stores_state = True template_dir_name = "videoalpha" diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 6cee9ed39b..58704ed6b1 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,11 +2,24 @@

      ${display_name}

      % endif + %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      %else: -
      +
      From ed00d20708721c62978622f58169b1654f22fb1b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 10:35:07 +0200 Subject: [PATCH 06/29] Updated all coffee scripts to define and use Alpha version of classes. Added missing functions for HTML5Video.Player prototype. Now bare bones works without errors. YouTube version also works without errors. --- .../xmodule/js/src/videoalpha/display.coffee | 23 +++------- .../js/src/videoalpha/display/_subview.coffee | 2 +- .../js/src/videoalpha/display/html5_video.js | 43 +++++++++++-------- .../videoalpha/display/video_caption.coffee | 2 +- .../videoalpha/display/video_control.coffee | 2 +- .../videoalpha/display/video_player.coffee | 24 +++++------ .../display/video_progress_slider.coffee | 2 +- .../display/video_quality_control.coffee | 2 +- .../display/video_speed_control.coffee | 2 +- .../display/video_volume_control.coffee | 2 +- lms/templates/videoalpha.html | 2 +- 11 files changed, 49 insertions(+), 57 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index b97b02e68c..735e916573 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -9,7 +9,6 @@ class @VideoAlpha @show_captions = @el.data('show-captions') == "true" window.player = null @el = $("#video_#{@id}") - if @parseVideos(@el.data("streams")) is true @videoType = "youtube" @fetchMetadata() @@ -18,25 +17,21 @@ class @VideoAlpha @videoType = "html5" @parseVideoSources @el.data("mp4-source"), @el.data("webm-source"), @el.data("ogg-source") @speeds = ["0.75", "1.0", "1.25", "1.5"] + @videos = + "0.75": "" + "1.0": "" + "1.25": "" + "1.5": "" @setSpeed($.cookie('video_speed')) - $("#video_#{@id}").data('video', this).addClass('video-load-complete') - @hide_captions = $.cookie('hide_captions') == 'true' - if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) - console.log 'one' @embed() else - console.log 'two' if @videoType is "youtube" - console.log 'three' window.onYouTubePlayerAPIReady = -> _this.embed() else if @videoType is "html5" - console.log 'four' - console.log @videoType - console.log HTML5Video.Player window.onHTML5PlayerAPIReady = -> _this.embed() @@ -45,10 +40,6 @@ class @VideoAlpha VideoAlpha::parseVideos = (videos) -> return false if (typeof videos isnt "string") or (videos.length is 0) - - console.log 'We got this far' - console.log videos - @videos = {} _this = this $.each videos.split(/,/), (index, video) -> @@ -63,7 +54,6 @@ class @VideoAlpha mp4: null webm: null ogg: null - @html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0) @html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0) @html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0) @@ -82,7 +72,7 @@ class @VideoAlpha @speed = "1.0" embed: -> - @player = new VideoPlayer video: this + @player = new VideoPlayerAlpha video: this fetchMetadata: (url) -> @metadata = {} @@ -98,7 +88,6 @@ class @VideoAlpha code: @youtubeId() currentTime: @player.currentTime speed: @speed - if @videoType is "youtube" logInfo.code = @youtubeId() else logInfo.code = "html5" if @videoType is "html5" diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee index 2e14289843..6b86296dfa 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee @@ -1,4 +1,4 @@ -class @Subview +class @SubviewAlpha constructor: (options) -> $.each options, (key, value) => @[key] = value diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 3db8bb97ed..c61d725e5c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -1,5 +1,3 @@ -console.log('We are in "html5_video.js" script.'); - this.HTML5Video = (function () { var HTML5Video = {}; @@ -55,8 +53,6 @@ this.HTML5Video = (function () { * } */ function Player(el, config) { - console.log('We are inside HTML5Video.Player constructor.'); - if (typeof el === 'string') { this.el = $(el); } else if ($.isPlainObject(el) === true) { @@ -70,8 +66,6 @@ this.HTML5Video = (function () { return; } - console.log('We got a proper DOM element.'); - if ($.isPlainObject(config) === true) { this.config = config; } else { @@ -91,9 +85,6 @@ this.HTML5Video = (function () { } }; } - - console.log('The config is:'); - console.log(this.config); } /* @@ -122,11 +113,11 @@ this.HTML5Video = (function () { }; /* - * The original YouTube API function player.setPlayBackQuality changed (if it was possible) the quality of the + * The original YouTube API function player.setPlaybackQuality changed (if it was possible) the quality of the * played video. In our case, this function will not do anything because we can't change the quality of HTML5 * video since we only get one source of video with one quality. */ - Player.prototype.setPlayBackQuality = function (value) { + Player.prototype.setPlaybackQuality = function (value) { }; @@ -134,20 +125,30 @@ this.HTML5Video = (function () { }; - Player.prototype.sekkTo = function () { + Player.prototype.seekTo = function () { }; // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this - // function accordingly. - Player.prototype.loadVideoBySource = function () { + // function accordingly. However, not to cause conflicts, there will also be a loadVideoById function which + // will call this function. + Player.prototype.loadVideoBySource = function (source) { }; - // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this - // function accordingly. - Player.prototype.cueVideoBySource = function () { + Player.prototype.loadVideoById = function (id) { + this.loadVideoBySource(id); + } + // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this + // function accordingly. However, not to cause conflicts, there will also be a cueVideoById function which + // will call this function. + Player.prototype.cueVideoBySource = function (source) { + + }; + + Player.prototype.cueVideoById = function (id) { + this.cueVideoBySource(id); }; Player.prototype.setVolume = function () { @@ -178,6 +179,12 @@ this.HTML5Video = (function () { }; + Player.prototype.getDuration = function () { + // TODO: Return valid video duration. + + return 0; + }; + return Player; }()); @@ -192,5 +199,3 @@ this.HTML5Video = (function () { return HTML5Video; }()); - -console.log(HTML5Video); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee index e840cd2a77..9ecdaca474 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee @@ -1,4 +1,4 @@ -class @VideoCaption extends Subview +class @VideoCaptionAlpha extends SubviewAlpha initialize: -> @loaded = false diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee index 856549c3e2..311b69dbbe 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee @@ -1,4 +1,4 @@ -class @VideoControl extends Subview +class @VideoControlAlpha extends SubviewAlpha bind: -> @$('.video_control').click @togglePlayback diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 3bee570bc8..f295096388 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,4 +1,4 @@ -class @VideoPlayer extends Subview +class @VideoPlayerAlpha extends SubviewAlpha initialize: -> if @video.videoType is 'youtube' # Define a missing constant of Youtube API @@ -26,19 +26,17 @@ class @VideoPlayer extends Subview @toggleFullScreen(event) render: -> - console.log '1.1' - @control = new VideoControl el: @$('.video-controls') - @qualityControl = new VideoQualityControl el: @$('.secondary-controls') - @caption = new VideoCaption + @control = new VideoControlAlpha el: @$('.video-controls') + @qualityControl = new VideoQualityControlAlpha el: @$('.secondary-controls') + @caption = new VideoCaptionAlpha el: @el youtubeId: @video.youtubeId('1.0') currentSpeed: @currentSpeed() captionAssetPath: @video.caption_asset_path unless onTouchBasedDevice() - @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') - console.log '1.2' - @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() - @progressSlider = new VideoProgressSlider el: @$('.slider') + @volumeControl = new VideoVolumeControlAlpha el: @$('.secondary-controls') + @speedControl = new VideoSpeedControlAlpha el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() + @progressSlider = new VideoProgressSliderAlpha el: @$('.slider') @playerVars = controls: 0 wmode: 'transparent' @@ -46,15 +44,12 @@ class @VideoPlayer extends Subview showinfo: 0 enablejsapi: 1 modestbranding: 1 - console.log '1.3' if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' if @video.end # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end - console.log '1.4' - if @video.videoType is 'html5' @player = new HTML5Video.Player @video.id, playerVars: @playerVars, @@ -182,7 +177,10 @@ class @VideoPlayer extends Subview @player.pauseVideo() if @player.pauseVideo duration: -> - @video.getDuration() + if @video.videoType is "youtube" + return @video.getDuration() + else return @player.getDuration() if @video.videoType is "html5" + 0 currentSpeed: -> @video.speed diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee index 874756cb71..19eff226fb 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee @@ -1,4 +1,4 @@ -class @VideoProgressSlider extends Subview +class @VideoProgressSliderAlpha extends SubviewAlpha initialize: -> @buildSlider() unless onTouchBasedDevice() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee index f8f6167075..c67969e34e 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee @@ -1,4 +1,4 @@ -class @VideoQualityControl extends Subview +class @VideoQualityControlAlpha extends SubviewAlpha initialize: -> @quality = null; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee index 1d0d8b7d44..7b75baddb8 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee @@ -1,4 +1,4 @@ -class @VideoSpeedControl extends Subview +class @VideoSpeedControlAlpha extends SubviewAlpha bind: -> @$('.video_speeds a').click @changeVideoSpeed if onTouchBasedDevice() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee index 096b50042d..8c17012ca2 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee @@ -1,4 +1,4 @@ -class @VideoVolumeControl extends Subview +class @VideoVolumeControlAlpha extends SubviewAlpha initialize: -> @currentVolume = 100 diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 58704ed6b1..7761eb980d 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,7 +2,7 @@

      ${display_name}

      % endif - + %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      From 786c11484ea3a3795dc2657c71246734228aebda Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 11:23:13 +0200 Subject: [PATCH 07/29] tag now support multi-source --- common/lib/xmodule/xmodule/video_module.py | 5 ++--- .../lib/xmodule/xmodule/videoalpha_module.py | 20 ++++++++++++------- lms/templates/videoalpha.html | 6 +++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 359b97df7c..27388f7630 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -4,6 +4,8 @@ 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.xml import XMLModuleStore @@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent import datetime import time -import datetime -import time - log = logging.getLogger(__name__) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 912505d0a6..06dd993f7a 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -4,6 +4,8 @@ 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 @@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent import datetime import time -import datetime -import time - log = logging.getLogger(__name__) @@ -44,6 +43,9 @@ class VideoAlphaModule(XModule): self.position = 0 self.show_captions = xmltree.get('show_captions', 'true') self.source = self._get_source(xmltree) + self.mp4_source = self._get_source(xmltree, ['mp4']) + self.wemb_source = self._get_source(xmltree, ['wemb']) + self.ogv_source = self._get_source(xmltree, ['ogv']) self.track = self._get_track(xmltree) self.start_time, self.end_time = self._get_timeframe(xmltree) @@ -52,15 +54,16 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree): + def _get_source(self, xmltree, extension=['mp4', 'ogv', 'avi', 'webm']): # find the first valid source - return self._get_first_external(xmltree, 'source') + condition = lambda src: any([src.endswith(ext) for ext in extension]) + 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): + def _get_first_external(self, xmltree, tag, condition=bool): """ Will return the first valid element of the given tag. @@ -69,7 +72,7 @@ class VideoAlphaModule(XModule): result = None for element in xmltree.findall(tag): src = element.get('src') - if src: + if condition(src): result = src break return result @@ -134,6 +137,9 @@ class VideoAlphaModule(XModule): 'streams': self.videoalpha_list(), 'id': self.location.html_id(), 'position': self.position, + 'mp4_source': self.mp4_source, + 'wemb_source': self.wemb_source, + 'ogv_source': self.ogv_source, 'source': self.source, 'track': self.track, 'display_name': self.display_name, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 7761eb980d..932d247c61 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -11,9 +11,9 @@ id="video_${id}" class="video" data-streams="" - data-mp4-source="http://clips.vorwaerts-gmbh.de/VfE_html5.mp4" - data-webm-source="http://clips.vorwaerts-gmbh.de/VfE_html5.webm" - data-ogg-source="http://clips.vorwaerts-gmbh.de/VfE_html5.ogv" + ${'data-mp4-source="{}"'.format(mp4_source) if mp4_source else ''} + ${'data-wemb-source="{}"'.format(wemb_source) if wemb_source else ''} + ${'data-ogg-source="{}"'.format(ogv_source) if ogv_source else ''} data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}" data-start="${start}" From 067f2e97f524d09f815f97c8660e4dacb7422ff6 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 12:48:03 +0200 Subject: [PATCH 08/29] Enabled play/pause buttons for HTML5 video player. --- .../js/src/videoalpha/display/html5_video.js | 127 ++++++++++++++---- .../videoalpha/display/video_player.coffee | 21 ++- lms/templates/videoalpha.html | 2 - 3 files changed, 115 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index c61d725e5c..05a2e97bf4 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -19,7 +19,7 @@ this.HTML5Video = (function () { * * 'height': 390, * - * 'videoSources': null, // An object of with properties being video sources. The property name is the + * 'videoSources': {}, // An object of with properties being video sources. The property name is the * // video format of the source. Supported video formats are: 'mp4', 'webm', and * // 'ogg'. By default videoSources property is null. This means that the * // player will initialize, and not play anything. If you do not provide a @@ -53,12 +53,14 @@ this.HTML5Video = (function () { * } */ function Player(el, config) { + var sourceStr, _this; + if (typeof el === 'string') { this.el = $(el); - } else if ($.isPlainObject(el) === true) { + } else if (el instanceof jQuery) { this.el = el; } else { - // Error. el parameter is required. + // Error. Parameter el does not have a recognized type. // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype // is called after we return. @@ -69,22 +71,99 @@ this.HTML5Video = (function () { if ($.isPlainObject(config) === true) { this.config = config; } else { - this.config = { - 'width': 640, - 'height': 390, - 'videoSource': '', - 'playerVars': { - 'controls': 1, - 'start': null, - 'end': null - }, - 'events': { - 'onReady': null, - 'onStateChange': null, - 'onPlaybackQualityChange': null - } - }; + // Error. Parameter config does not have a recognized type. + + // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype + // is called after we return. + + return; } + + sourceStr = { + 'mp4': ' ', + 'webm': ' ', + 'ogg': ' ' + }; + + _this = this; + $.each(sourceStr, function (videoType, videoSource) { + if ( + (_this.config.videoSources.hasOwnProperty(videoType) === true) && + (typeof _this.config.videoSources[videoType] === 'string') && + (_this.config.videoSources[videoType].length > 0) + ) { + sourceStr[videoType] = + ' '; + } + }); + + this.playerState = HTML5Video.PlayerState.UNSTARTED; + + this.videoEl = $( + '' + ); + + this.video = this.videoEl[0]; + + this.video.addEventListener('canplay', function () { + console.log('We got a "canplay" event.'); + + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onReady) === true) { + console.log('Callback function "onReady" is defined.'); + + _this.config.events.onReady({}); + } + }, false); + this.video.addEventListener('play', function () { + console.log('We got a "play" event.'); + + _this.playerState = HTML5Video.PlayerState.PLAYING; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + this.video.addEventListener('pause', function () { + console.log('We got a "pause" event.'); + + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + this.video.addEventListener('ended', function () { + console.log('We got a "ended" event.'); + + _this.playerState = HTML5Video.PlayerState.ENDED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + + this.videoEl.appendTo(this.el.find('.video-player div')); } /* @@ -122,7 +201,9 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { + console.log('Player.prototype.pauseVideo'); + this.video.pause(); }; Player.prototype.seekTo = function () { @@ -160,21 +241,15 @@ this.HTML5Video = (function () { }; Player.prototype.playVideo = function () { + console.log('Player.prototype.playVideo'); + this.video.play(); }; Player.prototype.getPlayerState = function () { }; - Player.prototype.pauseVideo = function () { - - }; - - Player.prototype.setVolume = function () { - - }; - Player.prototype.getVolume = function () { }; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index f295096388..f7f90971df 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,8 +1,11 @@ class @VideoPlayerAlpha extends SubviewAlpha initialize: -> if @video.videoType is 'youtube' + @PlayerState = YT.PlayerState # Define a missing constant of Youtube API - YT.PlayerState.UNSTARTED = -1 + @PlayerState.UNSTARTED = -1 + else if @video.videoType is 'html5' + @PlayerState = HTML5Video.PlayerState @currentTime = 0 @el = $("#video_#{@video.id}") @@ -51,7 +54,7 @@ class @VideoPlayerAlpha extends SubviewAlpha # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end if @video.videoType is 'html5' - @player = new HTML5Video.Player @video.id, + @player = new HTML5Video.Player @video.el, playerVars: @playerVars, videoSources: @video.html5Sources, events: @@ -80,13 +83,13 @@ class @VideoPlayerAlpha extends SubviewAlpha onStateChange: (event) => switch event.data - when YT.PlayerState.UNSTARTED + when @PlayerState.UNSTARTED @onUnstarted() - when YT.PlayerState.PLAYING + when @PlayerState.PLAYING @onPlay() - when YT.PlayerState.PAUSED + when @PlayerState.PAUSED @onPause() - when YT.PlayerState.ENDED + when @PlayerState.ENDED @onEnded() onPlaybackQualityChange: (event, value) => @@ -168,12 +171,16 @@ class @VideoPlayerAlpha extends SubviewAlpha # Delegates play: => + console.log 'Play clicked' + console.log @player.playVideo @player.playVideo() if @player.playVideo isPlaying: -> - @player.getPlayerState() == YT.PlayerState.PLAYING + @player.getPlayerState() == @PlayerState.PLAYING pause: => + console.log 'Pause clicked' + console.log @player.pauseVideo @player.pauseVideo() if @player.pauseVideo duration: -> diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 932d247c61..9d146a222c 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,8 +2,6 @@

      ${display_name}

      % endif - - %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      %else: From 80fa9d0116ad84654549b49d26027bd338cbfdfa Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 13:24:17 +0200 Subject: [PATCH 09/29] Added support for time line in HTML5. --- .../js/src/videoalpha/display/html5_video.js | 25 ++----------------- .../videoalpha/display/video_player.coffee | 7 ++---- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 05a2e97bf4..555d12187d 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -113,50 +113,34 @@ this.HTML5Video = (function () { this.video = this.videoEl[0]; this.video.addEventListener('canplay', function () { - console.log('We got a "canplay" event.'); - _this.playerState = HTML5Video.PlayerState.PAUSED; if ($.isFunction(_this.config.events.onReady) === true) { - console.log('Callback function "onReady" is defined.'); - _this.config.events.onReady({}); } }, false); this.video.addEventListener('play', function () { - console.log('We got a "play" event.'); - _this.playerState = HTML5Video.PlayerState.PLAYING; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); } }, false); this.video.addEventListener('pause', function () { - console.log('We got a "pause" event.'); - _this.playerState = HTML5Video.PlayerState.PAUSED; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); } }, false); this.video.addEventListener('ended', function () { - console.log('We got a "ended" event.'); - _this.playerState = HTML5Video.PlayerState.ENDED; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); @@ -201,7 +185,6 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { - console.log('Player.prototype.pauseVideo'); this.video.pause(); }; @@ -237,12 +220,10 @@ this.HTML5Video = (function () { }; Player.prototype.getCurrentTime = function () { - + return this.video.currentTime; }; Player.prototype.playVideo = function () { - console.log('Player.prototype.playVideo'); - this.video.play(); }; @@ -255,9 +236,7 @@ this.HTML5Video = (function () { }; Player.prototype.getDuration = function () { - // TODO: Return valid video duration. - - return 0; + return this.video.duration; }; return Player; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index f7f90971df..20b16ae01c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -171,22 +171,19 @@ class @VideoPlayerAlpha extends SubviewAlpha # Delegates play: => - console.log 'Play clicked' - console.log @player.playVideo @player.playVideo() if @player.playVideo isPlaying: -> @player.getPlayerState() == @PlayerState.PLAYING pause: => - console.log 'Pause clicked' - console.log @player.pauseVideo @player.pauseVideo() if @player.pauseVideo duration: -> if @video.videoType is "youtube" return @video.getDuration() - else return @player.getDuration() if @video.videoType is "html5" + else if @video.videoType is "html5" + return @player.getDuration() 0 currentSpeed: -> From 4f08d96cfd1c33207742772a64ae0820a45bd745 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 14:34:36 +0200 Subject: [PATCH 10/29] Added ability to change playback rate, added pause/play on video click, minor improvements. --- .../xmodule/js/src/videoalpha/display.coffee | 1 + .../js/src/videoalpha/display/html5_video.js | 47 ++++++++++++++++--- .../videoalpha/display/video_player.coffee | 12 +++-- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 735e916573..86f8e896c0 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -25,6 +25,7 @@ class @VideoAlpha @setSpeed($.cookie('video_speed')) $("#video_#{@id}").data('video', this).addClass('video-load-complete') @hide_captions = $.cookie('hide_captions') == 'true' + _this = this if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) @embed() else diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 555d12187d..12a5734f13 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -112,6 +112,28 @@ this.HTML5Video = (function () { this.video = this.videoEl[0]; + this.videoEl.on('click', function (event) { + if (_this.playerState === HTML5Video.PlayerState.PAUSED) { + _this.video.play(); + _this.playerState = HTML5Video.PlayerState.PLAYING; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + } else if (_this.playerState === HTML5Video.PlayerState.PLAYING) { + _this.video.pause(); + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + } + }); + this.video.addEventListener('canplay', function () { _this.playerState = HTML5Video.PlayerState.PAUSED; @@ -185,12 +207,13 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { - this.video.pause(); }; - Player.prototype.seekTo = function () { - + Player.prototype.seekTo = function (value) { + if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) { + this.video.currentTime = value; + } }; // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this @@ -215,8 +238,10 @@ this.HTML5Video = (function () { this.cueVideoBySource(id); }; - Player.prototype.setVolume = function () { - + Player.prototype.setVolume = function (value) { + if ((typeof value === 'number') && (value <= 100) && (value >= 0)) { + this.video.volume = value * 0.01; + } }; Player.prototype.getCurrentTime = function () { @@ -232,13 +257,23 @@ this.HTML5Video = (function () { }; Player.prototype.getVolume = function () { - + return this.video.volume; }; Player.prototype.getDuration = function () { return this.video.duration; }; + Player.prototype.setSpeed = function (value) { + var newSpeed; + + newSpeed = parseFloat(value); + + if (isFinite(newSpeed) === true) { + this.video.playbackRate = value; + } + } + return Player; }()); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 20b16ae01c..0094a2f47f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -139,11 +139,13 @@ class @VideoPlayerAlpha extends SubviewAlpha newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' @video.setSpeed(newSpeed) @caption.currentSpeed = newSpeed - - if @isPlaying() - @player.loadVideoById(@video.youtubeId(), @currentTime) - else - @player.cueVideoById(@video.youtubeId(), @currentTime) + if @video.videoType is 'html5' + @player.setSpeed(newSpeed) + else if @video.videoType is 'youtube' + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) @updatePlayTime @currentTime onVolumeChange: (event, volume) => From 5cce5688363ee3728b45f4c042243c44916824c4 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 14:52:58 +0200 Subject: [PATCH 11/29] add docstrings in videoalpha_module.py --- common/lib/xmodule/xmodule/videoalpha_module.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 06dd993f7a..47183a7fc7 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -54,9 +54,10 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree, extension=['mp4', 'ogv', 'avi', 'webm']): - # find the first valid source - condition = lambda src: any([src.endswith(ext) for ext in extension]) + def _get_source(self, xmltree, extensions=['mp4', 'ogv', 'avi', 'webm']): + """Find the first valid source, which ends with one of + `extensions`.""" + condition = lambda src: any([src.endswith(ext) for ext in extensions]) return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): @@ -64,10 +65,8 @@ class VideoAlphaModule(XModule): 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 has a non-empty 'src' attribute + """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): From 3ac9d54ff7c4fcc0e601b8d03afe78c7bf003375 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 15:43:39 +0200 Subject: [PATCH 12/29] fix wemb -> webm --- common/lib/xmodule/xmodule/videoalpha_module.py | 4 ++-- lms/templates/videoalpha.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 47183a7fc7..7ec63f6015 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -44,7 +44,7 @@ class VideoAlphaModule(XModule): self.show_captions = xmltree.get('show_captions', 'true') self.source = self._get_source(xmltree) self.mp4_source = self._get_source(xmltree, ['mp4']) - self.wemb_source = self._get_source(xmltree, ['wemb']) + self.webm_source = self._get_source(xmltree, ['webm']) self.ogv_source = self._get_source(xmltree, ['ogv']) self.track = self._get_track(xmltree) self.start_time, self.end_time = self._get_timeframe(xmltree) @@ -137,7 +137,7 @@ class VideoAlphaModule(XModule): 'id': self.location.html_id(), 'position': self.position, 'mp4_source': self.mp4_source, - 'wemb_source': self.wemb_source, + 'webm_source': self.webm_source, 'ogv_source': self.ogv_source, 'source': self.source, 'track': self.track, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 9d146a222c..18c8135823 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -10,7 +10,7 @@ class="video" data-streams="" ${'data-mp4-source="{}"'.format(mp4_source) if mp4_source else ''} - ${'data-wemb-source="{}"'.format(wemb_source) if wemb_source else ''} + ${'data-webm-source="{}"'.format(webm_source) if webm_source else ''} ${'data-ogg-source="{}"'.format(ogv_source) if ogv_source else ''} data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}" From d72036a6dc2adcab45373bf84d71bbacefbef2dc Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 19:04:55 +0200 Subject: [PATCH 13/29] add support `sub` attribute for
      %endif -% if source: +% if sources.get('main'):
      -

      Download video here.

      +

      Download video here.

      % endif From 0b73f0a59dd3ea52880202a9e9fe36078f9fd817 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:11:59 +0200 Subject: [PATCH 23/29] remove unnecessary code --- common/lib/xmodule/xmodule/videoalpha_module.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index f47a433fa7..b12dd359c3 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -111,16 +111,6 @@ class VideoAlphaModule(XModule): return json.dumps({'success': True}) raise Http404() - def get_progress(self): - ''' TODO (vshnayder): Get and save duration of youtube video, then return - fraction watched. - (Be careful to notice when video link changes and update) - - For now, we have no way of knowing if the video has even been watched, so - just return None. - ''' - return None - def get_instance_state(self): #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) @@ -139,7 +129,6 @@ class VideoAlphaModule(XModule): return self.system.render_template('videoalpha.html', { 'streams': self.videoalpha_list(), 'id': self.location.html_id(), - 'position': self.position, 'sub': self.sub, 'sources': self.sources, 'track': self.track, From 8cf8dcd10bcbdb45a019eccd072bc59c3675cb22 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:28:09 +0200 Subject: [PATCH 24/29] rm `videoalpha_list` method --- common/lib/xmodule/xmodule/videoalpha_module.py | 7 ++----- lms/templates/videoalpha.html | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index b12dd359c3..2861fe57f8 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -39,7 +39,7 @@ class VideoAlphaModule(XModule): XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) xmltree = etree.fromstring(self.definition['data']) - self.youtube = xmltree.get('youtube') + self.youtube_streams = xmltree.get('youtube') self.sub = xmltree.get('sub') self.position = 0 self.show_captions = xmltree.get('show_captions', 'true') @@ -115,9 +115,6 @@ class VideoAlphaModule(XModule): #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) - def videoalpha_list(self): - return self.youtube - def get_html(self): if isinstance(modulestore(), MongoModuleStore): caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_' @@ -127,7 +124,7 @@ class VideoAlphaModule(XModule): caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir']) return self.system.render_template('videoalpha.html', { - 'streams': self.videoalpha_list(), + 'youtube_streams': self.youtube_streams, 'id': self.location.html_id(), 'sub': self.sub, 'sources': self.sources, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index baed857a56..2ddcdd57e1 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -8,7 +8,7 @@
      Date: Wed, 13 Feb 2013 13:35:10 +0200 Subject: [PATCH 25/29] add xml exmaple for videoalpja module in docstring --- common/lib/xmodule/xmodule/videoalpha_module.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 2861fe57f8..067932a5de 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -19,6 +19,18 @@ log = logging.getLogger(__name__) class VideoAlphaModule(XModule): + """ + XML source example: + + + + + + + """ video_time = 0 icon_class = 'video' From b69b88a718cd7b61fc9513414c65cf11135380c7 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:35:34 +0200 Subject: [PATCH 26/29] some small fix --- common/lib/xmodule/xmodule/videoalpha_module.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 067932a5de..6910728d8c 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -22,14 +22,14 @@ class VideoAlphaModule(XModule): """ XML source example: - - - - - + + + + + """ video_time = 0 icon_class = 'video' From 1254e11836b89ae49630ce288a923f15f0bfa427 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 13 Feb 2013 14:09:35 +0200 Subject: [PATCH 27/29] Fixes and additions. Addressing comments by Carlos for pull request 1409. --- .../xmodule/js/src/videoalpha/display.coffee | 32 ++++++------ .../js/src/videoalpha/display/html5_video.js | 4 ++ .../videoalpha/display/video_player.coffee | 52 ++++++++++++++++--- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 8079383f6f..a27362b094 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -8,13 +8,13 @@ class @VideoAlpha @caption_asset_path = @el.data('caption-asset-path') @show_captions = @el.data('show-captions').toString() == "true" @el = $("#video_#{@id}") - if @parseVideos(@el.data("streams")) is true + if @parseYoutubeId(@el.data("streams")) is true @videoType = "youtube" @fetchMetadata() @parseSpeed() else @videoType = "html5" - @parseVideoSources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source') + @parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source') @speeds = ['0.75', '1.0', '1.25', '1.50'] sub = @el.data('sub') if (typeof sub isnt "string") or (sub.length is 0) @@ -33,32 +33,30 @@ class @VideoAlpha @hide_captions = true $.cookie('hide_captions', @hide_captions, expires: 3650, path: '/') @el.addClass 'closed' - _this = this if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) @embed() else if @videoType is "youtube" - window.onYouTubePlayerAPIReady = -> - _this.embed() + window.onYouTubePlayerAPIReady = => + @embed() else if @videoType is "html5" - window.onHTML5PlayerAPIReady = -> - _this.embed() + window.onHTML5PlayerAPIReady = => + @embed() youtubeId: (speed)-> @videos[speed || @speed] - parseVideos: (videos)-> + parseYoutubeId: (videos)-> return false if (typeof videos isnt "string") or (videos.length is 0) @videos = {} - _this = this - $.each videos.split(/,/), (index, video) -> + $.each videos.split(/,/), (index, video) => speed = undefined video = video.split(/:/) speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0") - _this.videos[speed] = video[1] + @videos[speed] = video[1] true - parseVideoSources: (mp4Source, webmSource, oggSource)-> + parseHtml5Sources: (mp4Source, webmSource, oggSource)-> @html5Sources = mp4: null webm: null @@ -71,12 +69,14 @@ class @VideoAlpha @speeds = ($.map @videos, (url, speed) -> speed).sort() @setSpeed $.cookie('video_speed') - setSpeed: (newSpeed)-> + setSpeed: (newSpeed, updateCookie)-> if @speeds.indexOf(newSpeed) isnt -1 @speed = newSpeed - $.cookie "video_speed", "" + newSpeed, - expires: 3650 - path: "/" + + if updateCookie isnt false + $.cookie "video_speed", "" + newSpeed, + expires: 3650 + path: "/" else @speed = "1.0" diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index fb34733323..acdc03932c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -48,6 +48,10 @@ this.HTML5Video = (function () { }; Player.prototype.getDuration = function () { + if (isFinite(this.video.duration) === false) { + return 0; + } + return this.video.duration; }; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 566e4d785a..2def749d23 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,8 +1,13 @@ class @VideoPlayerAlpha extends SubviewAlpha initialize: -> + # If we switch verticals while the video is playing, then HTML content is + # removed, but JS code is still executing (setInterval() method), and there will + # arise conflicts (no HTML content, but code tries to access it). Therefore + # we must pause the player (stop setInterval() method). if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause) window.OldVideoPlayerAlpha.onPause() window.OldVideoPlayerAlpha = this + if @video.videoType is 'youtube' @PlayerState = YT.PlayerState # Define a missing constant of Youtube API @@ -99,16 +104,37 @@ class @VideoPlayerAlpha extends SubviewAlpha _this = this switch event.data when @PlayerState.UNSTARTED + # Before the video starts playing, let us see if we are in YouTube player, + # and if YouTube is in HTML5 mode. If both cases are true, then we can make + # it so that speed switching happens natively. + if @video.videoType is "youtube" + # Because YouTube API does not have a direct method to determine the mode we + # are in (Flash or HTML5), we rely on an indirect method. Currently, when in + # Flash mode, YouTube player reports that there is only one (1.0) speed + # available. When in HTML5 mode, it reports multiple speeds available. We + # will use this fact. + # + # NOTE: It is my strong belief that in the future YouTube Flash player will + # not get speed changes. This is a dying technology. So we can safely use + # this indirect method to determine player mode. availableSpeeds = @player.getAvailablePlaybackRates() prev_player_type = $.cookie('prev_player_type') if availableSpeeds.length > 1 + # If the user last accessed the page and watched a movie via YouTube + # player, and it was using Flash mode, then we must reset the current + # YouTube speed to 1.0 (by loading appropriate video that is encoded at + # 1.0 speed). if prev_player_type == 'youtube' $.cookie('prev_player_type', 'html5', expires: 3650, path: '/') - @onSpeedChange null, '1.0' + @onSpeedChange null, '1.0', false else if prev_player_type != 'html5' $.cookie('prev_player_type', 'html5', expires: 3650, path: '/') + # Now we must update all the speeds to the ones available via the YouTube + # HTML5 API. The default speeds are not exactly the same as reported by + # YouTube, so we will remove the default speeds, and populate all the + # necessary data with correct available speeds. baseSpeedSubs = @video.videos["1.0"] $.each @video.videos, (index, value) -> delete _this.video.videos[index] @@ -116,15 +142,26 @@ class @VideoPlayerAlpha extends SubviewAlpha $.each availableSpeeds, (index, value) -> _this.video.videos[value.toFixed(2).replace(/\.00$/, ".0")] = baseSpeedSubs _this.video.speeds.push value.toFixed(2).replace(/\.00$/, ".0") + + # We must update the Speed Control to reflect the new avialble speeds. @speedControl.reRender @video.speeds, @video.speed + + # Now we set the videoType to 'HTML5'. This works because my HTML5Video + # class is fully compatible with YouTube HTML5 API. @video.videoType = 'html5' @video.setSpeed $.cookie('video_speed') + + # Change the speed to the required one. @player.setPlaybackRate @video.speed else + # We are in YouTube player, and in Flash mode. Check previos mode. if prev_player_type != 'youtube' $.cookie('prev_player_type', 'youtube', expires: 3650, path: '/') + # We need to set the proper speed when previous mode was not 'youtube'. + @onSpeedChange null, $.cookie('video_speed') + @onUnstarted() when @PlayerState.PLAYING @onPlay() @@ -176,11 +213,11 @@ class @VideoPlayerAlpha extends SubviewAlpha @currentTime = time @updatePlayTime time - onSpeedChange: (event, newSpeed) => + onSpeedChange: (event, newSpeed, updateCookie) => if @video.videoType is 'youtube' @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' - @video.setSpeed newSpeed + @video.setSpeed newSpeed, updateCookie if @video.videoType is 'youtube' if @video.show_captions is true @caption.currentSpeed = newSpeed @@ -230,11 +267,10 @@ class @VideoPlayerAlpha extends SubviewAlpha @player.pauseVideo() if @player.pauseVideo duration: -> - if @video.videoType is "youtube" - return @video.getDuration() - else if @video.videoType is "html5" - return @player.getDuration() - 0 + duration = @player.getDuration() + if isFinite(duration) is false + duration = @video.getDuration() + duration currentSpeed: -> @video.speed From 92dc8859a3fd619396ce4cc520e0e2ebefe58d95 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 13 Feb 2013 16:47:06 +0200 Subject: [PATCH 28/29] YouTube HTML5 mode is used by default. Fix typo in video alpha template. --- .../xmodule/js/src/videoalpha/display/video_player.coffee | 1 + lms/templates/videoalpha.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 2def749d23..1b761594de 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -59,6 +59,7 @@ class @VideoPlayerAlpha extends SubviewAlpha showinfo: 0 enablejsapi: 1 modestbranding: 1 + html5: 1 if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 2ddcdd57e1..2028d3c320 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -32,7 +32,7 @@ % if sources.get('main'):
      -

      Download video here.

      +

      Download video here.

      % endif From d55b818bb2bb518402250b252cab06c0726824b3 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 14 Feb 2013 11:25:36 +0200 Subject: [PATCH 29/29] Updated YouTube JS API file to the new one. --- lms/templates/courseware/courseware.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index fcbc83d815..33dc9562a7 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -32,7 +32,7 @@ % if timer_expiration_duration: - % endif