diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/bind.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/bind.js new file mode 100644 index 0000000000..a5bdf66af0 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/bind.js @@ -0,0 +1,19 @@ +(function (requirejs, require, define) { + +// Bind module. +define( +'videoalpha/display/bind.js', +[], +function () { + + // bind() function. + return function (fn, me) { + return function () { + return fn.apply(me, arguments); + }; + }; + +}); + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); +// var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 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 c3cc462ab8..0b9ff0148a 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,4 +1,10 @@ -this.HTML5Video = (function () { +(function (requirejs, require, define) { + +// HTML5Video module. +define( +'videoalpha/display/html5_video.js', +[], +function () { var HTML5Video; HTML5Video = {}; @@ -48,10 +54,6 @@ this.HTML5Video = (function () { }; Player.prototype.getDuration = function () { - if (isFinite(this.video.duration) === false) { - return 0; - } - return this.video.duration; }; @@ -290,5 +292,8 @@ this.HTML5Video = (function () { 'CUED': 5 }; + // HTML5Video object - what this module exports. return HTML5Video; -}()); +}) + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/initialize.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/initialize.js new file mode 100644 index 0000000000..a87088d147 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/initialize.js @@ -0,0 +1,232 @@ +(function (requirejs, require, define) { + +// Initialize module. +define( +'videoalpha/display/initialize.js', +['videoalpha/display/bind.js'], +function (bind) { + + // Initialize() function - what this module "exports". + return function (state, element) { + // Functions which will be accessible via 'state' object. + makeFunctionsPublic(state); + + // The parent element of the video, and the ID. + state.el = $(element).find('.video'); + state.id = state.el.attr('id').replace(/video_/, ''); + + // We store all settings passed to us by the server in one place. These are "read only", so don't + // modify them. All variable content lives in 'state' object. + state.config = { + 'element': element, + + 'start': state.el.data('start'), + 'end': state.el.data('end'), + + 'caption_data_dir': state.el.data('caption-data-dir'), + 'caption_asset_path': state.el.data('caption-asset-path'), + 'show_captions': (state.el.data('show-captions').toString() === 'true'), + + 'youtubeStreams': state.el.data('streams'), + + 'sub': state.el.data('sub'), + 'mp4Source': state.el.data('mp4-source'), + 'webmSource': state.el.data('webm-source'), + 'oggSource': state.el.data('ogg-source') + }; + + // Try to parse YouTube stream ID's. If + if (parseYoutubeStreams(state, state.config.youtubeStreams) === true) { + state.videoType = 'youtube'; + + fetchMetadata(state); + parseSpeed(state); + } + + // If we do not have YouTube ID's, try parsing HTML5 video sources. + else { + state.videoType = 'html5'; + + parseVideoSources( + state, + state.config.mp4Source, + state.config.webmSource, + state.config.oggSource + ); + + if ((typeof state.config.sub !== 'string') || (state.config.sub.length === 0)) { + state.config.sub = ''; + state.config.show_captions = false; + } + + state.speeds = ['0.75', '1.0', '1.25', '1.50']; + state.videos = { + '0.75': state.config.sub, + '1.0': state.config.sub, + '1.25': state.config.sub, + '1.5': state.config.sub + }; + + state.setSpeed($.cookie('video_speed')); + } + + // TODO: Check after refactoring whether this can be removed. + state.el.addClass('video-load-complete'); + + // Configure displaying of captions. + // + // Option + // + // state.config.show_captions = true | false + // + // defines whether to turn off/on the captions altogether. User will not have the ability to turn them on/off. + // + // Option + // + // state.hide_captions = true | false + // + // represents the user's choice of having the subtitles shown or hidden. This choice is stored in cookies. + if (state.config.show_captions === true) { + state.hide_captions = ($.cookie('hide_captions') === 'true'); + } else { + state.hide_captions = true; + + $.cookie('hide_captions', state.hide_captions, { + expires: 3650, + path: '/' + }); + + state.el.addClass('closed'); + } + + // Launch embedding of actual video content, or set it up so that it will be done as soon as the + // appropriate video player (YouTube or stand alone HTML5) is loaded, and can handle embedding. + if ( + ((state.videoType === 'youtube') && (window.YT) && (window.YT.Player)) || + ((state.videoType === 'html5') && (window.HTML5Video) && (window.HTML5Video.Player)) + ) { + embed(state); + } else { + if (state.videoType === 'youtube') { + window.onYouTubePlayerAPIReady = function() { + embed(state); + }; + } else if (state.videoType === 'html5') { + window.onHTML5PlayerAPIReady = function() { + embed(state); + }; + } + } + }; + + // Private functions start here. + + function makeFunctionsPublic(state) { + state.setSpeed = bind(setSpeed, state); + state.youtubeId = bind(youtubeId, state); + state.getDuration = bind(getDuration, state); + state.log = bind(log, state); + } + + function parseYoutubeStreams(state, youtubeStreams) { + if ((typeof youtubeStreams !== 'string') || (youtubeStreams.length === 0)) { + return false; + } + + state.videos = {}; + + $.each(youtubeStreams.split(/,/), function(index, video) { + var speed; + + video = video.split(/:/); + speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0"); + + state.videos[speed] = video[1]; + }); + + return true; + } + + function parseVideoSources(state, mp4Source, webmSource, oggSource) { + state.html5Sources = { 'mp4': null, 'webm': null, 'ogg': null }; + + if ((typeof mp4Source === 'string') && (mp4Source.length > 0)) { + state.html5Sources.mp4 = mp4Source; + } + if ((typeof webmSource === 'string') && (webmSource.length > 0)) { + state.html5Sources.webm = webmSource; + } + if ((typeof oggSource === 'string') && (oggSource.length > 0)) { + state.html5Sources.ogg = oggSource; + } + } + + function fetchMetadata(state) { + state.metadata = {}; + + $.each(state.videos, function (speed, url) { + $.get('https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc', (function(data) { + state.metadata[data.data.id] = data.data; + }), 'jsonp'); + }); + } + + function parseSpeed(state) { + state.speeds = ($.map(state.videos, function(url, speed) { + return speed; + })).sort(); + + state.setSpeed($.cookie('video_speed')); + } + + function embed(state) { } + + // Public functions start here. + // These are available via the 'state' object. Their context ('this' keyword) is the 'state' object. + // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). + + function setSpeed(newSpeed) { + if (this.speeds.indexOf(newSpeed) !== -1) { + this.speed = newSpeed; + + $.cookie('video_speed', '' + newSpeed, { + expires: 3650, + path: '/' + }); + } else { + this.speed = '1.0'; + } + } + + function youtubeId(speed) { + return this.videos[speed || this.speed]; + } + + function getDuration() { + return this.metadata[this.youtubeId()].duration; + } + + function log(eventName) { + var logInfo; + + logInfo = { + 'id': this.id, + 'code': this.youtubeId(), + 'currentTime': this.player.currentTime, + 'speed': this.speed + }; + + if (this.videoType === 'youtube') { + logInfo.code = this.youtubeId(); + } else { + if (this.videoType === 'html5') { + logInfo.code = 'html5'; + } + } + + Logger.log(eventName, logInfo); + } + +}); + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.js new file mode 100644 index 0000000000..bc8b76867b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.js @@ -0,0 +1,413 @@ +(function (requirejs, require, define) { + +// VideoPlayer module. +define( +'videoalpha/display/video_player.js', +['videoalpha/display/html5_video.js'], +function (HTML5Video) { + return function (state) { + console.log('HTML5Video object:'); + console.log(HTML5Video); + }; +}); + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); + + + +/* + +// Generated by CoffeeScript 1.4.0 +(function() { + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + this.VideoPlayerAlpha = (function(_super) { + + __extends(VideoPlayerAlpha, _super); + + function VideoPlayerAlpha() { + this.pause = __bind(this.pause, this); + + this.play = __bind(this.play, this); + + this.toggleFullScreen = __bind(this.toggleFullScreen, this); + + this.update = __bind(this.update, this); + + this.onVolumeChange = __bind(this.onVolumeChange, this); + + this.onSpeedChange = __bind(this.onSpeedChange, this); + + this.onSeek = __bind(this.onSeek, this); + + this.onEnded = __bind(this.onEnded, this); + + this.onPause = __bind(this.onPause, this); + + this.onPlay = __bind(this.onPlay, this); + + this.onUnstarted = __bind(this.onUnstarted, this); + + this.handlePlaybackQualityChange = __bind(this.handlePlaybackQualityChange, this); + + this.onPlaybackQualityChange = __bind(this.onPlaybackQualityChange, this); + + this.onStateChange = __bind(this.onStateChange, this); + + this.onReady = __bind(this.onReady, this); + + this.bindExitFullScreen = __bind(this.bindExitFullScreen, this); + return VideoPlayerAlpha.__super__.constructor.apply(this, arguments); + } + + VideoPlayerAlpha.prototype.initialize = function() { + if (window.OldVideoPlayerAlpha && window.OldVideoPlayerAlpha.onPause) { + window.OldVideoPlayerAlpha.onPause(); + } + window.OldVideoPlayerAlpha = this; + if (this.video.videoType === 'youtube') { + this.PlayerState = YT.PlayerState; + this.PlayerState.UNSTARTED = -1; + } else if (this.video.videoType === 'html5') { + this.PlayerState = HTML5Video.PlayerState; + } + this.currentTime = 0; + return this.el = $("#video_" + this.video.id); + }; + + VideoPlayerAlpha.prototype.bind = function() { + $(this.control).bind('play', this.play).bind('pause', this.pause); + if (this.video.videoType === 'youtube') { + $(this.qualityControl).bind('changeQuality', this.handlePlaybackQualityChange); + } + if (this.video.show_captions === true) { + $(this.caption).bind('seek', this.onSeek); + } + $(this.speedControl).bind('speedChange', this.onSpeedChange); + $(this.progressSlider).bind('seek', this.onSeek); + if (this.volumeControl) { + $(this.volumeControl).bind('volumeChange', this.onVolumeChange); + } + $(document).keyup(this.bindExitFullScreen); + this.$('.add-fullscreen').click(this.toggleFullScreen); + if (!onTouchBasedDevice()) { + return this.addToolTip(); + } + }; + + VideoPlayerAlpha.prototype.bindExitFullScreen = function(event) { + if (this.el.hasClass('fullscreen') && event.keyCode === 27) { + return this.toggleFullScreen(event); + } + }; + + VideoPlayerAlpha.prototype.render = function() { + var prev_player_type, youTubeId; + this.control = new VideoControlAlpha({ + el: this.$('.video-controls') + }); + if (this.video.videoType === 'youtube') { + this.qualityControl = new VideoQualityControlAlpha({ + el: this.$('.secondary-controls') + }); + } + if (this.video.show_captions === true) { + this.caption = new VideoCaptionAlpha({ + el: this.el, + youtubeId: this.video.youtubeId('1.0'), + currentSpeed: this.currentSpeed(), + captionAssetPath: this.video.caption_asset_path + }); + } + if (!onTouchBasedDevice()) { + this.volumeControl = new VideoVolumeControlAlpha({ + el: this.$('.secondary-controls') + }); + } + this.speedControl = new VideoSpeedControlAlpha({ + el: this.$('.secondary-controls'), + speeds: this.video.speeds, + currentSpeed: this.currentSpeed() + }); + this.progressSlider = new VideoProgressSliderAlpha({ + el: this.$('.slider') + }); + this.playerVars = { + controls: 0, + wmode: 'transparent', + rel: 0, + showinfo: 0, + enablejsapi: 1, + modestbranding: 1 + }; + if (this.video.start) { + this.playerVars.start = this.video.start; + this.playerVars.wmode = 'window'; + } + if (this.video.end) { + this.playerVars.end = this.video.end; + } + if (this.video.videoType === 'html5') { + this.player = new HTML5Video.Player(this.video.el, { + playerVars: this.playerVars, + videoSources: this.video.html5Sources, + events: { + onReady: this.onReady, + onStateChange: this.onStateChange + } + }); + } else if (this.video.videoType === 'youtube') { + prev_player_type = $.cookie('prev_player_type'); + if (prev_player_type === 'html5') { + youTubeId = this.video.videos['1.0']; + } else { + youTubeId = this.video.youtubeId(); + } + this.player = new YT.Player(this.video.id, { + playerVars: this.playerVars, + videoId: youTubeId, + events: { + onReady: this.onReady, + onStateChange: this.onStateChange, + onPlaybackQualityChange: this.onPlaybackQualityChange + } + }); + } + if (this.video.show_captions === true) { + return this.caption.hideCaptions(this['video'].hide_captions); + } + }; + + VideoPlayerAlpha.prototype.addToolTip = function() { + return this.$('.add-fullscreen, .hide-subtitles').qtip({ + position: { + my: 'top right', + at: 'top center' + } + }); + }; + + VideoPlayerAlpha.prototype.onReady = function(event) { + if (this.video.videoType === 'html5') { + this.player.setPlaybackRate(this.video.speed); + } + if (!onTouchBasedDevice()) { + return $('.video-load-complete:first').data('video').player.play(); + } + }; + + VideoPlayerAlpha.prototype.onStateChange = function(event) { + var availableSpeeds, baseSpeedSubs, prev_player_type, _this; + _this = this; + switch (event.data) { + case this.PlayerState.UNSTARTED: + if (this.video.videoType === "youtube") { + availableSpeeds = this.player.getAvailablePlaybackRates(); + prev_player_type = $.cookie('prev_player_type'); + if (availableSpeeds.length > 1) { + if (prev_player_type === 'youtube') { + $.cookie('prev_player_type', 'html5', { + expires: 3650, + path: '/' + }); + this.onSpeedChange(null, '1.0'); + } else if (prev_player_type !== 'html5') { + $.cookie('prev_player_type', 'html5', { + expires: 3650, + path: '/' + }); + } + baseSpeedSubs = this.video.videos["1.0"]; + $.each(this.video.videos, function(index, value) { + return delete _this.video.videos[index]; + }); + this.video.speeds = []; + $.each(availableSpeeds, function(index, value) { + _this.video.videos[value.toFixed(2).replace(/\.00$/, ".0")] = baseSpeedSubs; + return _this.video.speeds.push(value.toFixed(2).replace(/\.00$/, ".0")); + }); + this.speedControl.reRender(this.video.speeds, this.video.speed); + this.video.videoType = 'html5'; + this.video.setSpeed($.cookie('video_speed')); + this.player.setPlaybackRate(this.video.speed); + } else { + if (prev_player_type !== 'youtube') { + $.cookie('prev_player_type', 'youtube', { + expires: 3650, + path: '/' + }); + } + } + } + return this.onUnstarted(); + case this.PlayerState.PLAYING: + return this.onPlay(); + case this.PlayerState.PAUSED: + return this.onPause(); + case this.PlayerState.ENDED: + return this.onEnded(); + } + }; + + VideoPlayerAlpha.prototype.onPlaybackQualityChange = function(event, value) { + var quality; + quality = this.player.getPlaybackQuality(); + return this.qualityControl.onQualityChange(quality); + }; + + VideoPlayerAlpha.prototype.handlePlaybackQualityChange = function(event, value) { + return this.player.setPlaybackQuality(value); + }; + + VideoPlayerAlpha.prototype.onUnstarted = function() { + this.control.pause(); + if (this.video.show_captions === true) { + return this.caption.pause(); + } + }; + + VideoPlayerAlpha.prototype.onPlay = function() { + this.video.log('play_video'); + if (!this.player.interval) { + this.player.interval = setInterval(this.update, 200); + } + if (this.video.show_captions === true) { + this.caption.play(); + } + this.control.play(); + return this.progressSlider.play(); + }; + + VideoPlayerAlpha.prototype.onPause = function() { + this.video.log('pause_video'); + clearInterval(this.player.interval); + this.player.interval = null; + if (this.video.show_captions === true) { + this.caption.pause(); + } + return this.control.pause(); + }; + + VideoPlayerAlpha.prototype.onEnded = function() { + this.control.pause(); + if (this.video.show_captions === true) { + return this.caption.pause(); + } + }; + + VideoPlayerAlpha.prototype.onSeek = function(event, time) { + this.player.seekTo(time, true); + if (this.isPlaying()) { + clearInterval(this.player.interval); + this.player.interval = setInterval(this.update, 200); + } else { + this.currentTime = time; + } + return this.updatePlayTime(time); + }; + + VideoPlayerAlpha.prototype.onSpeedChange = function(event, newSpeed) { + if (this.video.videoType === 'youtube') { + this.currentTime = Time.convert(this.currentTime, parseFloat(this.currentSpeed()), newSpeed); + } + newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0'); + this.video.setSpeed(newSpeed); + if (this.video.videoType === 'youtube') { + if (this.video.show_captions === true) { + this.caption.currentSpeed = newSpeed; + } + } + if (this.video.videoType === 'html5') { + this.player.setPlaybackRate(newSpeed); + } else if (this.video.videoType === 'youtube') { + if (this.isPlaying()) { + this.player.loadVideoById(this.video.youtubeId(), this.currentTime); + } else { + this.player.cueVideoById(this.video.youtubeId(), this.currentTime); + } + } + if (this.video.videoType === 'youtube') { + return this.updatePlayTime(this.currentTime); + } + }; + + VideoPlayerAlpha.prototype.onVolumeChange = function(event, volume) { + return this.player.setVolume(volume); + }; + + VideoPlayerAlpha.prototype.update = function() { + if (this.currentTime = this.player.getCurrentTime()) { + return this.updatePlayTime(this.currentTime); + } + }; + + VideoPlayerAlpha.prototype.updatePlayTime = function(time) { + var progress; + progress = Time.format(time) + ' / ' + Time.format(this.duration()); + this.$(".vidtime").html(progress); + if (this.video.show_captions === true) { + this.caption.updatePlayTime(time); + } + return this.progressSlider.updatePlayTime(time, this.duration()); + }; + + VideoPlayerAlpha.prototype.toggleFullScreen = function(event) { + event.preventDefault(); + if (this.el.hasClass('fullscreen')) { + this.$('.add-fullscreen').attr('title', 'Fill browser'); + this.el.removeClass('fullscreen'); + } else { + this.el.addClass('fullscreen'); + this.$('.add-fullscreen').attr('title', 'Exit fill browser'); + } + if (this.video.show_captions === true) { + return this.caption.resize(); + } + }; + + VideoPlayerAlpha.prototype.play = function() { + if (this.player.playVideo) { + return this.player.playVideo(); + } + }; + + VideoPlayerAlpha.prototype.isPlaying = function() { + return this.player.getPlayerState() === this.PlayerState.PLAYING; + }; + + VideoPlayerAlpha.prototype.pause = function() { + if (this.player.pauseVideo) { + return this.player.pauseVideo(); + } + }; + + VideoPlayerAlpha.prototype.duration = function() { + if (this.video.videoType === "youtube") { + return this.video.getDuration(); + } else if (this.video.videoType === "html5") { + return this.player.getDuration(); + } + return 0; + }; + + VideoPlayerAlpha.prototype.currentSpeed = function() { + return this.video.speed; + }; + + VideoPlayerAlpha.prototype.volume = function(value) { + if (value != null) { + return this.player.setVolume(value); + } else { + return this.player.getVolume(); + } + }; + + return VideoPlayerAlpha; + + })(SubviewAlpha); + +}).call(this); + +*/ diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/main.js b/common/lib/xmodule/xmodule/js/src/videoalpha/main.js new file mode 100644 index 0000000000..582c151afb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/main.js @@ -0,0 +1,20 @@ +(function (requirejs, require, define) { + +// Main module +require( +['videoalpha/display/initialize.js', 'videoalpha/display/video_player.js'], +function (Initialize, VideoPlayer) { + window.VideoAlpha = function (element) { + var state; + + state = {}; + + new Initialize(state, element); + new VideoPlayer(state); + + console.log('Finished constructing "state" object. state = '); + console.log(state); + }; +}); + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index d8ed8949f1..20bab318a9 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -68,14 +68,14 @@ class VideoAlphaModule(VideoAlphaFields, XModule): icon_class = 'video' js = { - 'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')], - 'coffee': - [resource_string(__name__, 'js/src/time.coffee'), - resource_string(__name__, 'js/src/videoalpha/display.coffee')] + - [resource_string(__name__, 'js/src/videoalpha/display/' + filename) - for filename - in sorted(resource_listdir(__name__, 'js/src/videoalpha/display')) - if filename.endswith('.coffee')]} + 'js': [ + resource_string(__name__, 'js/src/videoalpha/display/bind.js'), + resource_string(__name__, 'js/src/videoalpha/display/initialize.js'), + resource_string(__name__, 'js/src/videoalpha/display/html5_video.js'), + resource_string(__name__, 'js/src/videoalpha/display/video_player.js'), + resource_string(__name__, 'js/src/videoalpha/main.js') + ] + } css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]} js_module_name = "VideoAlpha"