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 [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
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;
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"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]);
// Function to jump 15 seconds backward
@ -128,10 +130,21 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span>
</div>
{/* Video container with persistent background for audio files */}
<div className="ios-video-wrapper">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="ios-audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md"
className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
src={videoUrl}
controls
playsInline
@ -144,6 +157,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return (
<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
ref={videoRef}
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata"
crossOrigin="anonymous"
onClick={handleVideoClick}

View File

@ -8,12 +8,40 @@
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 {
position: relative;
width: 100%;
height: auto;
max-height: 360px;
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 {

View File

@ -76,10 +76,26 @@
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 {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 2;
/* Force hardware acceleration */
transform: translateZ(0);
-webkit-transform: translateZ(0);
@ -88,6 +104,11 @@
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 */
@supports (-webkit-touch-callout: none) {
.video-player-container video {
@ -109,6 +130,7 @@
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 3;
}
.video-player-container:hover .play-pause-indicator {
@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0;
transition: opacity 0.3s;
z-index: 3;
}
.video-player-container:hover .video-controls {

View File

@ -13,6 +13,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
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;
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"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]);
// Function to jump 15 seconds backward
@ -128,10 +130,21 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span>
</div>
{/* Video container with persistent background for audio files */}
<div className="ios-video-wrapper">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="ios-audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md"
className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
src={videoUrl}
controls
playsInline
@ -144,6 +157,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return (
<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
ref={videoRef}
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata"
crossOrigin="anonymous"
onClick={handleVideoClick}

View File

@ -8,12 +8,40 @@
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 {
position: relative;
width: 100%;
height: auto;
max-height: 360px;
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 {

View File

@ -76,10 +76,26 @@
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 {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 2;
/* Force hardware acceleration */
transform: translateZ(0);
-webkit-transform: translateZ(0);
@ -88,6 +104,11 @@
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 */
@supports (-webkit-touch-callout: none) {
.video-player-container video {
@ -109,6 +130,7 @@
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 3;
}
.video-player-container:hover .play-pause-indicator {
@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0;
transition: opacity 0.3s;
z-index: 3;
}
.video-player-container:hover .video-controls {