|
|
Line 7: |
Line 7: |
| <includeonly> | | <includeonly> |
| <html> | | <html> |
|
| |
|
| |
| <style> | | <style> |
| button { | | button { |
Line 190: |
Line 188: |
| } | | } |
| </style> | | </style> |
| <script>
| |
| import lottieWeb from 'https://cdn.skypack.dev/lottie-web';
| |
|
| |
| class AudioPlayer extends HTMLElement {
| |
| constructor() {
| |
| super();
| |
| const template = document.querySelector('template');
| |
| const templateContent = template.content;
| |
| const shadow = this.attachShadow({mode: 'open'});
| |
| shadow.appendChild(templateContent.cloneNode(true));
| |
| }
| |
|
| |
| connectedCallback() {
| |
| everything(this);
| |
| }
| |
| }
| |
|
| |
| const everything = function(element) {
| |
| const shadow = element.shadowRoot;
| |
|
| |
| const audioPlayerContainer = shadow.getElementById('audio-player-container');
| |
| const playIconContainer = shadow.getElementById('play-icon');
| |
| const seekSlider = shadow.getElementById('seek-slider');
| |
| const volumeSlider = shadow.getElementById('volume-slider');
| |
| const muteIconContainer = shadow.getElementById('mute-icon');
| |
| const audio = shadow.querySelector('audio');
| |
| const durationContainer = shadow.getElementById('duration');
| |
| const currentTimeContainer = shadow.getElementById('current-time');
| |
| const outputContainer = shadow.getElementById('volume-output');
| |
| let playState = 'play';
| |
| let muteState = 'unmute';
| |
| let raf = null;
| |
|
| |
| audio.src = element.getAttribute('data-src');
| |
|
| |
| const playAnimation = lottieWeb.loadAnimation({
| |
| container: playIconContainer,
| |
| path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/pause/pause.json',
| |
| renderer: 'svg',
| |
| loop: false,
| |
| autoplay: false,
| |
| name: "Play Animation",
| |
| });
| |
|
| |
| const muteAnimation = lottieWeb.loadAnimation({
| |
| container: muteIconContainer,
| |
| path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/mute/mute.json',
| |
| renderer: 'svg',
| |
| loop: false,
| |
| autoplay: false,
| |
| name: "Mute Animation",
| |
| });
| |
|
| |
| playAnimation.goToAndStop(14, true);
| |
|
| |
| const whilePlaying = () => {
| |
| seekSlider.value = Math.floor(audio.currentTime);
| |
| currentTimeContainer.textContent = calculateTime(seekSlider.value);
| |
| audioPlayerContainer.style.setProperty('--seek-before-width', `${seekSlider.value / seekSlider.max * 100}%`);
| |
| raf = requestAnimationFrame(whilePlaying);
| |
| }
| |
|
| |
| const showRangeProgress = (rangeInput) => {
| |
| if(rangeInput === seekSlider) audioPlayerContainer.style.setProperty('--seek-before-width', rangeInput.value / rangeInput.max * 100 + '%');
| |
| else audioPlayerContainer.style.setProperty('--volume-before-width', rangeInput.value / rangeInput.max * 100 + '%');
| |
| }
| |
|
| |
| const calculateTime = (secs) => {
| |
| const minutes = Math.floor(secs / 60);
| |
| const seconds = Math.floor(secs % 60);
| |
| const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
| |
| return `${minutes}:${returnedSeconds}`;
| |
| }
| |
|
| |
| const displayDuration = () => {
| |
| durationContainer.textContent = calculateTime(audio.duration);
| |
| }
| |
|
| |
| const setSliderMax = () => {
| |
| seekSlider.max = Math.floor(audio.duration);
| |
| }
| |
|
| |
| const displayBufferedAmount = () => {
| |
| const bufferedAmount = Math.floor(audio.buffered.end(audio.buffered.length - 1));
| |
| audioPlayerContainer.style.setProperty('--buffered-width', `${(bufferedAmount / seekSlider.max) * 100}%`);
| |
| }
| |
|
| |
| if (audio.readyState > 0) {
| |
| displayDuration();
| |
| setSliderMax();
| |
| displayBufferedAmount();
| |
| } else {
| |
| audio.addEventListener('loadedmetadata', () => {
| |
| displayDuration();
| |
| setSliderMax();
| |
| displayBufferedAmount();
| |
| });
| |
| }
| |
|
| |
| playIconContainer.addEventListener('click', () => {
| |
| if(playState === 'play') {
| |
| audio.play();
| |
| playAnimation.playSegments([14, 27], true);
| |
| requestAnimationFrame(whilePlaying);
| |
| playState = 'pause';
| |
| } else {
| |
| audio.pause();
| |
| playAnimation.playSegments([0, 14], true);
| |
| cancelAnimationFrame(raf);
| |
| playState = 'play';
| |
| }
| |
| });
| |
|
| |
| muteIconContainer.addEventListener('click', () => {
| |
| if(muteState === 'unmute') {
| |
| muteAnimation.playSegments([0, 15], true);
| |
| audio.muted = true;
| |
| muteState = 'mute';
| |
| } else {
| |
| muteAnimation.playSegments([15, 25], true);
| |
| audio.muted = false;
| |
| muteState = 'unmute';
| |
| }
| |
| });
| |
|
| |
| audio.addEventListener('progress', displayBufferedAmount);
| |
|
| |
| seekSlider.addEventListener('input', (e) => {
| |
| showRangeProgress(e.target);
| |
| currentTimeContainer.textContent = calculateTime(seekSlider.value);
| |
| if(!audio.paused) {
| |
| cancelAnimationFrame(raf);
| |
| }
| |
| });
| |
|
| |
| seekSlider.addEventListener('change', () => {
| |
| audio.currentTime = seekSlider.value;
| |
| if(!audio.paused) {
| |
| requestAnimationFrame(whilePlaying);
| |
| }
| |
| });
| |
|
| |
| volumeSlider.addEventListener('input', (e) => {
| |
| const value = e.target.value;
| |
| showRangeProgress(e.target);
| |
| outputContainer.textContent = value;
| |
| audio.volume = value / 100;
| |
| });
| |
|
| |
| if('mediaSession' in navigator) {
| |
| navigator.mediaSession.metadata = new MediaMetadata({
| |
| title: 'Komorebi',
| |
| artist: 'Anitek',
| |
| album: 'MainStay',
| |
| artwork: [
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '96x96', type: 'image/png' },
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '128x128', type: 'image/png' },
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '192x192', type: 'image/png' },
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '256x256', type: 'image/png' },
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '384x384', type: 'image/png' },
| |
| { src: 'https://assets.codepen.io/4358584/1.300.jpg', sizes: '512x512', type: 'image/png' }
| |
| ]
| |
| });
| |
| navigator.mediaSession.setActionHandler('play', () => {
| |
| if(playState === 'play') {
| |
| audio.play();
| |
| playAnimation.playSegments([14, 27], true);
| |
| requestAnimationFrame(whilePlaying);
| |
| playState = 'pause';
| |
| } else {
| |
| audio.pause();
| |
| playAnimation.playSegments([0, 14], true);
| |
| cancelAnimationFrame(raf);
| |
| playState = 'play';
| |
| }
| |
| });
| |
| navigator.mediaSession.setActionHandler('pause', () => {
| |
| if(playState === 'play') {
| |
| audio.play();
| |
| playAnimation.playSegments([14, 27], true);
| |
| requestAnimationFrame(whilePlaying);
| |
| playState = 'pause';
| |
| } else {
| |
| audio.pause();
| |
| playAnimation.playSegments([0, 14], true);
| |
| cancelAnimationFrame(raf);
| |
| playState = 'play';
| |
| }
| |
| });
| |
| navigator.mediaSession.setActionHandler('seekbackward', (details) => {
| |
| audio.currentTime = audio.currentTime - (details.seekOffset || 10);
| |
| });
| |
| navigator.mediaSession.setActionHandler('seekforward', (details) => {
| |
| audio.currentTime = audio.currentTime + (details.seekOffset || 10);
| |
| });
| |
| navigator.mediaSession.setActionHandler('seekto', (details) => {
| |
| if (details.fastSeek && 'fastSeek' in audio) {
| |
| audio.fastSeek(details.seekTime);
| |
| return;
| |
| }
| |
| audio.currentTime = details.seekTime;
| |
| });
| |
| navigator.mediaSession.setActionHandler('stop', () => {
| |
| audio.currentTime = 0;
| |
| seekSlider.value = 0;
| |
| audioPlayerContainer.style.setProperty('--seek-before-width', '0%');
| |
| currentTimeContainer.textContent = '0:00';
| |
| if(playState === 'pause') {
| |
| playAnimation.playSegments([0, 14], true);
| |
| cancelAnimationFrame(raf);
| |
| playState = 'play';
| |
| }
| |
| });
| |
| }
| |
| }
| |
|
| |
| customElements.define('audio-player', AudioPlayer);
| |
| </script>
| |
| <div id="audio-player-container"> | | <div id="audio-player-container"> |
| <audio src="" preload="metadata" loop></audio> | | <audio src="" preload="metadata" loop></audio> |