Commit 009b8475 by Valera Rozuvan Committed by Vasyl Nakvasiuk

Tracing bug in Firefox whereby the HTML5 native video freezes. Turns out that…

Tracing bug in Firefox whereby the HTML5 native video freezes. Turns out that setting currentTime property tiggers a canplay event. And this becomes cyclic because the callback for canplay event updates the currentTime property.
Video alpha 2. Work in progress.
parent e8d07d53
......@@ -3,8 +3,8 @@
// Initialize module.
define(
'videoalpha/display/initialize.js',
['videoalpha/display/bind.js'],
function (bind) {
['videoalpha/display/bind.js', 'videoalpha/display/video_player.js'],
function (bind, VideoPlayer) {
// Initialize() function - what this module "exports".
return function (state, element) {
......@@ -99,6 +99,23 @@ function (bind) {
state.el.addClass('closed');
}
// By default we will be forcing HTML5 player mode. Only in the case when, after initializtion, we will
// get one available playback rate, we will change to Flash player mode. There is a need to store this
// setting in cookies because otherwise we will have to change from HTML5 to Flash on every page load
// in a browser that doesn't fully support HTML5. When we have this setting in cookies, we can select
// the proper mode from the start (not having to change mode later on).
(function (currentPlayerMode) {
if ((currentPlayerMode !== 'html5') && (currentPlayerMode !== 'flash')) {
$.cookie('current_player_mode', 'html5', {
expires: 3650,
path: '/'
});
state.currentPlayerMode = 'html5';
} else {
state.currentPlayerMode = currentPlayerMode;
}
}($.cookie('current_player_mode')));
// 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 (
......@@ -139,7 +156,7 @@ function (bind) {
var speed;
video = video.split(/:/);
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0");
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, '.0');
state.videos[speed] = video[1];
});
......@@ -179,22 +196,26 @@ function (bind) {
state.setSpeed($.cookie('video_speed'));
}
function embed(state) { }
function embed(state) {
VideoPlayer(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) {
function setSpeed(newSpeed, updateCookie) {
if (this.speeds.indexOf(newSpeed) !== -1) {
this.speed = newSpeed;
} else {
this.speed = '1.0';
}
$.cookie('video_speed', '' + newSpeed, {
if (updateCookie !== false) {
$.cookie('video_speed', this.speed, {
expires: 3650,
path: '/'
});
} else {
this.speed = '1.0';
}
}
......
(function (requirejs, require, define) {
// VideoPlayer module.
define(
'videoalpha/display/video_control.js',
['videoalpha/display/bind.js'],
function (bind) {
// VideoControl() function - what this module "exports".
return function (state) {
state.videoControl = {};
// Functions which will be accessible via 'state' object.
makeFunctionsPublic(state);
// TODO.
console.log('We are inside VideoControl() function.');
renderElements(state);
bindHandlers();
};
// Private functions start here.
function makeFunctionsPublic(state) {
state.videoControl.play = bind(play, state);
state.videoControl.pause = bind(pause, state);
state.videoControl.togglePlayback = bind(togglePlayback, state);
}
function renderElements(state) {
var el;
el = $(
'<div class="slider"></div>' +
'<div>' +
'<ul class="vcr">' +
'<li><a class="video_control" href="#"></a></li>' +
'<li><div class="vidtime">0:00 / 0:00</div></li>' +
'</ul>' +
'<div class="secondary-controls">' +
'<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>' +
'</div>' +
'</div>'
);
state.videoControl.el = state.el.find('.video-controls');
state.videoControl.el.append(el);
state.videoControl.playPauseEl = state.videoControl.el.find('.video_control');
if (!onTouchBasedDevice()) {
state.videoControl.playPauseEl.addClass('play').html('Play');
}
}
function bindHandlers(state) {
state.videoControl.playPauseEl.click(state.videoControl.togglePlayback);
}
// 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 play() {
this.videoControl.playPauseEl.removeClass('play').addClass('pause').html('Pause');
this.videoControl.state = 'playing';
}
function pause() {
this.videoControl.playPauseEl.removeClass('pause').addClass('play').html('Play');
this.videoControl.state = 'paused';
}
function togglePlayback(event) {
event.preventDefault();
console.log('We are in togglePlayback() function. this =');
console.log(this);
/*
if (this.$('.video_control').hasClass('play')) {
$(this).trigger('play');
} else if (this.$('.video_control').hasClass('pause')) {
$(this).trigger('pause');
}
*/
}
});
}(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.VideoControlAlpha = (function(_super) {
__extends(VideoControlAlpha, _super);
function VideoControlAlpha() {
this.togglePlayback = __bind(this.togglePlayback, this);
return VideoControlAlpha.__super__.constructor.apply(this, arguments);
}
VideoControlAlpha.prototype.bind = function() {
return this.$('.video_control').click(this.togglePlayback);
};
VideoControlAlpha.prototype.render = function() {
this.el.append("<div class=\"slider\"></div>\n<div>\n <ul class=\"vcr\">\n <li><a class=\"video_control\" href=\"#\"></a></li>\n <li>\n <div class=\"vidtime\">0:00 / 0:00</div>\n </li>\n </ul>\n <div class=\"secondary-controls\">\n <a href=\"#\" class=\"add-fullscreen\" title=\"Fill browser\">Fill Browser</a>\n </div>\n</div>");
if (!onTouchBasedDevice()) {
return this.$('.video_control').addClass('play').html('Play');
}
};
VideoControlAlpha.prototype.play = function() {
return this.$('.video_control').removeClass('play').addClass('pause').html('Pause');
};
VideoControlAlpha.prototype.pause = function() {
return this.$('.video_control').removeClass('pause').addClass('play').html('Play');
};
VideoControlAlpha.prototype.togglePlayback = function(event) {
event.preventDefault();
if (this.$('.video_control').hasClass('play')) {
return $(this).trigger('play');
} else if (this.$('.video_control').hasClass('pause')) {
return $(this).trigger('pause');
}
};
return VideoControlAlpha;
})(SubviewAlpha);
}).call(this);
*/
......@@ -3,79 +3,240 @@
// VideoPlayer module.
define(
'videoalpha/display/video_player.js',
['videoalpha/display/html5_video.js'],
function (HTML5Video) {
['videoalpha/display/html5_video.js', 'videoalpha/display/bind.js'],
function (HTML5Video, bind) {
// VideoPlayer() function - what this module "exports".
return function (state) {
console.log('HTML5Video object:');
console.log(HTML5Video);
state.videoPlayer = {};
// Functions which will be accessible via 'state' object.
makeFunctionsPublic(state);
if (state.videoType === 'youtube') {
state.videoPlayer.PlayerState = YT.PlayerState;
state.videoPlayer.PlayerState.UNSTARTED = -1;
} else { // if (state.videoType === 'html5') {
state.videoPlayer.PlayerState = HTML5Video.PlayerState;
}
state.videoPlayer.currentTime = 0;
renderElements(state);
bindHandlers();
};
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
// Private functions start here.
function makeFunctionsPublic(state) {
state.videoPlayer.pause = bind(pause, state);
state.videoPlayer.play = bind(play, state);
state.videoPlayer.toggleFullScreen = bind(toggleFullScreen, state);
state.videoPlayer.update = bind(update, state);
state.videoPlayer.onVolumeChange = bind(onVolumeChange, state);
state.videoPlayer.onSpeedChange = bind(onSpeedChange, state);
state.videoPlayer.onSeek = bind(onSeek, state);
state.videoPlayer.onEnded = bind(onEnded, state);
state.videoPlayer.onPause = bind(onPause, state);
state.videoPlayer.onPlay = bind(onPlay, state);
state.videoPlayer.onUnstarted = bind(onUnstarted, state);
state.videoPlayer.handlePlaybackQualityChange = bind(handlePlaybackQualityChange, state);
state.videoPlayer.onPlaybackQualityChange = bind(onPlaybackQualityChange, state);
state.videoPlayer.onStateChange = bind(onStateChange, state);
state.videoPlayer.onReady = bind(onReady, state);
state.videoPlayer.bindExitFullScreen = bind(bindExitFullScreen, state);
}
function renderElements(state) {
var youTubeId;
state.videoPlayer.playerVars = {
'controls': 0,
'wmode': 'transparent',
'rel': 0,
'showinfo': 0,
'enablejsapi': 1,
'modestbranding': 1
};
/*
if (state.currentPlayerMode !== 'flash') {
state.videoPlayer.playerVars.html5 = 1;
}
// 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; };
if (state.config.start) {
state.videoPlayer.playerVars.start = state.config.start;
state.videoPlayer.playerVars.wmode = 'window';
}
if (state.config.end) {
state.videoPlayer.playerVars.end = state.config.end;
}
this.VideoPlayerAlpha = (function(_super) {
if (state.videoType === 'html5') {
state.videoPlayer.player = new HTML5Video.Player(state.el, {
'playerVars': state.videoPlayer.playerVars,
'videoSources': state.html5Sources,
'events': {
'onReady': state.videoPlayer.onReady,
'onStateChange': state.videoPlayer.onStateChange
}
});
} else if (state.videoType === 'youtube') {
if (state.currentPlayerMode === 'flash') {
youTubeId = state.youtubeId();
} else {
youTubeId = state.youtubeId('1.0');
}
state.videoPlayer.player = new YT.Player(state.id, {
'playerVars': state.videoPlayer.playerVars,
'videoId': youTubeId,
'events': {
'onReady': state.videoPlayer.onReady,
'onStateChange': state.videoPlayer.onStateChange,
'onPlaybackQualityChange': state.videoPlayer.onPlaybackQualityChange
}
});
}
}
__extends(VideoPlayerAlpha, _super);
function bindHandlers() {
function VideoPlayerAlpha() {
this.pause = __bind(this.pause, this);
}
this.play = __bind(this.play, this);
function reinitAsFlash(state) {
state.videoPlayer.player.destroy();
this.toggleFullScreen = __bind(this.toggleFullScreen, this);
$.cookie('current_player_mode', 'flash', {
expires: 3650,
path: '/'
});
state.currentPlayerMode = 'flash';
this.update = __bind(this.update, this);
delete state.videoPlayer.playerVars.html5;
this.onVolumeChange = __bind(this.onVolumeChange, this);
state.videoPlayer.player = new YT.Player(state.id, {
'playerVars': state.videoPlayer.playerVars,
'videoId': state.youtubeId(),
'events': {
'onReady': state.videoPlayer.onReady,
'onStateChange': state.videoPlayer.onStateChange,
'onPlaybackQualityChange': state.videoPlayer.onPlaybackQualityChange
}
});
}
this.onSpeedChange = __bind(this.onSpeedChange, this);
// 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().
this.onSeek = __bind(this.onSeek, this);
function pause() { }
this.onEnded = __bind(this.onEnded, this);
function play() {
if (this.videoPlayer.player.playVideo) {
this.videoPlayer.player.playVideo();
}
this.onPause = __bind(this.onPause, this);
console.log('state is:');
console.log(this);
}
this.onPlay = __bind(this.onPlay, this);
function toggleFullScreen() { }
this.onUnstarted = __bind(this.onUnstarted, this);
function update() { }
this.handlePlaybackQualityChange = __bind(this.handlePlaybackQualityChange, this);
function onVolumeChange() { }
this.onPlaybackQualityChange = __bind(this.onPlaybackQualityChange, this);
function onSpeedChange() { }
this.onStateChange = __bind(this.onStateChange, this);
function onSeek() { }
this.onReady = __bind(this.onReady, this);
function onEnded() {
console.log('this.videoPlayer.PlayerState.ENDED');
}
this.bindExitFullScreen = __bind(this.bindExitFullScreen, this);
return VideoPlayerAlpha.__super__.constructor.apply(this, arguments);
function onPause() {
console.log('this.videoPlayer.PlayerState.PAUSED');
}
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);
};
function onPlay() {
console.log('this.videoPlayer.PlayerState.PLAYING');
}
function onUnstarted() {
console.log('this.videoPlayer.PlayerState.UNSTARTED');
}
function handlePlaybackQualityChange() { }
function onPlaybackQualityChange() { }
function onReady() {
var availablePlaybackRates, baseSpeedSubs, _this;
console.log('We are in ready function.');
availablePlaybackRates = this.videoPlayer.player.getAvailablePlaybackRates();
if ((this.currentPlayerMode === 'html5') && (this.videoType === 'youtube')) {
if (availablePlaybackRates.length === 1) {
console.log('We are playing YouTube video in HTML5 mode but have only one speed. Will reload in Flash mode.');
reinitAsFlash(this);
return;
} else if (availablePlaybackRates.length > 1) {
// We need to synchronize available frame rates with the ones that the user specified.
console.log('We are a YouTube video in HTML5 player mode.');
baseSpeedSubs = this.videos['1.0'];
_this = this;
$.each(this.videos, function(index, value) {
delete _this.videos[index];
});
this.speeds = [];
$.each(availablePlaybackRates, function(index, value) {
_this.videos[value.toFixed(2).replace(/\.00$/, '.0')] = baseSpeedSubs;
_this.speeds.push(value.toFixed(2).replace(/\.00$/, '.0'));
});
this.setSpeed($.cookie('video_speed'));
}
}
if (this.currentPlayerMode === 'html5') {
this.videoPlayer.player.setPlaybackRate(this.speed);
}
if (!onTouchBasedDevice()) {
this.videoPlayer.play();
}
}
function onStateChange() {
console.log('function onStateChange()');
}
function onStateChange(event) {
switch (event.data) {
case this.videoPlayer.PlayerState.UNSTARTED:
this.videoPlayer.onUnstarted();
break;
case this.videoPlayer.PlayerState.PLAYING:
this.videoPlayer.onPlay();
break;
case this.videoPlayer.PlayerState.PAUSED:
this.videoPlayer.onPause();
break;
case this.videoPlayer.PlayerState.ENDED:
this.videoPlayer.onEnded();
break;
}
}
function bindExitFullScreen() { }
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
/*
VideoPlayerAlpha.prototype.bind = function() {
$(this.control).bind('play', this.play).bind('pause', this.pause);
......@@ -103,83 +264,6 @@ function (HTML5Video) {
}
};
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: {
......@@ -189,67 +273,7 @@ function (HTML5Video) {
});
};
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;
......@@ -308,12 +332,12 @@ function (HTML5Video) {
return this.updatePlayTime(time);
};
VideoPlayerAlpha.prototype.onSpeedChange = function(event, newSpeed) {
VideoPlayerAlpha.prototype.onSpeedChange = function(event, newSpeed, updateCookie) {
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);
this.video.setSpeed(newSpeed, updateCookie);
if (this.video.videoType === 'youtube') {
if (this.video.show_captions === true) {
this.caption.currentSpeed = newSpeed;
......@@ -384,12 +408,12 @@ function (HTML5Video) {
};
VideoPlayerAlpha.prototype.duration = function() {
if (this.video.videoType === "youtube") {
return this.video.getDuration();
} else if (this.video.videoType === "html5") {
return this.player.getDuration();
var duration;
duration = this.player.getDuration();
if (isFinite(duration) === false) {
duration = this.video.getDuration();
}
return 0;
return duration;
};
VideoPlayerAlpha.prototype.currentSpeed = function() {
......
(function (requirejs, require, define) {
// Main module
// Main module.
require(
['videoalpha/display/initialize.js', 'videoalpha/display/video_player.js'],
function (Initialize, VideoPlayer) {
[
'videoalpha/display/initialize.js',
'videoalpha/display/video_control.js',
],
function (Initialize, VideoControl) {
var previousState;
// Because this constructor can be called multiple times on a single page (when
// the user switches verticals, the page doesn't reload, but the content changes), we must
// will check each time if there is a previous copy of 'state' object. If there is, we
// will make sure that copy exists cleanly. We have to do this because when verticals switch,
// the code does not handle any Xmodule JS code that is running - it simply removes DOM
// elements from the page. Any functions that were running during this, and that will run
// afterwards (expecting the DOM elements to be present) must be stopped by hand.
previousState = null;
window.VideoAlpha = function (element) {
var state;
// Check for existance of previous state, uninitialize it if necessary, and create a new state.
// Store new state for future invocation of this module consturctor function.
if (previousState !== null) {
previousState.videoPlayer.onPause();
}
state = {};
previousState = state;
new Initialize(state, element);
new VideoPlayer(state);
Initialize(state, element);
VideoControl(state);
console.log('Finished constructing "state" object. state = ');
console.log('state is:');
console.log(state);
};
});
......
......@@ -73,6 +73,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
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/display/video_control.js'),
resource_string(__name__, 'js/src/videoalpha/main.js')
]
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment