Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions src/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,21 @@ export const App = () => {
return (
<Wrapper>
<DatabaseContextProvider>
<AudioContextProvider>
<>
<Header />
<div className="app-body">
<h1>transcript.fish</h1>
<UnderConstructionBanner />
<img className="logo" src={mediaUrl.images('logo-transparent.png')} />
<ErrorBoundary FallbackComponent={EpisodeSearchFallback}>
<FiltersContextProvider>
<FiltersContextProvider>
<AudioContextProvider>
<>
<Header />
<div className="app-body">
<h1>transcript.fish</h1>
<UnderConstructionBanner />
<img className="logo" src={mediaUrl.images('logo-transparent.png')} />
<ErrorBoundary FallbackComponent={EpisodeSearchFallback}>
<Outlet />
</FiltersContextProvider>
</ErrorBoundary>
</div>
</>
</AudioContextProvider>
</ErrorBoundary>
</div>
</>
</AudioContextProvider>
</FiltersContextProvider>
</DatabaseContextProvider>
</Wrapper>
);
Expand Down
6 changes: 4 additions & 2 deletions src/js/audio/AudioContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { Ref, createContext } from 'react';

export const AudioContext = createContext<{
isPlaying: (episodeNum: number) => boolean;
playPause: (episodeNum: number) => void;
play: (episodeNum: number) => void;
pause: () => void;
audioRef?: Ref<HTMLAudioElement>;
playingEpisode?: number;
currentTime: number;
seek: (time: number) => void;
ended: boolean;
}>({
isPlaying: () => false,
playPause: () => undefined,
play: () => undefined,
pause: () => undefined,
audioRef: null,
playingEpisode: undefined,
currentTime: 0,
Expand Down
150 changes: 108 additions & 42 deletions src/js/audio/AudioContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,151 @@
import { ReactElement, useEffect, useRef, useState } from 'react';
import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { AudioPlayer } from './AudioPlayer';
import { AudioContext } from './AudioContext';
import { DatabaseContext } from '../database/DatabaseContext';
import { setMetadata } from './audioUtils';
import { FiltersContext } from '../filters/FiltersContext';

export const AudioContextProvider = ({ children }: { children: ReactElement }) => {
const audioRef = useRef<HTMLAudioElement>(null);
const { episodes } = useContext(DatabaseContext);
const { getFilteredEpisodes } = useContext(FiltersContext);
const [playingEpisode, setPlayingEpisode] = useState<number>();
const [ended, setEnded] = useState(false);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const playPause = (episodeNum: number) => {
setPlaying(p => {
if (episodeNum === playingEpisode) {
return !p;
}
const audioRef = useRef<HTMLAudioElement>(null);

setPlayingEpisode(episodeNum);
return true;
});
};
const play = useCallback((episodeNum: number) => {
setPlaying(true);
setPlayingEpisode(episodeNum);
audioRef.current?.play();
}, []);

const isPlaying = (episodeNum: number) => {
return playing && episodeNum === playingEpisode;
};
const pause = useCallback(() => {
setPlaying(false);
audioRef.current?.pause();
}, []);

const seek = (time: number) => {
const isPlaying = useCallback(
(episodeNum: number) => {
return playing && episodeNum === playingEpisode;
},
[playing, playingEpisode]
);

const seek = useCallback((time: number) => {
if (audioRef.current) {
audioRef.current.currentTime = time;
audioRef.current.fastSeek(time);
}
};
}, []);

const filteredEpisodes = getFilteredEpisodes(episodes.data);

// Add event listeners
useEffect(() => {
const audio = audioRef.current;
if (!audio || !playingEpisode) {
return;
}

if (!filteredEpisodes) {
return;
}

audio.playbackRate = 1;
audio.preservesPitch = true;

const episodeIdx = filteredEpisodes?.findIndex(({ episode }) => episode === playingEpisode);

navigator.mediaSession.setActionHandler('play', () => play(playingEpisode));
navigator.mediaSession.setActionHandler('pause', pause);

// Show back button on everything but the first episode
if (episodeIdx > 0) {
navigator.mediaSession.setActionHandler('previoustrack', () => {
if (typeof episodeIdx === 'number') {
setPlayingEpisode(filteredEpisodes[episodeIdx - 1].episode);
setCurrentTime(0);
setPlaying(true);
audio.currentTime = 0;
}
});
} else {
navigator.mediaSession.setActionHandler('previoustrack', null);
}

// Show foward button on everything but the last episode
if (episodeIdx < filteredEpisodes.length - 1) {
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (typeof episodeIdx === 'number') {
setPlayingEpisode(filteredEpisodes[episodeIdx + 1].episode);
setCurrentTime(0);
setPlaying(true);
audio.currentTime = 0;
}
});
} else {
navigator.mediaSession.setActionHandler('nexttrack', null);
}

navigator.mediaSession.setActionHandler('seekbackward', () => {
setCurrentTime(t => {
const seekTime = t - 10_000;
audio.fastSeek(seekTime);
return seekTime;
});
});

navigator.mediaSession.setActionHandler('seekforward', () => {
setCurrentTime(t => {
const seekTime = t + 10_000;
audio.fastSeek(seekTime);
return seekTime;
});
});

navigator.mediaSession.setActionHandler('seekto', ({ seekTime }) => {
if (seekTime) {
setCurrentTime(seekTime);
audio.fastSeek(seekTime);
}
});

const handleTimeupdate = (event: Event) => {
const { currentTime } = event.target as HTMLAudioElement;
setCurrentTime(currentTime);
setEnded(false);
};
const handlePlay = () => setPlaying(true);
const handlePause = () => setPlaying(false);
const handleEnded = () => setEnded(true);

audio?.addEventListener('timeupdate', handleTimeupdate);
audio?.addEventListener('play', handlePlay);
audio?.addEventListener('pause', handlePause);
audio?.addEventListener('ended', handleEnded);
audio?.addEventListener('seeked', handleTimeupdate);
const handleEnded = () => {
setEnded(true);
setPlaying(false);
};

audio.addEventListener('timeupdate', handleTimeupdate);
audio.addEventListener('seeked', handleTimeupdate);
audio.addEventListener('ended', handleEnded);

return () => {
audio?.removeEventListener('timeupdate', handleTimeupdate);
audio?.removeEventListener('play', handlePlay);
audio?.removeEventListener('pause', handlePause);
audio?.removeEventListener('ended', handleEnded);
audio?.removeEventListener('seeked', handleTimeupdate);
audio.removeEventListener('timeupdate', handleTimeupdate);
audio.removeEventListener('seeked', handleTimeupdate);
audio.removeEventListener('ended', handleEnded);
};
}, [playingEpisode]);
}, [playingEpisode, play, pause, filteredEpisodes, getFilteredEpisodes]);

