Igor Zhukov
10 years ago
5 changed files with 519 additions and 7 deletions
@ -1,13 +1,15 @@
@@ -1,13 +1,15 @@
|
||||
<div class="audio_player_wrap"> |
||||
<div class="audio_player_button"> |
||||
<div class="audio_player_wrap" ng-class=""> |
||||
<div class="audio_player_button" ng-click="togglePlay()"> |
||||
<i class="icon audio_player_btn_icon"></i> |
||||
</div> |
||||
<div class="audio_player_title_wrap"> |
||||
<div class="audio_player_title"></div> |
||||
<div class="audio_player_meta"></div> |
||||
<div class="audio_player_title" my-i18n="message_attach_audio_message"></div> |
||||
<div class="audio_player_meta"> |
||||
<span class="audio_player_duration"></span> |
||||
</div> |
||||
</div> |
||||
<div class="audio_player_progress_wrap"> |
||||
<div class="audio_player_progress"></div> |
||||
<i class="icon icon-player-track"></i> |
||||
<div class="audio_player_progress" ng-style="{width: audio.progress.percent + '%'}"></div> |
||||
<!-- <i class="icon icon-player-track"></i> --> |
||||
</div> |
||||
</div> |
@ -0,0 +1,508 @@
@@ -0,0 +1,508 @@
|
||||
/** |
||||
* 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[mediaName] = 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; |
||||
}; |
||||
}]); |
Loading…
Reference in new issue