Merge pull request #1409 from MITx/feature/valera/video_alpha
Feature/valera/video alpha
This commit is contained in:
@@ -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",
|
||||
|
||||
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
*.js
|
||||
|
||||
# Please do not ignore *.js files. Some xmodules are written in JS.
|
||||
|
||||
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
class @SubviewAlpha
|
||||
constructor: (options) ->
|
||||
$.each options, (key, value) =>
|
||||
@[key] = value
|
||||
@initialize()
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
initialize: ->
|
||||
render: ->
|
||||
bind: ->
|
||||
@@ -0,0 +1,285 @@
|
||||
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;
|
||||
}());
|
||||
@@ -0,0 +1,152 @@
|
||||
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()
|
||||
@@ -0,0 +1,35 @@
|
||||
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')
|
||||
@@ -0,0 +1,283 @@
|
||||
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()
|
||||
@@ -0,0 +1,49 @@
|
||||
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)}")
|
||||
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
@@ -0,0 +1,53 @@
|
||||
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")
|
||||
@@ -0,0 +1,40 @@
|
||||
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
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
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
|
||||
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__)
|
||||
|
||||
|
||||
|
||||
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
@@ -0,0 +1,155 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user