Add audio poster support for audio files in video players

Introduces an audio-poster.jpg image and updates both chapters and video editor React video player components to display a poster image for audio files when no poster is provided. Also adds a posterUrl field to MEDIA_DATA and ensures fallback logic for poster images is consistent across iOS and standard video players.
This commit is contained in:
Yiannis Christodoulou 2025-10-19 13:56:17 +03:00
parent bfcb774183
commit 54e2f1d7e4
13 changed files with 133 additions and 93 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

View File

@ -11,6 +11,7 @@ interface IOSVideoPlayerProps {
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(''); const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -26,15 +27,24 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
let url = '';
if (videoRef.current && videoRef.current.querySelector('source')) { if (videoRef.current && videoRef.current.querySelector('source')) {
const source = videoRef.current.querySelector('source') as HTMLSourceElement; const source = videoRef.current.querySelector('source') as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
setVideoUrl(source.src); url = source.src;
} }
} else { } else {
// Fallback to sample video if needed // Fallback to sample video if needed
setVideoUrl('/videos/sample-video.mp4'); url = '/videos/sample-video.mp4';
} }
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
setPosterImage(mediaPosterUrl || (isAudioFile ? '/audio-poster.jpg' : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -127,6 +137,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
x-webkit-airplay="allow" x-webkit-airplay="allow"
preload="auto" preload="auto"
crossOrigin="anonymous" crossOrigin="anonymous"
poster={posterImage}
> >
<source src={videoUrl} type="video/mp4" /> <source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

View File

@ -38,6 +38,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4'; (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
// Check if the media is an audio file
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const posterImage = mediaPosterUrl || (isAudioFile ? '/audio-poster.jpg' : undefined);
// Detect iOS device and Safari browser // Detect iOS device and Safari browser
useEffect(() => { useEffect(() => {
const checkIOS = () => { const checkIOS = () => {
@ -354,6 +361,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
x-webkit-airplay="allow" x-webkit-airplay="allow"
controls={false} controls={false}
muted={isMuted} muted={isMuted}
poster={posterImage}
> >
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
{/* Safari fallback for audio files */} {/* Safari fallback for audio files */}

View File

@ -6,6 +6,7 @@ if (typeof window !== 'undefined') {
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: '', videoUrl: '',
mediaId: '', mediaId: '',
posterUrl: ''
}; };
window.lastSeekedPosition = 0; window.lastSeekedPosition = 0;
} }
@ -15,6 +16,7 @@ declare global {
MEDIA_DATA: { MEDIA_DATA: {
videoUrl: string; videoUrl: string;
mediaId: string; mediaId: string;
posterUrl?: string;
}; };
seekToFunction?: (time: number) => void; seekToFunction?: (time: number) => void;
lastSeekedPosition: number; lastSeekedPosition: number;

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

View File

@ -11,6 +11,7 @@ interface IOSVideoPlayerProps {
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(''); const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -26,15 +27,24 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
let url = '';
if (videoRef.current && videoRef.current.querySelector('source')) { if (videoRef.current && videoRef.current.querySelector('source')) {
const source = videoRef.current.querySelector('source') as HTMLSourceElement; const source = videoRef.current.querySelector('source') as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
setVideoUrl(source.src); url = source.src;
} }
} else { } else {
// Fallback to sample video if needed // Fallback to sample video if needed
setVideoUrl('/videos/sample-video.mp4'); url = '/videos/sample-video.mp3';
} }
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
setPosterImage(mediaPosterUrl || (isAudioFile ? '/audio-poster.jpg' : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -127,6 +137,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
x-webkit-airplay="allow" x-webkit-airplay="allow"
preload="auto" preload="auto"
crossOrigin="anonymous" crossOrigin="anonymous"
poster={posterImage}
> >
<source src={videoUrl} type="video/mp4" /> <source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

View File

@ -36,7 +36,14 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [tooltipTime, setTooltipTime] = useState(0); const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4'; (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp3';
// Check if the media is an audio file
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const posterImage = mediaPosterUrl || (isAudioFile ? '/audio-poster.jpg' : undefined);
// Detect iOS device // Detect iOS device
useEffect(() => { useEffect(() => {
@ -344,6 +351,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
x-webkit-airplay="allow" x-webkit-airplay="allow"
controls={false} controls={false}
muted={isMuted} muted={isMuted}
poster={posterImage}
> >
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long