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.
This commit is contained in:
Valera Rozuvan
2013-02-15 17:42:53 +02:00
committed by Vasyl Nakvasiuk
parent e8d07d5377
commit 009b8475dc
5 changed files with 433 additions and 222 deletions

View File

@@ -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';
}
}

View File

@@ -0,0 +1,145 @@
(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);
*/

View File

@@ -3,12 +3,233 @@
// 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();
};
// 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;
}
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;
}
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
}
});
}
}
function bindHandlers() {
}
function reinitAsFlash(state) {
state.videoPlayer.player.destroy();
$.cookie('current_player_mode', 'flash', {
expires: 3650,
path: '/'
});
state.currentPlayerMode = 'flash';
delete state.videoPlayer.playerVars.html5;
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
}
});
}
// 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 pause() { }
function play() {
if (this.videoPlayer.player.playVideo) {
this.videoPlayer.player.playVideo();
}
console.log('state is:');
console.log(this);
}
function toggleFullScreen() { }
function update() { }
function onVolumeChange() { }
function onSpeedChange() { }
function onSeek() { }
function onEnded() {
console.log('this.videoPlayer.PlayerState.ENDED');
}
function onPause() {
console.log('this.videoPlayer.PlayerState.PAUSED');
}
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));
@@ -17,66 +238,6 @@ function (HTML5Video) {
/*
// 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') {
@@ -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() {

View File

@@ -1,18 +1,38 @@
(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);
};
});

View File

@@ -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')
]
}