Fix audio poster display for Safari in video players (144)

Adds persistent background poster images for audio files in both VideoPlayer and IOSVideoPlayer components to address Safari rendering issues. Updates CSS and component logic to ensure poster images remain visible for audio files, improving user experience and visual consistency.
This commit is contained in:
Yiannis Christodoulou 2025-11-09 21:54:18 +02:00
parent 3abc012de1
commit 9a8a34317d
8 changed files with 188 additions and 38 deletions

View File

@ -13,6 +13,7 @@ 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); const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// 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);
@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
setVideoUrl(url); setVideoUrl(url);
// Check if the media is an audio file and set poster image // Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null; const audioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
setIsAudioFile(audioFile);
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined)); setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -128,22 +130,34 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span> </span>
</div> </div>
{/* iOS-optimized Video Element with Native Controls */} {/* Video container with persistent background for audio files */}
<video <div className="ios-video-wrapper">
ref={(ref) => setIosVideoRef(ref)} {/* Persistent background image for audio files (Safari fix) */}
className="w-full rounded-md" {isAudioFile && posterImage && (
src={videoUrl} <div
controls className="ios-audio-poster-background"
playsInline style={{ backgroundImage: `url(${posterImage})` }}
webkit-playsinline="true" aria-hidden="true"
x-webkit-airplay="allow" />
preload="auto" )}
crossOrigin="anonymous"
poster={posterImage} {/* iOS-optimized Video Element with Native Controls */}
> <video
<source src={videoUrl} type="video/mp4" /> ref={(ref) => setIosVideoRef(ref)}
<p>Your browser doesn't support HTML5 video.</p> className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
</video> src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */} {/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4"> <div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return ( return (
<div className="video-player-container"> <div className="video-player-container">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
<video <video
ref={videoRef} ref={videoRef}
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata" preload="metadata"
crossOrigin="anonymous" crossOrigin="anonymous"
onClick={handleVideoClick} onClick={handleVideoClick}

View File

@ -8,12 +8,40 @@
overflow: hidden; overflow: hidden;
} }
/* Video wrapper for positioning background */
.ios-video-wrapper {
position: relative;
width: 100%;
background-color: black;
border-radius: 0.5rem;
overflow: hidden;
}
/* Persistent background poster for audio files (Safari fix) */
.ios-audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
}
.ios-video-player-container video { .ios-video-player-container video {
position: relative;
width: 100%; width: 100%;
height: auto; height: auto;
max-height: 360px; max-height: 360px;
aspect-ratio: 16/9; aspect-ratio: 16/9;
background-color: black; }
/* Make video transparent only for audio files with poster so background shows through */
.ios-video-player-container video.audio-with-poster {
background-color: transparent;
} }
.ios-time-display { .ios-time-display {

View File

@ -76,10 +76,26 @@
user-select: none; user-select: none;
} }
/* Persistent background poster for audio files (Safari fix) */
.audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
.video-player-container video { .video-player-container video {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
z-index: 2;
/* Force hardware acceleration */ /* Force hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
-webkit-transform: translateZ(0); -webkit-transform: translateZ(0);
@ -88,6 +104,11 @@
user-select: none; user-select: none;
} }
/* Make video transparent only for audio files with poster so background shows through */
.video-player-container video.audio-with-poster {
background: transparent;
}
/* iOS-specific styles */ /* iOS-specific styles */
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {
.video-player-container video { .video-player-container video {
@ -109,6 +130,7 @@
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
pointer-events: none; pointer-events: none;
z-index: 3;
} }
.video-player-container:hover .play-pause-indicator { .video-player-container:hover .play-pause-indicator {
@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
z-index: 3;
} }
.video-player-container:hover .video-controls { .video-player-container:hover .video-controls {

View File

@ -13,6 +13,7 @@ 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); const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// 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);
@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
setVideoUrl(url); setVideoUrl(url);
// Check if the media is an audio file and set poster image // Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null; const audioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
setIsAudioFile(audioFile);
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined)); setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -128,22 +130,34 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span> </span>
</div> </div>
{/* iOS-optimized Video Element with Native Controls */} {/* Video container with persistent background for audio files */}
<video <div className="ios-video-wrapper">
ref={(ref) => setIosVideoRef(ref)} {/* Persistent background image for audio files (Safari fix) */}
className="w-full rounded-md" {isAudioFile && posterImage && (
src={videoUrl} <div
controls className="ios-audio-poster-background"
playsInline style={{ backgroundImage: `url(${posterImage})` }}
webkit-playsinline="true" aria-hidden="true"
x-webkit-airplay="allow" />
preload="auto" )}
crossOrigin="anonymous"
poster={posterImage} {/* iOS-optimized Video Element with Native Controls */}
> <video
<source src={videoUrl} type="video/mp4" /> ref={(ref) => setIosVideoRef(ref)}
<p>Your browser doesn't support HTML5 video.</p> className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
</video> src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */} {/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4"> <div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return ( return (
<div className="video-player-container"> <div className="video-player-container">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
<video <video
ref={videoRef} ref={videoRef}
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata" preload="metadata"
crossOrigin="anonymous" crossOrigin="anonymous"
onClick={handleVideoClick} onClick={handleVideoClick}

View File

@ -8,12 +8,40 @@
overflow: hidden; overflow: hidden;
} }
/* Video wrapper for positioning background */
.ios-video-wrapper {
position: relative;
width: 100%;
background-color: black;
border-radius: 0.5rem;
overflow: hidden;
}
/* Persistent background poster for audio files (Safari fix) */
.ios-audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
}
.ios-video-player-container video { .ios-video-player-container video {
position: relative;
width: 100%; width: 100%;
height: auto; height: auto;
max-height: 360px; max-height: 360px;
aspect-ratio: 16/9; aspect-ratio: 16/9;
background-color: black; }
/* Make video transparent only for audio files with poster so background shows through */
.ios-video-player-container video.audio-with-poster {
background-color: transparent;
} }
.ios-time-display { .ios-time-display {

View File

@ -76,10 +76,26 @@
user-select: none; user-select: none;
} }
/* Persistent background poster for audio files (Safari fix) */
.audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
.video-player-container video { .video-player-container video {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
z-index: 2;
/* Force hardware acceleration */ /* Force hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
-webkit-transform: translateZ(0); -webkit-transform: translateZ(0);
@ -88,6 +104,11 @@
user-select: none; user-select: none;
} }
/* Make video transparent only for audio files with poster so background shows through */
.video-player-container video.audio-with-poster {
background: transparent;
}
/* iOS-specific styles */ /* iOS-specific styles */
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {
.video-player-container video { .video-player-container video {
@ -109,6 +130,7 @@
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
pointer-events: none; pointer-events: none;
z-index: 3;
} }
.video-player-container:hover .play-pause-indicator { .video-player-container:hover .play-pause-indicator {
@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
z-index: 3;
} }
.video-player-container:hover .video-controls { .video-player-container:hover .video-controls {