mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-05 23:18:53 -05:00
Audio poster image is now imported as a module and referenced via AUDIO_POSTER_URL in both chapters-editor and video-editor players. Removed custom file copy plugin from Vite config, updated asset handling to use Vite's asset import and output options, and added type declarations for image modules.
198 lines
7.5 KiB
TypeScript
198 lines
7.5 KiB
TypeScript
import { useEffect, useState, useRef } from 'react';
|
|
import { formatTime } from '@/lib/timeUtils';
|
|
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
|
|
import '../styles/IOSVideoPlayer.css';
|
|
|
|
interface IOSVideoPlayerProps {
|
|
videoRef: React.RefObject<HTMLVideoElement>;
|
|
currentTime: number;
|
|
duration: number;
|
|
}
|
|
|
|
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);
|
|
|
|
// Refs for hold-to-continue functionality
|
|
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const decrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Clean up intervals on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
|
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
|
};
|
|
}, []);
|
|
|
|
// Get the video source URL from the main player
|
|
useEffect(() => {
|
|
let url = '';
|
|
if (videoRef.current && videoRef.current.querySelector('source')) {
|
|
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
|
if (source && source.src) {
|
|
url = source.src;
|
|
}
|
|
} else {
|
|
// Fallback to sample video if needed
|
|
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, 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));
|
|
}, [videoRef]);
|
|
|
|
// Function to jump 15 seconds backward
|
|
const jumpBackward15 = () => {
|
|
if (iosVideoRef) {
|
|
const newTime = Math.max(0, iosVideoRef.currentTime - 15);
|
|
iosVideoRef.currentTime = newTime;
|
|
}
|
|
};
|
|
|
|
// Function to jump 15 seconds forward
|
|
const jumpForward15 = () => {
|
|
if (iosVideoRef) {
|
|
const newTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 15);
|
|
iosVideoRef.currentTime = newTime;
|
|
}
|
|
};
|
|
|
|
// Start continuous 50ms increment when button is held
|
|
const startIncrement = (e: React.MouseEvent | React.TouchEvent) => {
|
|
// Prevent default to avoid text selection
|
|
e.preventDefault();
|
|
|
|
if (!iosVideoRef) return;
|
|
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
|
|
|
// First immediate adjustment
|
|
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
|
|
|
// Setup continuous adjustment
|
|
incrementIntervalRef.current = setInterval(() => {
|
|
if (iosVideoRef) {
|
|
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
// Stop continuous increment
|
|
const stopIncrement = () => {
|
|
if (incrementIntervalRef.current) {
|
|
clearInterval(incrementIntervalRef.current);
|
|
incrementIntervalRef.current = null;
|
|
}
|
|
};
|
|
|
|
// Start continuous 50ms decrement when button is held
|
|
const startDecrement = (e: React.MouseEvent | React.TouchEvent) => {
|
|
// Prevent default to avoid text selection
|
|
e.preventDefault();
|
|
|
|
if (!iosVideoRef) return;
|
|
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
|
|
|
// First immediate adjustment
|
|
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
|
|
|
// Setup continuous adjustment
|
|
decrementIntervalRef.current = setInterval(() => {
|
|
if (iosVideoRef) {
|
|
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
// Stop continuous decrement
|
|
const stopDecrement = () => {
|
|
if (decrementIntervalRef.current) {
|
|
clearInterval(decrementIntervalRef.current);
|
|
decrementIntervalRef.current = null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="ios-video-player-container">
|
|
{/* Current Time / Duration Display */}
|
|
<div className="ios-time-display mb-2">
|
|
<span className="text-sm">
|
|
{formatTime(currentTime)} / {formatTime(duration)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* iOS-optimized Video Element with Native Controls */}
|
|
<video
|
|
ref={(ref) => setIosVideoRef(ref)}
|
|
className="w-full rounded-md"
|
|
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>
|
|
|
|
{/* iOS Video Skip Controls */}
|
|
<div className="ios-skip-controls mt-3 flex justify-center gap-4">
|
|
<button
|
|
onClick={jumpBackward15}
|
|
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
|
>
|
|
-15s
|
|
</button>
|
|
<button
|
|
onClick={jumpForward15}
|
|
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
|
>
|
|
+15s
|
|
</button>
|
|
</div>
|
|
|
|
{/* iOS Fine Control Buttons */}
|
|
<div className="ios-fine-controls mt-2 flex justify-center gap-4">
|
|
<button
|
|
onMouseDown={startDecrement}
|
|
onTouchStart={startDecrement}
|
|
onMouseUp={stopDecrement}
|
|
onMouseLeave={stopDecrement}
|
|
onTouchEnd={stopDecrement}
|
|
onTouchCancel={stopDecrement}
|
|
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
|
>
|
|
-50ms
|
|
</button>
|
|
<button
|
|
onMouseDown={startIncrement}
|
|
onTouchStart={startIncrement}
|
|
onMouseUp={stopIncrement}
|
|
onMouseLeave={stopIncrement}
|
|
onTouchEnd={stopIncrement}
|
|
onTouchCancel={stopIncrement}
|
|
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
|
>
|
|
+50ms
|
|
</button>
|
|
</div>
|
|
|
|
<div className="ios-note mt-2 text-xs text-gray-500">
|
|
<p>This player uses native iOS controls for better compatibility with iOS devices.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IOSVideoPlayer;
|