Commit 1f61f6ce by Carlos Andrés Rocha

Merge pull request #1409 from MITx/feature/valera/video_alpha

Feature/valera/video alpha
parents 5134c99d 237e6ff6
...@@ -37,6 +37,7 @@ setup( ...@@ -37,6 +37,7 @@ setup(
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor",
......
& {
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;
}
}
}
}
}
*.js # Please do not ignore *.js files. Some xmodules are written in JS.
class @VideoAlpha
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').toString() == "true"
@el = $("#video_#{@id}")
if @parseYoutubeId(@el.data("streams")) is true
@videoType = "youtube"
@fetchMetadata()
@parseSpeed()
else
@videoType = "html5"
@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)
sub = ""
@show_captions = false
@videos =
"0.75": sub
"1.0": sub
"1.25": sub
"1.5": sub
@setSpeed $.cookie('video_speed')
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
if @show_captions is true
@hide_captions = $.cookie('hide_captions') == 'true'
else
@hide_captions = true
$.cookie('hide_captions', @hide_captions, expires: 3650, path: '/')
@el.addClass 'closed'
if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player))
@embed()
else
if @videoType is "youtube"
window.onYouTubePlayerAPIReady = =>
@embed()
else if @videoType is "html5"
window.onHTML5PlayerAPIReady = =>
@embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseYoutubeId: (videos)->
return false if (typeof videos isnt "string") or (videos.length is 0)
@videos = {}
$.each videos.split(/,/), (index, video) =>
speed = undefined
video = video.split(/:/)
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0")
@videos[speed] = video[1]
true
parseHtml5Sources: (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: ->
@speeds = ($.map @videos, (url, speed) -> speed).sort()
@setSpeed $.cookie('video_speed')
setSpeed: (newSpeed, updateCookie)->
if @speeds.indexOf(newSpeed) isnt -1
@speed = newSpeed
if updateCookie isnt false
$.cookie "video_speed", "" + newSpeed,
expires: 3650
path: "/"
else
@speed = "1.0"
embed: ->
@player = new VideoPlayerAlpha 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)->
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
class @SubviewAlpha
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
this.HTML5Video = (function () {
var HTML5Video;
HTML5Video = {};
HTML5Video.Player = (function () {
Player.prototype.callStateChangeCallback = function () {
if ($.isFunction(this.config.events.onStateChange) === true) {
this.config.events.onStateChange({
'data': this.playerState
});
}
};
Player.prototype.pauseVideo = function () {
this.video.pause();
};
Player.prototype.seekTo = function (value) {
if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) {
this.start = 0;
this.end = this.video.duration;
this.video.currentTime = value;
}
};
Player.prototype.setVolume = function (value) {
if ((typeof value === 'number') && (value <= 100) && (value >= 0)) {
this.video.volume = value * 0.01;
}
};
Player.prototype.getCurrentTime = function () {
return this.video.currentTime;
};
Player.prototype.playVideo = function () {
this.video.play();
};
Player.prototype.getPlayerState = function () {
return this.playerState;
};
Player.prototype.getVolume = function () {
return this.video.volume;
};
Player.prototype.getDuration = function () {
if (isFinite(this.video.duration) === false) {
return 0;
}
return this.video.duration;
};
Player.prototype.setPlaybackRate = function (value) {
var newSpeed;
newSpeed = parseFloat(value);
if (isFinite(newSpeed) === true) {
this.video.playbackRate = value;
}
};
Player.prototype.getAvailablePlaybackRates = function () {
return [0.75, 1.0, 1.25, 1.5];
};
return Player;
/*
* 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 = {
*
* 'videoSources': {}, // An object with properties being video sources. The property name is the
* // video format of the source. Supported video formats are: 'mp4', 'webm', and
* // 'ogg'.
*
* 'playerVars': { // Object's properties identify player parameters.
* 'start': 0, // Possible values: positive integer. Position from which to start playing the
* // video. Measured in seconds. If value is non-numeric, 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
* }
* }
*/
function Player(el, config) {
var sourceStr, _this;
// If el is string, we assume it is an ID of a DOM element. Get the element, and check that the ID
// really belongs to an element. If we didn't get a DOM element, return. At this stage, nothing will
// break because other parts of the video player are waiting for 'onReady' callback to be called.
if (typeof el === 'string') {
this.el = $(el);
if (this.el.length === 0) {
return;
}
} else if (el instanceof jQuery) {
this.el = el;
} else {
return;
}
// A simple test to see that the 'config' is a normal object.
if ($.isPlainObject(config) === true) {
this.config = config;
} else {
return;
}
// We should have at least one video source. Otherwise there is no point to continue.
if (config.hasOwnProperty('videoSources') === false) {
return;
}
// From the start, all sources are empty. We will populate this object below.
sourceStr = {
'mp4': ' ',
'webm': ' ',
'ogg': ' '
};
// Will be used in inner functions to point to the current object.
_this = this;
// Create HTML markup for individual sources of the HTML5 <video> element.
$.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] =
'<source ' +
'src="' + _this.config.videoSources[videoType] + '" ' +
'type="video/' + videoType + '" ' +
'/> ';
}
});
// We should have at least one video source. Otherwise there is no point to continue.
if ((sourceStr.mp4 === ' ') && (sourceStr.webm === ' ') && (sourceStr.ogg === ' ')) {
return;
}
// Determine the starting and ending time for the video.
this.start = 0;
this.end = null;
if (config.hasOwnProperty('playerVars') === true) {
this.start = parseFloat(config.playerVars.start);
if ((isFinite(this.start) !== true) || (this.start < 0)) {
this.start = 0;
}
this.end = parseFloat(config.playerVars.end);
if ((isFinite(this.end) !== true) || (this.end < this.start)) {
this.end = null;
}
}
// Create HTML markup for the <video> element, populating it with sources from previous step.
this.videoEl = $(
'<video style="width: 100%;">' +
sourceStr.mp4 +
sourceStr.webm +
sourceStr.ogg +
'</video>'
);
// Get the DOM element (to access the HTML5 video API), and set the player state to UNSTARTED.
// The player state is used by other parts of the VideoPlayer to detrermine what the video is
// currently doing.
this.video = this.videoEl[0];
this.playerState = HTML5Video.PlayerState.UNSTARTED;
// this.callStateChangeCallback();
// Attach a 'click' event on the <video> element. It will cause the video to pause/play.
this.videoEl.on('click', function (event) {
if (_this.playerState === HTML5Video.PlayerState.PAUSED) {
_this.video.play();
_this.playerState = HTML5Video.PlayerState.PLAYING;
_this.callStateChangeCallback();
} else if (_this.playerState === HTML5Video.PlayerState.PLAYING) {
_this.video.pause();
_this.playerState = HTML5Video.PlayerState.PAUSED;
_this.callStateChangeCallback();
}
});
// When the <video> tag has been processed by the browser, and it is ready for playback,
// notify other parts of the VideoPlayer, and initially pause the video.
//
// Also, at this time we can get the real duration of the video. Update the starting end ending
// points of the video. Note that first time, the video will start playing at the specified start time,
// and end playing at the specified end time. After it was paused, or when a seek operation happeded,
// the starting time and ending time will reset to the beginning and the end of the video respectively.
this.video.addEventListener('canplay', function () {
_this.playerState = HTML5Video.PlayerState.PAUSED;
if (_this.start > _this.video.duration) {
_this.start = 0;
}
if ((_this.end === null) || (_this.end > _this.video.duration)) {
_this.end = _this.video.duration;
}
_this.video.currentTime = _this.start;
if ($.isFunction(_this.config.events.onReady) === true) {
_this.config.events.onReady(null);
}
}, false);
// Register the 'play' event.
this.video.addEventListener('play', function () {
_this.playerState = HTML5Video.PlayerState.PLAYING;
_this.callStateChangeCallback();
}, false);
// Register the 'pause' event.
this.video.addEventListener('pause', function () {
_this.playerState = HTML5Video.PlayerState.PAUSED;
_this.callStateChangeCallback();
}, false);
// Register the 'ended' event.
this.video.addEventListener('ended', function () {
_this.playerState = HTML5Video.PlayerState.ENDED;
_this.callStateChangeCallback();
}, false);
// Register the 'timeupdate' event. This is the place where we control when the video ends.
// If an ending time was specified, then after the video plays through to this spot, pauses, we
// must make sure to update the ending time to the end of the video. This way, the user can watch
// any parts of it afterwards.
this.video.addEventListener('timeupdate', function (data) {
if (_this.end < _this.video.currentTime) {
// When we call video.pause(), a 'pause' event will be formed, and we will catch it
// in another handler (see above).
_this.video.pause();
_this.end = _this.video.duration;
}
}, false);
// Place the <video> element on the page.
this.videoEl.appendTo(this.el.find('.video-player div'));
}
}());
HTML5Video.PlayerState = {
'UNSTARTED': -1,
'ENDED': 0,
'PLAYING': 1,
'PAUSED': 2,
'BUFFERING': 3,
'CUED': 5
};
return HTML5Video;
}());
class @VideoCaptionAlpha extends SubviewAlpha
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 """
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
# """
@$('.video-wrapper').after """
<ol class="subtitles"></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""#"
@$('.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 = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').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 <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').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()
class @VideoControlAlpha extends SubviewAlpha
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""#"
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')
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
@PlayerState.UNSTARTED = -1
else if @video.videoType is 'html5'
@PlayerState = HTML5Video.PlayerState
@currentTime = 0
@el = $("#video_#{@video.id}")
bind: ->
$(@control).bind('play', @play)
.bind('pause', @pause)
if @video.videoType is 'youtube'
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
if @video.show_captions is true
$(@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 VideoControlAlpha el: @$('.video-controls')
if @video.videoType is 'youtube'
@qualityControl = new VideoQualityControlAlpha el: @$('.secondary-controls')
if @video.show_captions is true
@caption = new VideoCaptionAlpha
el: @el
youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed()
captionAssetPath: @video.caption_asset_path
unless onTouchBasedDevice()
@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'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
html5: 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
if @video.videoType is 'html5'
@player = new HTML5Video.Player @video.el,
playerVars: @playerVars,
videoSources: @video.html5Sources,
events:
onReady: @onReady
onStateChange: @onStateChange
else if @video.videoType is 'youtube'
prev_player_type = $.cookie('prev_player_type')
if prev_player_type == 'html5'
youTubeId = @video.videos['1.0']
else
youTubeId = @video.youtubeId()
@player = new YT.Player @video.id,
playerVars: @playerVars
videoId: youTubeId
events:
onReady: @onReady
onStateChange: @onStateChange
onPlaybackQualityChange: @onPlaybackQualityChange
if @video.show_captions is true
@caption.hideCaptions(@['video'].hide_captions)
addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip
position:
my: 'top right'
at: 'top center'
onReady: (event) =>
if @video.videoType is 'html5'
@player.setPlaybackRate @video.speed
unless onTouchBasedDevice()
$('.video-load-complete:first').data('video').player.play()
onStateChange: (event) =>
_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', 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]
@video.speeds = []
$.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()
when @PlayerState.PAUSED
@onPause()
when @PlayerState.ENDED
@onEnded()
onPlaybackQualityChange: (event, value) =>
quality = @player.getPlaybackQuality()
@qualityControl.onQualityChange(quality)
handlePlaybackQualityChange: (event, value) =>
@player.setPlaybackQuality(value)
onUnstarted: =>
@control.pause()
if @video.show_captions is true
@caption.pause()
onPlay: =>
@video.log 'play_video'
unless @player.interval
@player.interval = setInterval(@update, 200)
if @video.show_captions is true
@caption.play()
@control.play()
@progressSlider.play()
onPause: =>
@video.log 'pause_video'
clearInterval(@player.interval)
@player.interval = null
if @video.show_captions is true
@caption.pause()
@control.pause()
onEnded: =>
@control.pause()
if @video.show_captions is true
@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, updateCookie) =>
if @video.videoType is 'youtube'
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
@video.setSpeed newSpeed, updateCookie
if @video.videoType is 'youtube'
if @video.show_captions is true
@caption.currentSpeed = newSpeed
if @video.videoType is 'html5'
@player.setPlaybackRate newSpeed
else if @video.videoType is 'youtube'
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
if @video.videoType is 'youtube'
@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)
if @video.show_captions is true
@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')
if @video.show_captions is true
@caption.resize()
# Delegates
play: =>
@player.playVideo() if @player.playVideo
isPlaying: ->
@player.getPlayerState() == @PlayerState.PLAYING
pause: =>
@player.pauseVideo() if @player.pauseVideo
duration: ->
duration = @player.getDuration()
if isFinite(duration) is false
duration = @video.getDuration()
duration
currentSpeed: ->
@video.speed
volume: (value) ->
if value?
@player.setVolume value
else
@player.getVolume()
class @VideoProgressSliderAlpha extends SubviewAlpha
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)}")
class @VideoQualityControlAlpha extends SubviewAlpha
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
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
class @VideoSpeedControlAlpha extends SubviewAlpha
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 """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed @currentSpeed
reRender: (newSpeeds, currentSpeed) ->
@$('.video_speeds').empty()
@$('.video_speeds li').removeClass('active')
@speeds = newSpeeds
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
listItem = $('<li>').attr('data-speed', speed).html(link);
listItem.addClass('active') if speed is currentSpeed
@$('.video_speeds').prepend listItem
@$('.video_speeds a').click @changeVideoSpeed
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")
class @VideoVolumeControlAlpha extends SubviewAlpha
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""#"
@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
---
metadata:
display_name: default
data_dir: a_made_up_name
data: |
<videoalpha youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
children: []
...@@ -4,6 +4,8 @@ import logging ...@@ -4,6 +4,8 @@ import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string, resource_listdir from pkg_resources import resource_string, resource_listdir
from django.http import Http404
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
...@@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent ...@@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent
import datetime import datetime
import time import time
import datetime
import time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
import json
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from django.http import Http404
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
import datetime
import time
log = logging.getLogger(__name__)
class VideoAlphaModule(XModule):
"""
XML source example:
<videoalpha show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
url_name="lecture_21_3" display_name="S19V3: Vacancies"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</videoalpha>
"""
video_time = 0
icon_class = 'video'
js = {
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
'coffee':
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
for filename
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
if filename.endswith('.coffee')]}
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
js_module_name = "VideoAlpha"
def __init__(self, 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_streams = xmltree.get('youtube')
self.sub = xmltree.get('sub')
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.sources = {
'main': self._get_source(xmltree),
'mp4': self._get_source(xmltree, ['mp4']),
'webm': self._get_source(xmltree, ['webm']),
'ogv': self._get_source(xmltree, ['ogv']),
}
self.track = self._get_track(xmltree)
self.start_time, self.end_time = self._get_timeframe(xmltree)
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, exts=None):
"""Find the first valid source, which ends with one of `exts`."""
exts = ['mp4', 'ogv', 'avi', 'webm'] if exts is None else exts
condition = lambda src: any([src.endswith(ext) for ext in exts])
return self._get_first_external(xmltree, 'source', condition)
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag, condition=bool):
"""Will return the first 'valid' element of the given tag.
'valid' means that `condition('src' attribute) == True`
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if condition(src):
result = src
break
return result
def _get_timeframe(self, xmltree):
""" Converts '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_instance_state(self):
return json.dumps({'position': self.position})
def get_html(self):
if isinstance(modulestore(), MongoModuleStore):
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
return self.system.render_template('videoalpha.html', {
'youtube_streams': self.youtube_streams,
'id': self.location.html_id(),
'sub': self.sub,
'sources': self.sources,
'track': self.track,
'display_name': self.display_name,
# 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 = VideoAlphaModule
stores_state = True
template_dir_name = "videoalpha"
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
<!-- TODO: http://docs.jquery.com/Plugins/Validation --> <!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript"> <script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' + document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); document.location.protocol + '//www.youtube.com/iframe_api">\x3C/script>');
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
</script> </script>
% if timer_expiration_duration: % if timer_expiration_duration:
<script type="text/javascript"> <script type="text/javascript">
var timer = { var timer = {
timer_inst : null, timer_inst : null,
end_time : null, end_time : null,
...@@ -79,8 +79,8 @@ ...@@ -79,8 +79,8 @@
remaining_secs = remaining_secs % 3600; remaining_secs = remaining_secs % 3600;
var minutes = pretty_time_string(Math.floor(remaining_secs / 60)); var minutes = pretty_time_string(Math.floor(remaining_secs / 60));
remaining_secs = remaining_secs % 60; remaining_secs = remaining_secs % 60;
var seconds = pretty_time_string(Math.floor(remaining_secs)); var seconds = pretty_time_string(Math.floor(remaining_secs));
var remainingTimeString = hours + ":" + minutes + ":" + seconds; var remainingTimeString = hours + ":" + minutes + ":" + seconds;
return remainingTimeString; return remainingTimeString;
}, },
...@@ -100,11 +100,11 @@ ...@@ -100,11 +100,11 @@
end : function(self) { end : function(self) {
clearInterval(self.timer_inst); clearInterval(self.timer_inst);
// redirect to specified URL: // redirect to specified URL:
window.location = "${time_expired_redirect_url}"; window.location = "${time_expired_redirect_url}";
} }
} }
// start timer right away: // start timer right away:
timer.start(); timer.start();
</script> </script>
% endif % endif
......
% if display_name is not UNDEFINED and display_name is not None:
<h2> ${display_name} </h2>
% endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
<div id="stub_out_video_for_testing"></div>
%else:
<div
id="video_${id}"
class="video"
data-streams="${youtube_streams}"
${'data-sub="{}"'.format(sub) if sub else ''}
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="${id}"></div>
</section>
<section class="video-controls"></section>
</article>
</div>
</div>
%endif
% if sources.get('main'):
<div class="video-sources">
<p>Download video <a href="${sources.get('main')}">here</a>.</p>
</div>
% endif
% if track:
<div class="video-tracks">
<p>Download subtitles <a href="${track}">here</a>.</p>
</div>
% endif
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