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

      ${display_name}

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

      Download video here.

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

      Download subtitles here.

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

      ${display_name}

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

      ${display_name}

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

      ${display_name}

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

      Download video here.

      +

      Download video here.

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

      Download video here.

      +

      Download video here.

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

          ${display_name}

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

          Download video here.

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

          Download subtitles here.

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

          ${display_name}

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

          ${display_name}

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

          ${display_name}

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

          Download video here.

          +

          Download video here.

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

          Download video here.

          +

          Download video here.

          % endif From d55b818bb2bb518402250b252cab06c0726824b3 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 14 Feb 2013 11:25:36 +0200 Subject: [PATCH 51/52] Updated YouTube JS API file to the new one. --- lms/templates/courseware/courseware.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index fcbc83d815..33dc9562a7 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -32,7 +32,7 @@ % if timer_expiration_duration: - % endif From 93000af79d09345bfec4ae2c5dae249f0dd27a76 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 14 Feb 2013 11:30:21 +0200 Subject: [PATCH 52/52] fix problem with mutable default value in `VideoAlphaModule._get_source` --- .../lib/xmodule/xmodule/videoalpha_module.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 6910728d8c..c892e7b1b8 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -69,10 +69,10 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree, extensions=['mp4', 'ogv', 'avi', 'webm']): - """Find the first valid source, which ends with one of - `extensions`.""" - condition = lambda src: any([src.endswith(ext) for ext in extensions]) + def _get_source(self, xmltree, ext=None): + """Find the first valid source, which ends with one of `ext`.""" + ext = ['mp4', 'ogv', 'avi', 'webm'] if ext is None else ext + condition = lambda src: any([src.endswith(ext) for ext in ext]) return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): @@ -110,11 +110,10 @@ class VideoAlphaModule(XModule): 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. - ''' + """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': @@ -124,7 +123,6 @@ class VideoAlphaModule(XModule): raise Http404() def get_instance_state(self): - #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) def get_html(self):