// Update MediaSession metadata when episode is changed
useEffect(() => {
const audio = audioRef.current;
if (!audio || !playingEpisode) {
return;
}

if (playing) {
audio.play();
} else {
audio.pause();
const episode = filteredEpisodes?.find(({ episode }) => episode === playingEpisode);
if (episode) {
setMetadata(episode);
}
}, [playingEpisode, playing]);
}, [playingEpisode, filteredEpisodes]);

return (
<AudioContext.Provider
value={{
isPlaying,
playPause,
play,
pause,
playingEpisode,
currentTime,
seek,
Expand Down
14 changes: 10 additions & 4 deletions src/js/audio/AudioControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ interface AudioControlsProps {
}

export const AudioControls = ({ episodeNum, duration }: AudioControlsProps) => {
const { isPlaying, playPause, currentTime, playingEpisode, seek, ended } =
const { isPlaying, play, pause, currentTime, playingEpisode, seek, ended } =
useContext(AudioContext);

const handleSkipBack: MouseEventHandler<HTMLSpanElement> = ({ clientX, currentTarget }) => {
Expand All @@ -160,9 +160,15 @@ export const AudioControls = ({ episodeNum, duration }: AudioControlsProps) => {
return (
<Wrapper>
<ButtonWrapper>
<Button onClick={() => playPause(episodeNum)}>
{isPlaying(episodeNum) ? <PauseIcon /> : <PlayIcon />}
</Button>
{isPlaying(episodeNum) ? (
<Button onClick={pause}>
<PauseIcon />
</Button>
) : (
<Button onClick={() => play(episodeNum)}>
<PlayIcon />
</Button>
)}
</ButtonWrapper>
<DurationWrapper>
{playingEpisode === episodeNum ? (
Expand Down
13 changes: 13 additions & 0 deletions src/js/audio/audioUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Episode } from '../types';
import { formatDate } from '../utils';

export const setMetadata = (episode: Episode) => {
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: `${episode?.episode}: ${episode?.title}`,
artist: 'No Such Thing As A Fish',
album: episode?.pubDate ? formatDate(episode.pubDate) : undefined,
artwork: [{ src: episode?.image || '', sizes: '512x512', type: 'image/png' }],
});
}
};