Commit 3cc793b0 by Adam

Merge pull request #856 from edx/adam/merge-conflicts

Adam/merge conflicts
parents e284ac68 67890b3c
......@@ -72,7 +72,7 @@ $('#fileupload').fileupload({
add: function(e, data) {
submitBtn.unbind('click');
var file = data.files[0];
if (file.type == "application/x-gzip") {
if (file.name.match(/tar\.gz$/)) {
submitBtn.click(function(e){
e.preventDefault();
submitBtn.hide();
......
......@@ -40,6 +40,12 @@ div.video {
padding-bottom: 56.25%;
position: relative;
div {
&.hidden {
display: none;
}
}
object, iframe {
border: none;
height: 100%;
......@@ -48,6 +54,15 @@ div.video {
top: 0;
width: 100%;
}
h3 {
text-align: center;
color: white;
&.hidden {
display: none;
}
}
}
section.video-controls {
......@@ -516,6 +531,12 @@ div.video {
height: 0px;
}
article.video-wrapper section.video-player {
h3 {
color: black;
}
}
ol.subtitles {
width: 0;
height: 0;
......@@ -563,6 +584,12 @@ div.video {
position: static;
}
article.video-wrapper section.video-player {
h3 {
color: white;
}
}
div.tc-wrapper {
@include clearfix;
display: table;
......
......@@ -10,6 +10,8 @@
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
......@@ -13,6 +13,8 @@
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
......@@ -13,6 +13,8 @@
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
......@@ -10,6 +10,8 @@
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
......@@ -90,12 +90,24 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
if settings.success
status = match[1].split('_')
if status and status[0] is 'status'
{
always: (callback) ->
callback.call(window, {}, status[1])
error: (callback) ->
callback.call(window, {}, status[1])
done: (callback) ->
callback.call(window, {}, status[1])
}
else if settings.success
# match[1] - it's video ID
settings.success data: jasmine.stubbedMetadata[match[1]]
else {
always: (callback) ->
callback.call(window, {}, 'success');
callback.call(window, {}, 'success')
done: (callback) ->
callback.call(window, {}, 'success')
}
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption
......
......@@ -55,46 +55,6 @@
expect(this.state.speed).toEqual('0.75');
});
});
describe('Check Youtube link existence', function () {
var statusList = {
error: 'html5',
timeout: 'html5',
abort: 'html5',
parsererror: 'html5',
success: 'youtube',
notmodified: 'youtube'
};
function stubDeffered(data, status) {
return {
always: function(callback) {
callback.call(window, data, status);
}
}
}
function checkPlayer(videoType, data, status) {
this.state = new window.Video('#example');
spyOn(this.state , 'getVideoMetadata')
.andReturn(stubDeffered(data, status));
this.state.initialize('#example');
expect(this.state.videoType).toEqual(videoType);
}
it('if video id is incorrect', function () {
checkPlayer('html5', { error: {} }, 'success');
});
$.each(statusList, function(status, mode){
it('Status:' + status + ', mode:' + mode, function () {
checkPlayer(mode, {}, status);
});
});
});
});
describe('HTML5', function () {
......@@ -154,10 +114,22 @@
it('parse Html5 sources', function () {
var html5Sources = {
mp4: 'xmodule/include/fixtures/test.mp4',
webm: 'xmodule/include/fixtures/test.webm',
ogg: 'xmodule/include/fixtures/test.ogv'
};
mp4: null,
webm: null,
ogg: null
}, v = document.createElement('video');
if (!!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''))) {
html5Sources['webm'] = 'xmodule/include/fixtures/test.webm';
}
if (!!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''))) {
html5Sources['mp4'] = 'xmodule/include/fixtures/test.mp4';
}
if (!!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''))) {
html5Sources['ogg'] = 'xmodule/include/fixtures/test.ogv';
}
expect(state.html5Sources).toEqual(html5Sources);
});
......
......@@ -147,8 +147,6 @@ function (VideoPlayer) {
if (state.parseYoutubeStreams(state.config.youtubeStreams)) {
state.videoType = 'youtube';
state.fetchMetadata();
state.parseSpeed();
return true;
}
return false;
......@@ -157,9 +155,7 @@ function (VideoPlayer) {
// function _prepareHTML5Video(state)
// The function prepare HTML5 video, parse HTML5
// video sources etc.
function _prepareHTML5Video(state) {
state.videoType = 'html5';
function _prepareHTML5Video(state, html5Mode) {
state.parseVideoSources(
{
mp4: state.config.mp4Source,
......@@ -168,20 +164,39 @@ function (VideoPlayer) {
}
);
if (html5Mode) {
state.speeds = ['0.75', '1.0', '1.25', '1.50'];
state.videos = {
'0.75': state.config.sub,
'1.0': state.config.sub,
'1.25': state.config.sub,
'1.5': state.config.sub
};
}
// We must have at least one non-YouTube video source available.
// Otherwise, return a negative.
if (
state.html5Sources.webm === null &&
state.html5Sources.mp4 === null &&
state.html5Sources.ogg === null
) {
state.el.find('.video-player div').addClass('hidden');
state.el.find('.video-player h3').removeClass('hidden');
return false;
}
state.videoType = 'html5';
if (!state.config.sub || !state.config.sub.length) {
state.config.sub = '';
state.config.show_captions = false;
}
state.speeds = ['0.75', '1.0', '1.25', '1.50'];
state.videos = {
'0.75': state.config.sub,
'1.0': state.config.sub,
'1.25': state.config.sub,
'1.5': state.config.sub
};
state.setSpeed($.cookie('video_speed'));
return true;
}
function _setConfigurations(state) {
......@@ -205,7 +220,7 @@ function (VideoPlayer) {
// The function set initial configuration and preparation.
function initialize(element) {
var _this = this;
var _this = this, tempYtTestTimeout;
// This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'.
this.isFullScreen = false;
......@@ -231,28 +246,61 @@ function (VideoPlayer) {
webmSource: this.el.data('webm-source'),
oggSource: this.el.data('ogg-source'),
ytTestUrl: this.el.data('yt-test-url'),
fadeOutTimeout: 1400,
availableQualities: ['hd720', 'hd1080', 'highres']
};
// Check if the YT test timeout has been set. If not, or it is in
// improper format, then set to default value.
tempYtTestTimeout = parseInt(this.el.data('yt-test-timeout'), 10);
if (!isFinite(tempYtTestTimeout)) {
tempYtTestTimeout = 1500;
}
this.config.ytTestTimeout = tempYtTestTimeout;
if (!(_parseYouTubeIDs(this))) {
// If we do not have YouTube ID's, try parsing HTML5 video sources.
_prepareHTML5Video(this);
if (!_prepareHTML5Video(this, true)) {
// Non-YouTube sources were not found either.
return;
}
_setConfigurations(this);
_renderElements(this);
} else {
this.getVideoMetadata()
if (!this.youtubeXhr) {
this.youtubeXhr = this.getVideoMetadata();
}
this.youtubeXhr
.always(function(json, status) {
var err = $.isPlainObject(json.error) ||
(status !== "success" && status !== "notmodified");
if (err){
(status !== 'success' && status !== 'notmodified');
if (err) {
// When the youtube link doesn't work for any reason
// (for example, the great firewall in china) any
// alternate sources should automatically play.
_prepareHTML5Video(_this);
_this.el.find('a.quality_control').hide();
if (!_prepareHTML5Video(_this)) {
// Non-YouTube sources were not found either.
_this.el.find('.video-player div').removeClass('hidden');
_this.el.find('.video-player h3').addClass('hidden');
// If in reality the timeout was to short, try to
// continue loading the YouTube video anyways.
_this.fetchMetadata();
_this.parseSpeed();
} else {
// In-browser HTML5 player does not support quality
// control.
_this.el.find('a.quality_control').hide();
}
} else {
_this.fetchMetadata();
_this.parseSpeed();
}
_setConfigurations(_this);
......@@ -298,7 +346,13 @@ function (VideoPlayer) {
// Take the HTML5 sources (URLs of videos), and make them available explictly for each type
// of video format (mp4, webm, ogg).
function parseVideoSources(sources) {
var _this = this;
var _this = this,
v = document.createElement('video'),
sourceCodecs = {
mp4: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
webm: 'video/webm; codecs="vp8, vorbis"',
ogg: 'video/ogg; codecs="theora"'
};
this.html5Sources = {
mp4: null,
......@@ -308,7 +362,14 @@ function (VideoPlayer) {
$.each(sources, function (name, source) {
if (source && source.length) {
_this.html5Sources[name] = source;
if (
Boolean(
v.canPlayType &&
v.canPlayType(sourceCodecs[name]).replace(/no/, '')
)
) {
_this.html5Sources[name] = source;
}
}
});
}
......@@ -325,7 +386,9 @@ function (VideoPlayer) {
$.each(this.videos, function (speed, url) {
_this.getVideoMetadata(url, function(data) {
_this.metadata[data.data.id] = data.data;
if (data.data) {
_this.metadata[data.data.id] = data.data;
}
});
});
}
......@@ -362,12 +425,11 @@ function (VideoPlayer) {
if (typeof url !== 'string') {
url = this.videos['1.0'] || '';
}
successHandler = ($.isFunction(callback)) ? callback : null;
xhr = $.ajax({
url: 'https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc',
timeout: 500,
url: this.config.ytTestUrl + url + '?v=2&alt=jsonc',
dataType: 'jsonp',
timeout: this.config.ytTestTimeout,
success: successHandler
});
......
......@@ -10,21 +10,31 @@ function () {
return function (state) {
state.videoSpeedControl = {};
if (state.videoType === 'html5') {
_initialize(state);
} else if (state.videoType === 'youtube' && state.youtubeXhr) {
state.youtubeXhr.done(function () {
_initialize(state);
});
}
if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
_hideSpeedControl(state);
return;
}
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
function _initialize(state) {
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
}
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called,
......
......@@ -20,7 +20,8 @@ function (
VideoSpeedControl,
VideoCaption
) {
var previousState;
var previousState,
youtubeXhr = null;
// Because this constructor can be called multiple times on a single page (when
// the user switches verticals, the page doesn't reload, but the content changes), we must
......@@ -53,7 +54,11 @@ function (
state = {};
previousState = state;
state.youtubeXhr = youtubeXhr;
Initialize(state, element);
if (!youtubeXhr) {
youtubeXhr = state.youtubeXhr;
}
VideoControl(state);
VideoQualityControl(state);
......@@ -67,6 +72,10 @@ function (
// Video with Jasmine.
return state;
};
window.Video.clearYoutubeXhr = function () {
youtubeXhr = null;
};
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
......@@ -164,6 +164,12 @@ class VideoModule(VideoFields, XModule):
sources = {get_ext(src): src for src in self.html5_sources}
sources['main'] = self.source
# for testing Youtube timeout in acceptance tests
if getattr(settings, 'VIDEO_PORT', None):
yt_test_url = "http://127.0.0.1:" + str(settings.VIDEO_PORT) + '/test_youtube/'
else:
yt_test_url = 'https://gdata.youtube.com/feeds/api/videos/'
return self.system.render_template('video.html', {
'youtube_streams': _create_youtube_string(self),
'id': self.location.html_id(),
......@@ -178,7 +184,11 @@ class VideoModule(VideoFields, XModule):
'show_captions': json.dumps(self.show_captions),
'start': self.start_time,
'end': self.end_time,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
# TODO: Later on the value 1500 should be taken from some global
# configuration setting field.
'yt_test_timeout': 1500,
'yt_test_url': yt_test_url
})
......
Feature: Video component
As a student, I want to view course videos in LMS.
Scenario: Video component is fully rendered in the LMS in HTML5 mode
Given the course has a Video component in HTML5 mode
Then when I view the video it has rendered in HTML5 mode
And all sources are correct
Scenario: Video component is fully rendered in the LMS in Youtube mode
Given the course has a Video component in Youtube mode
Then when I view the video it has rendered in Youtube mode
# Firefox doesn't have HTML5
# Firefox doesn't have HTML5 (only mp4 - fix here)
@skip_firefox
Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component in HTML5 mode
Then when I view the video it has autoplay enabled
# Youtube testing
Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
Given youtube server is up and response time is 0.4 seconds
And the course has a Video component in Youtube_HTML5 mode
Then when I view the video it has rendered in Youtube mode
Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources
Given youtube server is up and response time is 2 seconds
And the course has a Video component in Youtube_HTML5 mode
Then when I view the video it has rendered in HTML5 mode
Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
Given youtube server is up and response time is 2 seconds
And the course has a Video component in Youtube mode
Then when I view the video it has rendered in Youtube mode
Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser
Given youtube server is up and response time is 2 seconds
And the course has a Video component in Youtube_HTML5_Unsupported_Video mode
Then when I view the video it has rendered in Youtube mode
Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser
Given the course has a Video component in HTML5_Unsupported_Video mode
Then error message is shown
And error message has correct text
......@@ -3,6 +3,7 @@
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course, section_location
from django.utils.translation import ugettext as _
############### ACTIONS ####################
......@@ -11,6 +12,9 @@ HTML5_SOURCES = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv'
]
HTML5_SOURCES_INCORRECT = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99'
]
@step('when I view the (.*) it has autoplay enabled$')
def does_autoplay_video(_step, video_type):
......@@ -51,10 +55,37 @@ def add_video_to_course(course, player_mode):
'html5_sources': HTML5_SOURCES
}
})
if player_mode == 'youtube_html5':
kwargs.update({
'metadata': {
'html5_sources': HTML5_SOURCES
}
})
if player_mode == 'youtube_html5_unsupported_video':
kwargs.update({
'metadata': {
'html5_sources': HTML5_SOURCES_INCORRECT
}
})
if player_mode == 'html5_unsupported_video':
kwargs.update({
'metadata': {
'youtube_id_1_0': '',
'youtube_id_0_75': '',
'youtube_id_1_25': '',
'youtube_id_1_5': '',
'html5_sources': HTML5_SOURCES_INCORRECT
}
})
world.ItemFactory.create(**kwargs)
@step('youtube server is up and response time is (.*) seconds$')
def set_youtube_response_timeout(_step, time):
world.youtube_server.time_to_response = time
@step('when I view the video it has rendered in (.*) mode$')
def video_is_rendered(_step, mode):
modes = {
......@@ -64,9 +95,23 @@ def video_is_rendered(_step, mode):
html_tag = modes[mode.lower()]
assert world.css_find('.video {0}'.format(html_tag)).first
@step('all sources are correct$')
def all_sources_are_correct(_step):
sources = world.css_find('.video video source')
assert set(source['src'] for source in sources) == set(HTML5_SOURCES)
@step('error message is shown$')
def error_message_is_shown(_step):
selector = '.video .video-player h3'
assert world.css_visible(selector)
@step('error message has correct text$')
def error_message_has_correct_text(_step):
selector = '.video .video-player h3'
text = _('ERROR: No playable video sources found!')
assert world.css_has_text(selector, text)
#pylint: disable=C0111
#pylint: disable=W0621
from courseware.mock_youtube_server.mock_youtube_server import MockYoutubeServer
from lettuce import before, after, world
from django.conf import settings
import threading
from logging import getLogger
logger = getLogger(__name__)
@before.all
def setup_mock_youtube_server():
# import ipdb; ipdb.set_trace()
server_host = '127.0.0.1'
server_port = settings.VIDEO_PORT
address = (server_host, server_port)
# Create the mock server instance
server = MockYoutubeServer(address)
logger.debug("Youtube server started at {} port".format(str(server_port)))
server.time_to_response = 1 # seconds
# Start the server running in a separate daemon thread
# Because the thread is a daemon, it will terminate
# when the main thread terminates.
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Store the server instance in lettuce's world
# so that other steps can access it
# (and we can shut it down later)
world.youtube_server = server
@after.all
def teardown_mock_youtube_server(total):
# Stop the LTI server and free up the port
world.youtube_server.shutdown()
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import urlparse
from requests.packages.oauthlib.oauth1.rfc5849 import signature
import mock
import threading
import json
from logging import getLogger
logger = getLogger(__name__)
import time
class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
'''
A handler for Youtube GET requests.
'''
protocol = "HTTP/1.0"
def do_HEAD(self):
self._send_head()
def do_GET(self):
'''
Handle a GET request from the client and sends response back.
'''
self._send_head()
logger.debug("Youtube provider received GET request to path {}".format(
self.path)
) # Log the request
status_message = "I'm youtube."
response_timeout = float(self.server.time_to_response)
# threading timer produces TypeError: 'NoneType' object is not callable here
# so we use time.sleep, as we already in separate thread.
time.sleep(response_timeout)
self._send_response(status_message)
def _send_head(self):
'''
Send the response code and MIME headers
'''
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def _send_response(self, message):
'''
Send message back to the client
'''
callback = urlparse.parse_qs(self.path)['callback'][0]
response = callback + '({})'.format(json.dumps({'message': message}))
# Log the response
logger.debug("Youtube: sent response {}".format(message))
self.wfile.write(response)
class MockYoutubeServer(HTTPServer):
'''
A mock Youtube provider server that responds
to GET requests to localhost.
'''
def __init__(self, address):
'''
Initialize the mock XQueue server instance.
*address* is the (host, host's port to listen to) tuple.
'''
handler = MockYoutubeRequestHandler
HTTPServer.__init__(self, address, handler)
def shutdown(self):
'''
Stop the server and free up the port
'''
# First call superclass shutdown()
HTTPServer.shutdown(self)
# We also need to manually close the socket
self.socket.close()
"""
Test for Mock_Youtube_Server
"""
import unittest
import threading
import urllib
from mock_youtube_server import MockYoutubeServer
from nose.plugins.skip import SkipTest
class MockYoutubeServerTest(unittest.TestCase):
'''
A mock version of the Youtube provider server that listens on a local
port and responds with jsonp.
Used for lettuce BDD tests in lms/courseware/features/video.feature
'''
def setUp(self):
# This is a test of the test setup,
# so it does not need to run as part of the unit test suite
# You can re-enable it by commenting out the line below
raise SkipTest
# Create the server
server_port = 8034
server_host = '127.0.0.1'
address = (server_host, server_port)
self.server = MockYoutubeServer(address, )
self.server.time_to_response = 0.5
# Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
def tearDown(self):
# Stop the server, freeing up the port
self.server.shutdown()
def test_request(self):
"""
Tests that Youtube server processes request with right program
path, and responses with incorrect signature.
"""
# GET request
response_handle = urllib.urlopen(
'http://127.0.0.1:8034/feeds/api/videos/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
)
response = response_handle.read()
self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response)
......@@ -64,7 +64,9 @@ class TestVideo(BaseTestXmodule):
'sub': u'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': _create_youtube_string(self.item_module),
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.maxDiff = None
......@@ -114,7 +116,9 @@ class TestVideoNonYouTube(TestVideo):
'sub': 'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(context, expected_context)
......@@ -92,7 +92,9 @@ class VideoModuleUnitTest(unittest.TestCase):
'sources': sources,
'youtube_streams': _create_youtube_string(module),
'track': '',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(module.get_html(), expected_context)
......
......@@ -82,6 +82,11 @@ XQUEUE_INTERFACE = {
"basic_auth": ('anant', 'agarwal'),
}
# Set up Video information so that the lms will send
# requests to a mock Youtube server running locally
VIDEO_PORT = XQUEUE_PORT + 2
# Forums are disabled in test.py to speed up unit tests, but we do not have
# per-test control for acceptance tests
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
......
......@@ -23,6 +23,8 @@
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}"
data-yt-test-timeout="${yt_test_timeout}"
data-yt-test-url="${yt_test_url}"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......@@ -30,6 +32,7 @@
<section class="video-player">
<div id="${id}"></div>
<h3 class="hidden">${_('ERROR: No playable video sources found!')}</h3>
</section>
<div class="video-player-post"></div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment