Telegram Web, preconfigured for usage in I2P. http://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

510 lines
18 KiB

/**
* MDN references for hackers:
* ===========================
* Media events on <audio> and <video> tags:
* https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events
* Properties and Methods:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
* Internet Exploder version:
* http://msdn.microsoft.com/en-us/library/ff975061(v=vs.85).aspx
*
* Understanding TimeRanges objects:
* http://html5doctor.com/html5-audio-the-state-of-play/
*/
angular.module('mediaPlayer', ['mediaPlayer.helpers'])
.constant('mp.playerDefaults', {
// general properties
currentTrack: 0,
ended: undefined,
network: undefined,
playing: false,
seeking: false,
tracks: 0,
volume: 1,
// formatted properties
formatDuration: '00:00',
formatTime: '00:00',
loadPercent: 0
})
.directive('mediaPlayer', ['$rootScope', '$interpolate', '$timeout', 'mp.throttle', 'mp.playerDefaults',
function ($rootScope, $interpolate, $timeout, throttle, playerDefaults) {
var playerMethods = {
/**
* @usage load([mediaElement], [autoplay]);
*
* @param {mediaElement Obj} mediaElement a single mediaElement, may contain multiple <source>(s)
* @param {boolean} autoplay: flag to autostart loaded element
*/
load: function (mediaElement, autoplay) {
// method overloading
if (typeof mediaElement === 'boolean') {
autoplay = mediaElement;
mediaElement = null;
} else if (typeof mediaElement === 'object') {
this.$clearSourceList();
this.$addSourceList(mediaElement);
}
this.$domEl.load();
this.ended = undefined;
if (autoplay) {
this.$element.one('canplay', this.play.bind(this));
}
},
/**
* resets the player (clear the <source>s) and reloads the playlist
*
* @usage reset([autoplay]);
* @param {boolean} autoplay: flag to autoplay once resetted
*/
reset: function (autoplay) {
angular.extend(this, playerDefaults);
this.$clearSourceList();
this.load(this.$playlist, autoplay);
},
/**
* @usage play([index], [selectivePlay])
* @param {integer} index: playlist index (0...n), to start playing from
* @param {boolean} selectivePlay: only correct value is `true`, in which case will only play the specified,
* or current, track. The default is to continue playing the next one.
*/
play: function (index, selectivePlay) {
// method overloading
if (typeof index === 'boolean') {
selectivePlay = index;
index = undefined;
}
if (selectivePlay) {
this.$selective = true;
}
if (this.$playlist.length > index) {
this.currentTrack = index + 1;
return this.load(this.$playlist[index], true);
}
// readyState = HAVE_NOTHING (0) means there's nothing into the <audio>/<video> tag
if (!this.currentTrack && this.$domEl.readyState) { this.currentTrack++; }
// In case the stream is completed, reboot it with a load()
if (this.ended) {
this.load(true);
} else {
this.$domEl.play();
}
},
playPause: function (index, selectivePlay) {
// method overloading
if (typeof index === 'boolean') {
selectivePlay = index;
index = undefined;
}
if (selectivePlay) {
this.$selective = true;
}
if (typeof index === 'number' && index + 1 !== this.currentTrack) {
this.play(index);
} else if (this.playing) {
this.pause();
} else {
this.play();
}
},
pause: function () {
this.$domEl.pause();
},
stop: function () {
this.reset();
},
toggleMute: function () {
this.muted = this.$domEl.muted = !this.$domEl.muted;
},
next: function (autoplay) {
var self = this;
if (self.currentTrack && self.currentTrack < self.tracks) {
var wasPlaying = autoplay || self.playing;
self.pause();
$timeout(function () {
self.$clearSourceList();
self.$addSourceList(self.$playlist[self.currentTrack]);
self.load(wasPlaying); // setup autoplay here.
self.currentTrack++;
});
}
},
prev: function (autoplay) {
var self = this;
if (self.currentTrack && self.currentTrack - 1) {
var wasPlaying = autoplay || self.playing;
self.pause();
$timeout(function () {
self.$clearSourceList();
self.$addSourceList(self.$playlist[self.currentTrack - 2]);
self.load(wasPlaying); // setup autoplay here.
self.currentTrack--;
});
}
},
setPlaybackRate: function (value) {
this.$domEl.playbackRate = value;
},
setVolume: function (value) {
this.$domEl.volume = value;
},
seek: function (value) {
var doubleval = 0, valuesArr;
if (typeof value === 'string') {
valuesArr = value.split(':');
doubleval += parseInt(valuesArr.pop(), 10);
if (valuesArr.length) { doubleval += parseInt(valuesArr.pop(), 10) * 60; }
if (valuesArr.length) { doubleval += parseInt(valuesArr.pop(), 10) * 3600; }
if (!isNaN(doubleval)) {
return this.$domEl.currentTime = doubleval;
}
} else {
return this.$domEl.currentTime = value;
}
},
/**
* binds a specific event directly to the element
* @param {string} type event name
* @param {function} fn callback
* @return {function} unbind function, call it in order to remove the listener
*/
on: function (type, fn) {
return this.$element.on(type, fn);
},
off: function (type, fn) {
return this.$element.off(type, fn);
},
one: function (type, fn) {
return this.$element.one(type, fn);
},
$addSourceList: function (sourceList) {
var self = this;
if (angular.isArray(sourceList)) {
angular.forEach(sourceList, function (singleElement, index) {
var sourceElem = document.createElement('SOURCE');
['src', 'type', 'media'].forEach(function (key) { // use only a subset of the properties
if (singleElement[key] !== undefined) { // firefox is picky if you set undefined attributes
sourceElem.setAttribute(key, singleElement[key]);
}
});
self.$element.append(sourceElem);
});
} else if (angular.isObject(sourceList)) {
var sourceElem = document.createElement('SOURCE');
['src', 'type', 'media'].forEach(function (key) {
if (sourceList[key] !== undefined) {
sourceElem.setAttribute(key, sourceList[key]);
}
});
self.$element.append(sourceElem);
}
},
$clearSourceList: function () {
this.$element.contents().remove();
},
$formatTime: function (seconds) {
if (seconds === Infinity) {
return '∞'; // If the data is streaming
}
var hours = parseInt(seconds / 3600, 10) % 24,
minutes = parseInt(seconds / 60, 10) % 60,
secs = parseInt(seconds % 60, 10),
result,
fragment = (minutes < 10 ? '0' + minutes : minutes) + ':' + (secs < 10 ? '0' + secs : secs);
if (hours > 0) {
result = (hours < 10 ? '0' + hours : hours) + ':' + fragment;
} else {
result = fragment;
}
return result;
},
$attachPlaylist: function (pl) {
if (pl === undefined || pl === null) {
this.playlist = [];
} else {
this.$playlist = pl;
}
}
};
/**
* Binding function that gives life to AngularJS scope
* @param {Scope} au Player Scope
* @param {HTMLMediaElement} HTML5 element
* @param {jQlite} element <audio>/<video> element
* @return {function}
*
* Returns an unbinding function
*/
var bindListeners = function (au, al, element) {
var listeners = {
playing: function () {
au.$apply(function (scope) {
scope.playing = true;
scope.ended = false;
});
},
pause: function () {
au.$apply(function (scope) {
scope.playing = false;
});
},
ended: function () {
if (!au.$selective && au.currentTrack < au.tracks) {
au.next(true);
} else {
au.$apply(function (scope) {
scope.ended = true;
scope.playing = false; // IE9 does not throw 'pause' when file ends
});
}
},
timeupdate: throttle(1000, false, function () {
au.$apply(function (scope) {
scope.currentTime = al.currentTime;
scope.formatTime = scope.$formatTime(scope.currentTime);
});
}),
loadedmetadata: function () {
au.$apply(function (scope) {
if (!scope.currentTrack) { scope.currentTrack++; } // This is triggered *ONLY* the first time a <source> gets loaded.
scope.duration = al.duration;
scope.formatDuration = scope.$formatTime(scope.duration);
if (al.buffered.length) {
scope.loadPercent = Math.round((al.buffered.end(al.buffered.length - 1) / scope.duration) * 100);
}
});
},
progress: function () {
if (au.$domEl.buffered.length) {
au.$apply(function (scope) {
scope.loadPercent = Math.round((al.buffered.end(al.buffered.length - 1) / scope.duration) * 100);
scope.network = 'progress';
});
}
},
volumechange: function () { // Sent when volume changes (both when the volume is set and when the muted attribute is changed).
au.$apply(function (scope) {
// scope.volume = Math.floor(al.volume * 100);
scope.volume = al.volume;
scope.muted = al.muted;
});
},
seeked: function () {
au.$apply(function (scope) {
scope.seeking = false;
});
},
seeking: function () {
au.$apply(function (scope) {
scope.seeking = true;
});
},
ratechange: function () {
au.$apply(function (scope) {
// scope.playbackRate = Math.floor(al.playbackRate * 100);
scope.playbackRate = al.playbackRate;
});
},
stalled: function () {
au.$apply(function (scope) {
scope.network = 'stalled';
});
},
suspend: function () {
au.$apply(function (scope) {
scope.network = 'suspend';
});
}
};
angular.forEach(listeners, function (f, listener) {
element.on(listener, f);
});
};
var MediaPlayer = function (element) {
var mediaScope = angular.extend($rootScope.$new(true), {
$element: element,
$domEl: element[0],
$playlist: undefined,
// bind TimeRanges structures to actual MediaElement
buffered: element[0].buffered,
played: element[0].played,
seekable: element[0].seekable,
}, playerDefaults, playerMethods);
bindListeners(mediaScope, element[0], element);
return mediaScope;
};
// creates a watch function bound to the specific player
// optimizable: closures eats ram
function playlistWatch(player) {
return function (playlistNew, playlistOld, watchScope) {
var currentTrack,
newTrackNum = null;
// on every playlist change, it refreshes the reference, safer/shorter approach
// than using multiple ifs and refresh only if it changed; there's no benefit in that
player.$attachPlaylist(playlistNew);
if (playlistNew === undefined && playlistOld !== undefined) {
return player.pause();
}
/**
* Playlist update logic:
* If the player has started ->
* Check if the playing track is in the new Playlist [EXAMPLE BELOW]
* If it is ->
* Assign to it the new tracknumber
* Else ->
* If the new Playlist has some song ->
* Pause the player, and get the new Playlist
* Else ->
* Reset the player, and await for orders
*
* Else (if the player hasn't started yet)
* Just replace the <src> tags
*
* Example
* playlist: [a,b,c], playing: c, trackNum: 2
* ----delay 5 sec-----
* playlist: [f,a,b,c], playing: c, trackNum: 3
*
*/
if (player.currentTrack) {
currentTrack = playlistOld ? playlistOld[player.currentTrack - 1] : -1;
for (var i = 0; i < playlistNew.length; i++) {
if (angular.equals(playlistNew[i], currentTrack)) { newTrackNum = i; break; }
}
if (newTrackNum !== null) { // currentTrack it's still in the new playlist, update trackNumber
player.currentTrack = newTrackNum + 1;
player.tracks = playlistNew.length;
} else { // currentTrack has been removed.
player.pause();
if (playlistNew.length) { // if the new playlist has some elements, replace actual.
$timeout(function () { // need $timeout because the mediaTag needs a little time to launch the 'pause' event
player.$clearSourceList();
player.$addSourceList(playlistNew[0]);
player.load();
player.tracks = playlistNew.length;
});
} else { // the new playlist has no elements, clear actual
player.reset();
}
}
} else if (playlistNew.length) { // new playlist has elements, load them
player.$clearSourceList();
player.$addSourceList(playlistNew[0]);
player.load();
player.tracks = playlistNew.length;
} else { // new playlist has no elements, clear actual
player.reset();
}
};
}
return {
/**
* The directive uses the Scope in which gets declared,
* so it can read/write it with ease.
* The only isolated Scope here gets created for the MediaPlayer.
*/
scope: false,
link: function (scope, element, attrs, ctrl) {
var playlistName = attrs.playlist,
mediaName = attrs.mediaPlayer || attrs.playerControl;
var player = new MediaPlayer(element), playlist = scope[playlistName];
// create data-structures in the father Scope
if (playlistName === undefined) {
playlist = []; // local playlist gets defined as new
} else if (scope[playlistName] === undefined) {
playlist = scope[playlistName] = []; // define playlist on father scope
} else {
playlist = scope[playlistName];
}
if (mediaName !== undefined) {
scope.$eval(mediaName + ' = player', {player: player});
}
if (element[0].tagName !== 'AUDIO' && element[0].tagName !== 'VIDEO') {
return new Error('player directive works only when attached to an <audio>/<video> type tag');
}
var mediaElement = [],
sourceElements = element.find('source');
// create a single playlist element from <source> tag(s).
// if the <source> tag is one, use object notation...
if (sourceElements.length === 1) {
playlist.unshift({ src: sourceElements[0].src, type: sourceElements[0].type, media: sourceElements[0].media });
} else if (sourceElements.length > 1) { // otherwise use array notation
angular.forEach(sourceElements, function (sourceElement) {
mediaElement.push({ src: sourceElement.src, type: sourceElement.type, media: sourceElement.media });
});
// put mediaElement as first element in the playlist
playlist.unshift(mediaElement);
}
/**
* If the user wants to keep track of the playlist changes
* has to use data-playlist="variableName" in the <audio>/<video> tag
*
* Otherwise: it will be created an empty playlist and attached to the player.
*/
if (playlistName === undefined) {
player.$attachPlaylist(playlist); // empty playlist case
} else if (playlist.length) {
playlistWatch(player)(playlist, undefined, scope); // playlist already populated gets bootstrapped
scope.$watch(playlistName, playlistWatch(player), true); // then watch gets applied
} else {
scope.$watch(playlistName, playlistWatch(player), true); // playlist empty, only watch
}
}
};
}]
);
angular.module('mediaPlayer.helpers', [])
.factory('mp.throttle', ['$timeout', function ($timeout) {
return function (delay, no_trailing, callback, debounce_mode) {
var timeout_id,
last_exec = 0;
if (typeof no_trailing !== 'boolean') {
debounce_mode = callback;
callback = no_trailing;
no_trailing = undefined;
}
var wrapper = function () {
var that = this,
elapsed = +new Date() - last_exec,
args = arguments,
exec = function () {
last_exec = +new Date();
callback.apply(that, args);
},
clear = function () {
timeout_id = undefined;
};
if (debounce_mode && !timeout_id) { exec(); }
if (timeout_id) { $timeout.cancel(timeout_id); }
if (debounce_mode === undefined && elapsed > delay) {
exec();
} else if (no_trailing !== true) {
timeout_id = $timeout(debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay);
}
};
return wrapper;
};
}]);