Skip to content

Instantly share code, notes, and snippets.

@jabis
Created November 11, 2013 21:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jabis/f5e98571abe2aed3c43d to your computer and use it in GitHub Desktop.
Save jabis/f5e98571abe2aed3c43d to your computer and use it in GitHub Desktop.
A fast port of MooPlay, by challet, from MooTools 1.2.4 to 1.4.*
(function() {
/*
---
description: Base MooPlay object, useful but not really interesting
license: GNU GPL
authors:
- Clément Hallet
requires:
- core/1.2.4: [Core, Element, Element.Event, Element.Style, Class, Class.Extra.Events, Class.Extra.Options]
provides:
- MooPlay
- MooPlay.Subtitle
- MooPlay.Subtitle.Parser
- MooPlay.Control
- MooPlay.Display
...
*/
this.Element.NativeEvents = Object.extend(Element.NativeEvents, {
loadstart: 2,
progress: 2,
suspend: 2,
abort: 2,
error: 2,
emptied: 2,
stalled: 2,
play: 2,
pause: 2,
loadedmetadata: 2,
loadeddata: 2,
waiting: 2,
playing: 2,
canplay: 2,
canplaythrough: 2,
seeking: 2,
seeked: 2,
timeupdate: 2,
ended: 2,
ratechange: 2,
durationchange: 2,
volumechange: 2
});
var MooPlay = this.MooPlay = {
Subtitle: {
Parser: {}
},
Control: {},
Display: {}
};
/*
---
description: some utility functions
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
provides:
- MooPlay.Utils
...
*/
MooPlay.Utils = {
/**
* @param srt_time : format is '00:02:52,406'
*/
sexagesimalToTimestamp: function(srt_time) {
return ((srt_time.h * 60 + srt_time.m) * 60 + srt_time.s) * 1000 + srt_time.ms;
},
/**
* @return format is '00:02:52,406'
*/
timestampToSexagesimal: function(timestamp) {
var ms = timestamp.floor();
var s = (ms / 1000).floor() ;
var m = (s / 60).floor();
var h = (m / 60).floor();
return {
h: h,
m: m % 60,
s: s % 60,
ms: ms % 1000
};
},
readable: function(srt_time) {
srt_time.m = String(srt_time.m).pad(2,'0');
srt_time.s = String(srt_time.s).pad(2,'0');
srt_time.ms = String(srt_time.ms).pad(3,'0');
return srt_time;
}
}
//+ Jonas Raoni Soares Silva
//@ http://jsfromhell.com/string/pad [rev. #1]
String.prototype.pad = function(l, s, t){
return s || (s = " "), (l -= this.length) > 0 ? (s = new Array(Math.ceil(l / s.length)
+ 1).join(s)).substr(0, t = !t ? l : t == 1 ? 0 : Math.ceil(l / 2))
+ this + s.substr(0, l - t) : this;
};/*
---
description: proxy between any DOM element and a video element, controls and displays the current position inside the video
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- more/1.2.4: [Slider]
provides:
- MooPlay.Control.PlayProgress
...
*/
MooPlay.Control.PlayProgress = new Class({
Implements: [Options],
initialize: function(video, slider, options) {
this.setOptions(options);
this.slider = slider;
this.video = $(video);
this.suspended = false;
this.video.addEvents({
'timeupdate': this.tick.bind(this),
'seeked': this.resume.bind(this)
});
this.slider.knob.addEvents({
'mousedown': this.suspend.bind(this),
'mouseup': this.resume.bind(this),
'click': function(event) { event.stop(); }
});
this.slider.addEvent('change', this.change.bind(this));
},
suspend: function(event) {
event.preventDefault();
this.suspended = true;
},
resume: function(event) {
event.preventDefault();
this.suspended = false;
},
tick: function(event) {
if(!this.suspended) {
position = this.slider.toPosition( event.target.currentTime / event.target.duration * this.slider.range );
this.slider.knob.setStyle(this.slider.property, position);
}
},
change: function(step) {
this.suspended = true;
this.video.currentTime = this.video.duration * step / this.slider.steps;
}
});
/*
---
description: proxy between any DOM element and a video element, displays the loading progress of the video
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- progressbar: *
- more/1.2.4: [Slider]
provides:
- MooPlay.Control.LoadProgress
...
*/
MooPlay.Control.LoadProgress = new Class({
options: {
preload_class: 'preloading'
},
Implements: [Options],
initialize: function(video, progressbar, options) {
this.setOptions(options);
this.progressbar = progressbar;
this.video = $(video);
this.video.addEvents({
'progress': function(e, video, data) {
if(e.event.lengthComputable) {
this.tick(e.event.loaded, e.event.total);
} else {
this.preload(true);
}
}.bind(this),
'loadstart': this.preload.pass(true, this),
'seeking': this.preload.pass(true, this),
'loadedmetadata': this.preload.pass(false, this),
'seeked': this.preload.pass(false, this)
});
},
preload: function(state) {
if(state) {
this.progressbar.options.container.addClass(this.options.preload_class);
} else {
this.progressbar.options.container.removeClass(this.options.preload_class);
}
},
tick: function(loaded, total) {
this.progressbar.set(loaded / total * 100);
},
});
/*
---
description: make an element to act as a button.
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
provides:
- MooPlay.Control.BaseButton
...
*/
MooPlay.Control.BaseButton = new Class({
Implements: [Options],
options: {
over_state_class: 'over',
click_state_class: 'clicked'
},
initialize: function(video, element, options) {
this.setOptions(options);
this.element = $(element);
this.video = $(video);
//console.log(video,element,options,this.element,this.video);
this.element.addEvents({
'mouseenter': function(event) {
event.preventDefault();
this.element.addClass(this.options.over_state_class);
}.bind(this),
'mouseleave': function(event) {
event.preventDefault();
this.element.removeClass(this.options.over_state_class);
}.bind(this),
'mousedown': function(event) {
event.preventDefault();
this.element.addClass(this.options.click_state_class);
}.bind(this),
'mouseup': function(event) {
event.preventDefault();
this.element.removeClass(this.options.click_state_class);
}.bind(this)
});
this.specificInitialize();
}
});
/*
---
description: proxy between any DOM element and a video element, controls and displays the play and pause video states
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay.Control.BaseButton
provides:
- MooPlay.Control.PlayPause
...
*/
MooPlay.Control.PlayPause = new Class({
Extends: MooPlay.Control.BaseButton,
options: {
paused_state_class: 'paused'
},
initialize:function(video,element,options){
this.parent(video,element,options);
},
specificInitialize: function() {
if(this.video.paused) {
this.element.addClass(this.options.paused_state_class);
}
this.video.addEvents({
'play': function() {
this.element.removeClass(this.options.paused_state_class);
}.bind(this),
'pause': function() {
this.element.addClass(this.options.paused_state_class);
}.bind(this)
});
this.element.addEvents({
'click': function(event) {
event.preventDefault();
this.toggleState();
}.bind(this)
});
},
toggleState: function() {
if(this.video.paused) {
this.video.play();
} else {
this.video.pause();
}
}
});
/*
---
description: Allows to move inside the video, with a pscific speed factor
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay.Control.BaseButton
provides:
- MooPlay.Control.FastMove
...
*/
MooPlay.Control.FastMove = new Class({
Extends: MooPlay.Control.BaseButton,
options: {
speed_factor: 1
},
initialize:function(video,element,options){
this.parent(video,element,options)
},
specificInitialize: function() {
this.element.addEvents({
'mousedown': this.beginMove.bind(this),
'mouseup': this.stopMove.bind(this),
'mouseleave': this.stopMove.bind(this)
});
this.start_time = null;
this.timer = null;
this.start_pos = null;
},
beginMove: function() {
if(this.timer == null) {
this.start_time = Date.now();
this.start_pos = this.video.currentTime;
this.timer = this.tick.bind(this).periodical(50);
}
},
stopMove: function() {
if(this.timer != null) {
this.start_time = null;
this.start_pos = null;
clearTimeout(this.timer);
this.timer = null;
}
},
tick: function() {
if(this.timer == null) {
return;
}
var time_to_move = (Date.now() - this.start_time) * this.options.speed_factor;
this.video.currentTime = this.start_pos + (time_to_move / 1000);
}
});
/*
---
description: display the player with the dimension of the page
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
provides:
- MooPlay.Control.FullScreen
...
*/
MooPlay.Control.FullScreen = new Class({
Extends: MooPlay.Control.BaseButton,
Implements: [Events,Options],
options: {
active_state_class: 'active'
},
full_screened: false,
initialize:function(video,element,options){
this.parent(video,element,options);
},
specificInitialize: function() {
this.initialStyle = this.video.getStyles('position', 'top', 'left');
this.element.addEvents({
'click': function(event) {
event.preventDefault();
this.fireEvent(this.full_screened ? 'foldStart' : 'expandStart');
}.bind(this)
});
this.fx = new Fx.Morph(this.video, {
link: 'cancel',
onComplete: function() {
this.fireEvent(this.full_screened ? 'foldComplete' : 'expandComplete');
this.full_screened = !this.full_screened;
}.bind(this)
});
this.addEvents({
'expandStart': this.onExpandStart.bind(this),
'expandComplete': this.onExpandComplete.bind(this),
'foldStart': this.onFoldStart.bind(this),
'foldComplete': this.onFoldComplete.bind(this)
});
},
onExpandStart: function() {
document.body.setStyle('overflow', 'hidden');
var abs_coordinates = this.video.getCoordinates(document.body);
var body_scroll = document.body.getScroll();
this.initialCoordinates = {
top: abs_coordinates.top - body_scroll.y,
left: abs_coordinates.left - body_scroll.x,
height: abs_coordinates.height,
width: abs_coordinates.width,
};
this.video.setStyles({
position: 'fixed',
top: String(this.initialCoordinates.top) + 'px',
left: String(this.initialCoordinates.left) + 'px'
});
var body_dimension = document.body.getCoordinates();
this.fx.start({
height: body_dimension.height,
width: body_dimension.width,
top: 0,
left: 0
});
},
onExpandComplete: function() {
this.video.setStyles({
width: '100%',
height: '100%'
});
this.element.addClass(this.options.active_state_class);
},
onFoldStart: function() {
var video_dimension = this.video.getCoordinates();
this.video.setStyles({
width: String(video_dimension.width) + 'px',
height: String(video_dimension.height) + 'px'
});
this.fx.start({
height: this.initialCoordinates.height,
width: this.initialCoordinates.width,
top: this.initialCoordinates.top,
left: this.initialCoordinates.left
});
},
onFoldComplete: function() {
document.body.setStyle('overflow', 'visible');
this.video.setStyles(this.initialStyle);
this.element.removeClass(this.options.active_state_class);
document.body.setStyle('overflow', 'visible');
}
});
/*
---
description: control volume through a slider
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Utils
provides:
- MooPlay.Control.Volume
...
*/
MooPlay.Control.Volume = new Class({
options: {
auto_unmute: true
},
Implements: [Options],
initialize: function(video, slider, options) {
this.setOptions(options);
this.slider = slider;
this.video = $(video);
this.video.addEvent('volumechange', this.update.bind(this));
this.slider.addEvent('change', this.change.bind(this));
},
update: function(event) {
var volume = event.target.muted ? 0 : event.target.volume;
position = this.slider.toPosition( volume * this.slider.range );
this.slider.knob.setStyle(this.slider.property, position);
},
change: function(pos) {
this.video.volume = pos / this.slider.steps;
if(this.options.auto_unmute && this.video.muted) {
this.video.muted = false;
}
}
});
/*
---
description: control mute with a button
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Utils
provides:
- MooPlay.Control.Mute
...
*/
MooPlay.Control.Mute = new Class({
Extends: MooPlay.Control.BaseButton,
options: {
muted_state_class: 'muted'
},
initialize:function(video,element,options){
this.parent(video,element,options);
},
specificInitialize: function() {
this.video.addEvents({
'volumechange': this.update.bind(this)
});
this.element.addEvents({
'click': function(event) {
event.preventDefault();
this.toggleState();
}.bind(this)
});
},
update: function(event) {
if(event.target.muted) {
this.element.addClass(this.options.muted_state_class);
} else {
this.element.removeClass(this.options.muted_state_class);
}
},
toggleState: function() {
this.video.muted = !this.video.muted;
}
});
/*
---
description: display position in the video, in human readable time
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Utils
provides:
- MooPlay.Control.TimeDisplay
...
*/
MooPlay.Control.TimeDisplay = new Class({
Implements: [Options],
options: {
pattern: '{h}:{m}:{s},{ms}',
current: true, // vs 'remaining'
auto_update: true
},
initialize: function(video, container, options) {
this.setOptions(options);
this.container = $(container);
this.video = $(video);
if(this.options.auto_update) {
this.video.addEvent('timeupdate', function(event) {
if(this.options.current) {
this.update(event.target.currentTime * 1000);
} else {
this.update(Math.max(0, event.target.duration - event.target.currentTime) * 1000);
}
}.bind(this));
}
},
update: function(abs_movie_time) {
var new_text = this.options.pattern.substitute(
MooPlay.Utils.readable(MooPlay.Utils.timestampToSexagesimal(abs_movie_time))
);
if(new_text != this.container.get('text')) {
this.container.empty().appendText(new_text);
}
}
});
/*
---
description: object representation of a subtitle line
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
provides:
- MooPlay.Subtitle.Item
...
*/
MooPlay.Subtitle.Item = new Class({
initialize: function(start, end, texts) {
this.start = start;
this.end = end;
this.element = new Element('div');
texts.each(function(text) {
this.element.grab(
new Element('p').appendText(text)
);
}.bind(this));
},
});
/*
---
description: hash tree store for subtitles
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Subtitle.Item
provides:
- MooPlay.Subtitle.Tree
...
*/
MooPlay.Subtitle.Tree = new Class({
nb_childs: 2,
children: [],
subs: [],
initialize : function(start, end) {
this.start = start;
this.end = end;
},
buildChildren: function() {
var child_period = Math.ceil((this.end - this.start) / this.nb_childs);
for (var i = 0; i < this.nb_childs; i++) {
this.children.push(new MooPlay.Subtitle.Tree(
this.start + i * child_period, // start
this.start + (i + 1) * child_period // end
));
}
},
getChildren: function(even_empty) {
if(this.children.length == 0 && even_empty) {
this.buildChildren();
}
return this.children;
},
doesSubtitleFit: function(sub) {
return sub.start >= this.start && sub.end <= this.end;
},
addSub: function(sub) {
var fit_in_one_child = false;
this.getChildren(true).each(function(child) {
if(child.doesSubtitleFit(sub)) {
fit_in_one_child = true;
child.addSub(sub);
}
}.bind(this));
if(this.doesSubtitleFit(sub) && !fit_in_one_child) {
this.subs.push(sub);
}
},
getSubs: function(timestamp) {
if(timestamp < this.start && timestamp >= this.end) {
return [];
}
var subs = [];
this.subs.each(function(sub) {
if(timestamp >= sub.start && timestamp < sub.end) {
subs.push(sub);
}
});
this.getChildren(false).each(function(child) {
if(timestamp >= child.start && timestamp <= child.end) {
subs.extend(child.getSubs(timestamp));
}
});
return subs;
}
});
/*
---
description: ajax-loading subtitles and routing to the right parser
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Subtitle.Item
- MooPlay.Subtitle.Tree
provides:
- MooPlay.Subtitle.Loader
...
*/
MooPlay.Subtitle.Loader = new Class({
Implements: [Options],
initialize: function(url, options) {
this.url = url;
this.setOptions(options);
this.load();
},
load: function() {
var request = new Request({
url: this.url,
method: 'get',
onSuccess: this.run.bind(this)
});
request.send({});
},
run: function (data) {
var parser = this.selectParser();
return new parser(data, {onComplete: this.options.onComplete});
},
selectParser: function() {
var ext = this.url.split('.').pop();
switch(ext) {
case 'srt':
return MooPlay.Subtitle.Parser.SubRip;
break;
case 'sub':
return MooPlay.Subtitle.Parser.SubViewer;
break;
default:
throw 'the ' + ext + ' format is not known or supported as a subtitle file';
break;
}
}
});
/*
---
description: diplay subtitles synchronised with a video element
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Subtitle.Item
- MooPlay.Subtitle.Tree
provides:
- MooPlay.Subtitle.Player
...
*/
MooPlay.Subtitle.Player = new Class({
Implements: [Options],
options: {
subs_hash: null,
tick_delay: 100, // not in use for now
time_shift: 0,
onDispose: function(element, container, overlapping) {
element.dispose();
sub.element.removeClass('overlapping' + String(overlapping));
},
onDisplay: function(element, container, overlapping) {
element.addClass('overlapping' + String(overlapping));
element.inject(container, 'bottom');
}
},
initialize: function( video, container, options) {
this.setOptions(options);
this.video = $(video);
this.container = $(container);
if(this.options.subs_hash != null) {
this.loadSubtitles(this.options.subs_hash);
}
this.overlapping_level = 0;
this.displayed = [];
this.video.addEvent('timeupdate', function(event) {
if(this.subs_hash != null || this.displayed.length != 0) {
this.tick(event.target.currentTime * 1000);
}
}.bind(this));
},
loadSubtitles: function(subs_hash) {
this.unLoad();
this.subs_hash = subs_hash;
},
unLoad: function() {
this.subs_hash = null;
},
tick: function(abs_movie_time) {
var next_displayed = this.subs_hash != null ? this.subs_hash.getSubs(abs_movie_time - this.options.time_shift) : [];
// remove subs which are not here anymore
this.displayed.each(function(sub) {
var displayed = [];
if(!next_displayed.contains(sub) || this.subs_hash == null) {
this.options.onDispose(sub.element, this.container, --this.overlapping_level);
} else {
displayed.push(sub);
}
this.displayed = displayed;
}.bind(this));
// display subs which should to
next_displayed.each(function(sub) {
if(!this.displayed.contains(sub)) {
this.displayed.push(sub);
this.options.onDisplay(sub.element, this.container, this.overlapping_level++);
}
}.bind(this));
},
setTimeShift: function(shift) {
this.options.time_shift = parseInt(shift);
}
});
/*
---
description: base class for ajax-loading and parsing subtitles file
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Subtitle.Item
- MooPlay.Subtitle.Tree
provides:
- MooPlay.Subtitle.Parser.Base
...
*/
MooPlay.Subtitle.Parser.Base = new Class({
Implements: [Options],
options: {
onComplete: Function.from()
},
initialize: function(data, options) {
this.setOptions(options);
this.hash(
this.parse(data)
);
this.options.onComplete(this.hash_root);
},
parse:function(data){
return this;
},
hash: function(subs) {
var abs_start = Infinity;
var abs_end = 0;
Object.each(subs,function(sub) {
abs_start = Math.min(abs_start, sub.start);
abs_end = Math.max(abs_end, sub.end);
});
this.hash_root = new MooPlay.Subtitle.Tree(abs_start, abs_end);
Object.each(subs,function(sub) {
this.hash_root.addSub(sub);
}.bind(this));
}
});
/*
---
description: specific class for parsing subtitles file in SubRip format (.srt)
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Utils
- MooPlay.Subtitle.Parser.Base
- MooPlay.Subtitle.Item
provides:
- MooPlay.Subtitle.Parser.SubRip
...
*/
MooPlay.Subtitle.Parser.SubRip = new Class({
Extends: MooPlay.Subtitle.Parser.Base,
regexps: {
new_sub: /^(\d+)$/,
time: /^(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})$/,
text: /^(.+)$/
},
options: {
srt_end_of_line: '\n',
onComplete: Function.from()
},
initialize:function(){
},
parse: function(data) {
var subs = [];
var current_sub = null;
var current_text = null;
var index = null
var lines = data.split(this.options.srt_end_of_line);
// in case file doesn't end with an empty line
lines.push('');
do {
var line = lines.shift();
if(this.regexps.new_sub.test(line)) {
current_text = [];
current_sub = {};
} else if(line != null && this.regexps.time.test(line)) {
var times = this.regexps.time.exec(line);
current_sub.start = MooPlay.Utils.sexagesimalToTimestamp({ h: times[1].toInt(), m: times[2].toInt(), s: times[3].toInt(), ms: times[4].toInt() });
current_sub.end = MooPlay.Utils.sexagesimalToTimestamp({ h: times[5].toInt(), m: times[6].toInt(), s: times[7].toInt(), ms: times[8].toInt() });
var times = null;
} else if(line != null && this.regexps.text.test(line)) {
current_text.push(this.regexps.text.exec(line)[0]);
} else if(current_sub != null) {
subs.push(new MooPlay.Subtitle.Item(current_sub.start, current_sub.end, current_text));
current_sub = null;
current_text = null;
}
} while(line != null);
return subs;
},
});
/*
---
description: specific class for parsing subtitles file in SubViewer format (.sub)
license: GNU GPL
authors:
- Clément Hallet
requires:
- MooPlay
- MooPlay.Utils
- MooPlay.Subtitle.Parser.Base
- MooPlay.Subtitle.Item
provides:
- MooPlay.Subtitle.Parser.SubViewer
...
*/
MooPlay.Subtitle.Parser.SubViewer = new Class({
Implements: MooPlay.Subtitle.Parser.Base,
regexps: {
time: /^(\d{2}):(\d{2}):(\d{2}).(\d{3}),(\d{2}):(\d{2}):(\d{2}).(\d{3})$/,
text: /^(.+)$/
},
options: {
srt_end_of_line: '\n',
onComplete: Function.from()
},
parse: function(data) {
var subs = [];
var current_sub = null;
var current_text = null;
var index = null
var lines = data.split(this.options.srt_end_of_line);
// in case file doesn't end with an empty line
lines.push('');
do {
var line = lines.shift();
if(line != null && this.regexps.time.test(line)) {
current_text = [];
current_sub = {};
var times = this.regexps.time.exec(line);
current_sub.start = MooPlay.Utils.sexagesimalToTimestamp({ h: times[1].toInt(), m: times[2].toInt(), s: times[3].toInt(), ms: times[4].toInt() });
current_sub.end = MooPlay.Utils.sexagesimalToTimestamp({ h: times[5].toInt(), m: times[6].toInt(), s: times[7].toInt(), ms: times[8].toInt() });
var times = null;
} else if(line != null && this.regexps.text.test(line)) {
current_text = this.regexps.text.exec(line)[0].split('[BR]');
} else if(current_sub != null) {
subs.push(new MooPlay.Subtitle.Item(current_sub.start, current_sub.end, current_text));
current_sub = null;
current_text = null;
}
} while(line != null);
return subs;
},
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment