updated plugin AudioIgniter version 2.0.0

This commit is contained in:
2023-06-05 11:21:12 +00:00
committed by Gitium
parent 1c5b451d2f
commit e5482aabb7
36 changed files with 776 additions and 242 deletions

View File

@ -13,7 +13,7 @@
</head>
<body>
<div
id="audioigniter-2799"
id="audioigniter-01"
class="audioigniter-root"
data-player-type="full"
data-tracks-url="/dev-tracks.json"
@ -37,11 +37,11 @@
data-skip-amount="15"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-stop-on-finish="true"
data-tracks-delay="5"
data-timer-countdown="false"
data-shuffle="true"
data-shuffle-default="false"
data-shuffle-default="true"
data-soundcloud-client-id=""
data-remember-last="true"
data-player-buttons='[
@ -65,7 +65,7 @@
></div>
<div
id="audioigniter-2999"
id="audioigniter-02"
class="audioigniter-root"
data-player-type="simple"
data-tracks-url="/dev-tracks.json"
@ -106,7 +106,7 @@
></div>
<div
id="audioigniter-2519"
id="audioigniter-03"
class="audioigniter-root"
data-track='{"title":"Sunrise","subtitle":"Thoribass","audio":"https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3","buyUrl":"https:\/\/google.com","downloadUrl":"https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3","cover":"https:\/\/www.cssigniter.com\/demos\/audioigniter\/wp-content\/uploads\/sites\/48\/2016\/08\/CyberSDF-Flame-and-Go.jpg","lyrics":"Some lyrics"}'
@ -141,7 +141,7 @@
></div>
<div
id="audioigniter-8999"
id="audioigniter-04"
class="audioigniter-root"
data-player-type="global-footer"
data-tracks-url="/dev-tracks.json"
@ -170,6 +170,7 @@
data-timer-countdown="true"
data-shuffle="true"
data-soundcloud-client-id=""
data-remember-last="true"
data-player-buttons='[
{
"title": "CSSIgniter",

View File

@ -25,6 +25,10 @@ if (process.env.NODE_ENV !== 'production') {
skip_backward: 'Skip backward',
shuffle: 'Shuffle',
};
window.aiStats = {
apiUrl: '',
};
}
const nodes = document.getElementsByClassName('audioigniter-root');
@ -77,7 +81,7 @@ function renderApp(node) {
initialTrack: parseInt(node.getAttribute('data-initial-track'), 10),
delayBetweenTracks: parseInt(node.getAttribute('data-tracks-delay'), 10),
stopOnTrackFinish: JSON.parse(node.getAttribute('data-stop-on-finish')),
defaultShuffle: JSON.parse(node.getAttribute('data-shuffle')),
defaultShuffle: JSON.parse(node.getAttribute('data-shuffle-default')),
shuffleEnabled: JSON.parse(node.getAttribute('data-shuffle')),
countdownTimerByDefault: JSON.parse(
node.getAttribute('data-timer-countdown'),

View File

@ -73,6 +73,7 @@ const propTypes = {
icon: PropTypes.string,
}).isRequired,
),
playerId: PropTypes.string,
};
const GlobalFooterPlayer = ({
@ -83,6 +84,7 @@ const GlobalFooterPlayer = ({
position,
duration,
playbackRate,
playerId,
currentTrack,
playTrack,
@ -312,6 +314,7 @@ const GlobalFooterPlayer = ({
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
playerId={playerId}
/>
{playerButtons?.length > 0 && <PlayerButtons buttons={playerButtons} />}

View File

@ -80,10 +80,12 @@ const propTypes = {
icon: PropTypes.string,
}).isRequired,
),
playerId: PropTypes.string,
};
const Player = ({
tracks,
playerId,
playStatus,
activeIndex,
volume,
@ -345,6 +347,7 @@ const Player = ({
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
playerId={playerId}
/>
</div>

View File

@ -10,6 +10,7 @@ import PlayerButtons from './components/PlayerButtons';
const propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playerId: PropTypes.string,
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
@ -82,6 +83,7 @@ const SimplePlayer = props => {
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
playerId={props.playerId}
/>
</div>

View File

@ -16,6 +16,10 @@ const Time = ({ countdown, position, duration }) => {
* @returns {string} - Time pretty formatted
*/
const renderFormattedTime = () => {
if (!duration) {
return '00:00';
}
const positionInSeconds = showRemaining
? (duration - position) / 1000
: position / 1000;
@ -27,7 +31,7 @@ const Time = ({ countdown, position, duration }) => {
min = min >= 10 ? min : `0${min}`;
sec = sec >= 10 ? sec : `0${sec}`;
if (!Number.isNaN(sec)) {
if (Number.isInteger(parseInt(sec, 10))) {
if (hours) {
time = `${hours}:${min}:${sec}`;
} else {
@ -39,6 +43,10 @@ const Time = ({ countdown, position, duration }) => {
};
const handleClick = () => {
if (!duration) {
return;
}
setShowRemaining(x => !x);
};

View File

@ -46,6 +46,7 @@ const propTypes = {
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
playerId: PropTypes.string,
};
const Track = ({
@ -70,6 +71,7 @@ const Track = ({
setPlaybackRate,
allowPlaybackRate,
buffering,
playerId,
}) => {
const { toggleLyricsModal } = useContext(AppContext);
const isPlaying = isActive && playStatus === Sound.status.PLAYING;
@ -125,6 +127,7 @@ const Track = ({
<TrackButtons
buyButtonsTarget={buyButtonsTarget}
track={track}
buyUrl={track.buyUrl}
downloadUrl={track.downloadUrl}
downloadFilename={track.downloadFilename}
@ -138,6 +141,7 @@ const Track = ({
setPlaybackRate={setPlaybackRate}
allowPlaybackRate={allowPlaybackRate}
isPlaying={isPlaying}
playerId={playerId}
/>
{hasProgressBar && (

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { CartIcon, DownloadIcon, LyricsIcon, RefreshIcon } from './Icons';
import events, { EVENT, normalizePlayerId } from '../services/events';
const propTypes = {
buyButtonsTarget: PropTypes.bool,
@ -16,6 +17,10 @@ const propTypes = {
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
isPlaying: PropTypes.bool,
track: PropTypes.shape({
audio: PropTypes.string.isRequired,
}).isRequired,
playerId: PropTypes.string,
};
const TrackButtons = ({
@ -31,6 +36,8 @@ const TrackButtons = ({
playbackRate,
allowPlaybackRate,
isPlaying,
track,
playerId,
}) => {
if (
buyUrl == null &&
@ -64,6 +71,13 @@ const TrackButtons = ({
download={downloadFilename}
className="ai-track-btn"
role="button"
onClick={() => {
events.eventTrack({
event: EVENT.DOWNLOAD,
trackUrl: track.audio,
playerId: normalizePlayerId(playerId),
});
}}
aria-label={aiStrings.download_track}
title={aiStrings.download_track}
>

View File

@ -31,6 +31,7 @@ const propTypes = {
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
repeatingTrackIndex: PropTypes.bool,
playerId: PropTypes.string,
};
const Tracklist = ({ ...props }) => {
@ -69,6 +70,7 @@ const Tracklist = ({ ...props }) => {
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
playerId={props.playerId}
/>
);
})}

View File

@ -21,6 +21,7 @@ const propTypes = {
displayArtistNames: PropTypes.bool,
onTrackLoop: PropTypes.func,
repeatingTrackIndex: PropTypes.number,
playerId: PropTypes.string,
};
const TracklistWrap = ({
@ -40,6 +41,7 @@ const TracklistWrap = ({
displayCovers,
displayArtistNames,
repeatingTrackIndex,
playerId,
}) => {
const scrollbarRef = useRef(null);
@ -82,6 +84,7 @@ const TracklistWrap = ({
displayArtistNames={displayArtistNames}
onTrackLoop={onTrackLoop}
repeatingTrackIndex={repeatingTrackIndex}
playerId={playerId}
/>
);
};

View File

@ -1,82 +0,0 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import WaveSurfer from 'wavesurfer.js';
const propTypes = {
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
audio: PropTypes.string,
setPosition: PropTypes.func.isRequired,
};
const WaveformProgressBar = ({ audio, position, duration, setPosition }) => {
const waveFormDomRef = useRef(null);
const wavesurfer = useRef(null);
useEffect(() => {
if (waveFormDomRef.current && audio) {
wavesurfer.current = WaveSurfer.create({
container: waveFormDomRef.current,
mediaControls: false,
height: 40,
barWidth: 2,
barGap: 2,
barRadius: 3,
responsive: true,
cursorWidth: 0,
backgroundColor: 'transparent',
progressColor: '#f70f5d',
waveColor: '#fff',
xhr: {
mode: 'no-cors',
},
});
wavesurfer.current.load(audio);
wavesurfer.current.on('ready', () => {
console.log('wavesurfer loaded');
});
}
return () => {
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
};
}, [audio, waveFormDomRef.current]);
useEffect(() => {
// Sync wavesurfer with current playing position
const progress = position / duration;
if (wavesurfer.current && !Number.isNaN(progress)) {
wavesurfer.current.seekTo(progress || 0);
}
}, [position]);
const handleClick = event => {
if (setPosition == null) {
return;
}
const offsetX =
event.pageX - event.currentTarget.getBoundingClientRect().left;
const posX = offsetX / event.currentTarget.offsetWidth;
setPosition(posX * duration);
};
if (!audio) {
return null;
}
return (
<div className="ai-waveform-bar" onClick={handleClick}>
<div className="ai-waveform" ref={waveFormDomRef} />
<div className="ai-waveform-progress" />
</div>
);
};
WaveformProgressBar.propTypes = propTypes;
export default WaveformProgressBar;

View File

@ -0,0 +1,144 @@
/* global aiStats */
import isStreamTrack from '../../utils/isStreamTrack';
/**
* @enum EVENT
* @type {{PAUSE: string, PLAY: string, STOP: string, DOWNLOAD: string, SEEK: string}}
*/
export const EVENT = {
PLAY: 'PLAY',
PLAYING: 'PLAYING',
PAUSE: 'PAUSE',
STOP: 'STOP',
SEEK: 'SEEK',
DOWNLOAD: 'DOWNLOAD',
};
/**
* Normalizes a player ID.
*
* @param {String} playerId The player ID
* @returns {string|null}
*/
export const normalizePlayerId = playerId => {
return playerId?.replace('audioigniter-', '') ?? null;
};
/**
* Takes state and props from soundProvider and returns the formatted event data.
* @param state
* @param props
* @returns {{duration, position, trackUrl: *, playerId: *}}
*/
export const getEventMeta = (state, props) => {
const { activeIndex, tracks, position, duration } = state;
const { playerId } = props;
const track = tracks[activeIndex];
const { title, subtitle, audio } = track ?? {};
return {
trackUrl: audio,
// trackName: subtitle ? `${title} - ${subtitle}` : title,
trackTitle: title,
trackArtist: subtitle ?? '',
playerId: normalizePlayerId(playerId),
position,
duration,
isStream: isStreamTrack(audio),
};
};
class AudioIgniterEvents {
constructor() {
this.clientId = null;
this.queue = [];
if (!window.aiStats?.enabled) {
return;
}
this.eventQueueTimer();
this.initializeFingerprint();
// Flush the entire queue when the user ends their session.
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.eventQueueFlush();
}
});
}
initializeFingerprint = async () => {
const FingerprintJS = await import(/* webpackChunkName: "fingerprintjs" */ '@fingerprintjs/fingerprintjs');
const fingerprint = await FingerprintJS.load();
const result = await fingerprint.get();
this.clientId = result.visitorId;
};
fetch = async () => {
const headers = {
type: 'application/json',
};
const blob = new Blob([JSON.stringify(this.queue)], headers);
navigator.sendBeacon(`${aiStats.apiUrl}/log`, blob);
};
eventQueueTimer = () => {
setInterval(() => {
if (this.queue.length > 0) {
this.eventQueueFlush();
}
}, 15000);
};
eventQueueFlush = async () => {
await this.fetch();
this.queue = [];
};
eventTrack = ({
event,
trackUrl,
// trackName,
trackTitle,
trackArtist,
playerId,
position,
oldPosition,
duration,
isStream,
}) => {
if (!window.aiStats?.enabled) {
return;
}
// Failsafe for multi sound pausing, some tracks
// can be paused before they start due to external
// soundManager pausing (see playTrack event in soundProvider.js).
if (event === EVENT.PAUSE && position === 0) {
return;
}
this.queue.push({
event,
track_url: trackUrl,
// track_name: trackName,
track_title: trackTitle,
track_artist: trackArtist,
playlist_id: parseInt(playerId, 10),
timestamp: new Date().getTime(),
referrer_url: window.location.href,
event_data: {
position: Math.floor(position / 1000) ?? null,
old_position:
oldPosition != null ? Math.floor(oldPosition / 1000) : null,
duration: duration ? Math.floor(duration / 1000) : null,
},
client_fingerprint: this.clientId,
is_stream: isStream,
});
};
}
export default new AudioIgniterEvents();

View File

@ -6,6 +6,8 @@ import SoundCloud from '../utils/soundcloud';
import multiSoundDisabled from '../utils/multi-sound-disabled';
import { getInitialTrackQueueAndIndex } from '../utils/getInitialTrackIndex';
import playerStorage from '../utils/playerStorage';
import aiEvents, { EVENT, getEventMeta } from './services/events';
import throttle from '../utils/throttle';
const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
@ -61,6 +63,9 @@ const soundProvider = (Player, events) => {
this.getFinalProps = this.getFinalProps.bind(this);
this.onPlaying = this.onPlaying.bind(this);
this.onFinishedPlaying = this.onFinishedPlaying.bind(this);
this.aiEventTrackThrottled = throttle(options => {
aiEvents.eventTrack(options);
}, 60 * 1000);
}
componentDidMount() {
@ -166,6 +171,14 @@ const soundProvider = (Player, events) => {
const { activeIndex } = this.state;
const { playerId, rememberLastPosition } = this.props;
if (position > 60000) {
// Only start calling this after 1 minute into the track
this.aiEventTrackThrottled({
event: EVENT.PLAYING,
...getEventMeta(this.state, this.props),
});
}
this.setState(
() => ({ duration, position }),
() => {
@ -173,8 +186,12 @@ const soundProvider = (Player, events) => {
events.onPlaying(this.getFinalProps());
}
// Store last position every 5 seconds
if (playerId && rememberLastPosition && position % 5000 < 300) {
if (
playerId &&
rememberLastPosition &&
// Store last position on every 5th second or at the beginning of the track (tiny position num).
(position % 5000 < 300 || position < 350)
) {
playerStorage.set(playerId, {
position,
activeIndex,
@ -187,7 +204,15 @@ const soundProvider = (Player, events) => {
onFinishedPlaying() {
const { stopOnTrackFinish, delayBetweenTracks = 0 } = this.props;
const delayBetweenTracksMs = delayBetweenTracks * 1000;
this.setState(() => ({ playStatus: Sound.status.STOPPED }));
this.setState(
() => ({ playStatus: Sound.status.STOPPED }),
() => {
aiEvents.eventTrack({
event: EVENT.STOP,
...getEventMeta(this.state, this.props),
});
},
);
if (stopOnTrackFinish) {
return;
@ -228,7 +253,18 @@ const soundProvider = (Player, events) => {
}
setPosition(position) {
this.setState(() => ({ position }));
const currentPosition = this.state.position;
this.setState(
() => ({ position }),
() => {
aiEvents.eventTrack({
event: EVENT.SEEK,
...getEventMeta(this.state, this.props),
oldPosition: currentPosition,
});
},
);
}
setTrackCycling(index, event) {
@ -312,19 +348,44 @@ const soundProvider = (Player, events) => {
event.preventDefault();
}
const { repeatingTrackIndex, isMultiSoundDisabled } = this.state;
const {
repeatingTrackIndex,
isMultiSoundDisabled,
playStatus,
} = this.state;
if (isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
this.setState(() => ({
activeIndex: index,
position: 0,
playStatus: Sound.status.PLAYING,
}));
if (playStatus === Sound.status.PLAYING) {
aiEvents.eventTrack({
event: EVENT.STOP,
...getEventMeta(this.state, this.props),
});
}
// Reset repating track index if the track is not the active one.
this.setState(
() => ({
activeIndex: index,
position: 0,
playStatus: Sound.status.PLAYING,
}),
() => {
aiEvents.eventTrack({
event: EVENT.PLAY,
...getEventMeta(
{
...this.state,
duration: null,
},
this.props,
),
});
},
);
// Reset repeating track index if the track is not the active one.
if (index !== repeatingTrackIndex && repeatingTrackIndex != null) {
this.setTrackCycling(null);
}
@ -338,7 +399,15 @@ const soundProvider = (Player, events) => {
const { playStatus } = this.state;
if (playStatus === Sound.status.PLAYING) {
this.setState(() => ({ playStatus: Sound.status.PAUSED }));
this.setState(
() => ({ playStatus: Sound.status.PAUSED }),
() => {
aiEvents.eventTrack({
event: EVENT.PAUSE,
...getEventMeta(this.state, this.props),
});
},
);
}
}
@ -354,18 +423,29 @@ const soundProvider = (Player, events) => {
return;
}
this.setState(({ playStatus, isMultiSoundDisabled }) => {
if (playStatus !== Sound.status.PLAYING && isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
this.setState(
({ playStatus, isMultiSoundDisabled }) => {
if (playStatus !== Sound.status.PLAYING && isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
return {
playStatus:
playStatus === Sound.status.PLAYING
? Sound.status.PAUSED
: Sound.status.PLAYING,
};
});
return {
playStatus:
playStatus === Sound.status.PLAYING
? Sound.status.PAUSED
: Sound.status.PLAYING,
};
},
() => {
aiEvents.eventTrack({
event:
this.state.playStatus === Sound.status.PLAYING
? EVENT.PLAY
: EVENT.PAUSE,
...getEventMeta(this.state, this.props),
});
},
);
}
nextTrack() {

View File

@ -0,0 +1,12 @@
/**
* Determines whether a given url is that of a stream or not.
*
* @param {string} url The url.
* @returns {boolean}
*/
const isStreamTrack = url => {
const extensions = ['.mp3', '.flac', '.amr', '.aac', '.oga', '.wav', '.wma'];
return !extensions.some(extension => url.includes(extension));
};
export default isStreamTrack;

View File

@ -0,0 +1,21 @@
/**
* Simple throttling function.
*
* @param {Function} fn The function to throttle.
* @param {number} limit The limit in milliseconds.
* @returns {(function(*): void)|*}
*/
const throttle = (fn, limit) => {
let waiting = false;
return function throttleCallback(...args) {
if (!waiting) {
fn.apply(this, args);
waiting = true;
setTimeout(() => {
waiting = false;
}, limit);
}
};
};
export default throttle;