Parcourir la source

feat: mobile video player controls + comment panel space

- Centered overlay controls (prev frame | play | next frame) inside video frame
- Volume icon cycles through 4 levels (0-3 bars) on tap
- Volume slider and speed selector hidden on mobile
- Draw toolbar uses flex-wrap for second-row wrapping
- Removed timecode span from bottom controls
- Portrait: video 45vh (was 60vh), comment panel 55vh (was 40vh)
- Input area padding increased to p-4

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Dev il y a 1 mois
Parent
commit
c84eb16c01
2 fichiers modifiés avec 148 ajouts et 124 suppressions
  1. 3 3
      src/app/review/[assetId]/page.tsx
  2. 145 121
      src/components/video-player/VideoPlayer.tsx

+ 3 - 3
src/app/review/[assetId]/page.tsx

@@ -452,7 +452,7 @@ export default function ReviewPage() {
         <div
         <div
           className="overflow-y-auto p-3 sm:p-4 flex flex-col gap-3 min-w-0"
           className="overflow-y-auto p-3 sm:p-4 flex flex-col gap-3 min-w-0"
           style={isPortrait
           style={isPortrait
-            ? { flex: 'none', width: '100%', minHeight: '60vh' }
+            ? { flex: 'none', width: '100%', minHeight: '45vh' }
             : { flex: 1, overflowY: 'auto' }}
             : { flex: 1, overflowY: 'auto' }}
         >
         >
 
 
@@ -564,7 +564,7 @@ export default function ReviewPage() {
             ? {
             ? {
                 flex: 1,
                 flex: 1,
                 width: '100%',
                 width: '100%',
-                minHeight: '40vh',
+                minHeight: '55vh',
                 background: 'rgba(10,11,20,0.98)',
                 background: 'rgba(10,11,20,0.98)',
                 borderTop: '1px solid rgba(255,255,255,0.06)',
                 borderTop: '1px solid rgba(255,255,255,0.06)',
               }
               }
@@ -673,7 +673,7 @@ export default function ReviewPage() {
           </div>
           </div>
 
 
           {/* New comment / reply input */}
           {/* New comment / reply input */}
-          <div className="shrink-0 p-3"
+          <div className="shrink-0 p-4"
                style={{ borderTop: '1px solid rgba(255,255,255,0.06)', background: 'rgba(10,11,20,0.80)' }}>
                style={{ borderTop: '1px solid rgba(255,255,255,0.06)', background: 'rgba(10,11,20,0.80)' }}>
             {replyTo && (
             {replyTo && (
               <div className="flex items-center gap-2 mb-2 text-xs" style={{ color: 'var(--text-muted)' }}>
               <div className="flex items-center gap-2 mb-2 text-xs" style={{ color: 'var(--text-muted)' }}>

+ 145 - 121
src/components/video-player/VideoPlayer.tsx

@@ -61,18 +61,17 @@ export function VideoPlayer({
   const [fullscreen, setFullscreen] = useState(false);
   const [fullscreen, setFullscreen] = useState(false);
   const [showControls, setShowControls] = useState(true);
   const [showControls, setShowControls] = useState(true);
   const [dims, setDims] = useState({ width: 0, height: 0 });
   const [dims, setDims] = useState({ width: 0, height: 0 });
-  // Speech bubble state — derived from currentTime (auto-show within ±1s of comment)
-  const BUBBLE_WINDOW = 1; // seconds before/after comment timestamp
+  // Speech bubble — auto-show within ±1s of comment timestamp
+  const BUBBLE_WINDOW = 1;
   const [dismissedSet, setDismissedSet] = useState<Set<string>>(new Set());
   const [dismissedSet, setDismissedSet] = useState<Set<string>>(new Set());
   const dismissedRef = useRef<Set<string>>(new Set());
   const dismissedRef = useRef<Set<string>>(new Set());
   useEffect(() => { dismissedRef.current = dismissedSet; }, [dismissedSet]);
   useEffect(() => { dismissedRef.current = dismissedSet; }, [dismissedSet]);
   const [bubbleVisible, setBubbleVisible] = useState(false);
   const [bubbleVisible, setBubbleVisible] = useState(false);
 
 
-  // Auto-detect which comment (if any) is within the ±1.5s window of currentTime
+  // Active comment within bubble window
   const activeComment: Comment | null = (() => {
   const activeComment: Comment | null = (() => {
     if (!bubbleVisible) return null;
     if (!bubbleVisible) return null;
     const ts = comments.filter(c => c.timestamp != null && !c.deleted && !dismissedRef.current.has(c.id));
     const ts = comments.filter(c => c.timestamp != null && !c.deleted && !dismissedRef.current.has(c.id));
-    // Find the comment whose timestamp is closest to currentTime within the window
     let best: Comment | null = null;
     let best: Comment | null = null;
     let bestDist = Infinity;
     let bestDist = Infinity;
     for (const c of ts) {
     for (const c of ts) {
@@ -85,7 +84,6 @@ export function VideoPlayer({
     return best;
     return best;
   })();
   })();
 
 
-  // Show bubble when any comment is within ±1.5s of currentTime
   useEffect(() => {
   useEffect(() => {
     const hasMatch = comments.some(
     const hasMatch = comments.some(
       c => c.timestamp != null && !c.deleted && !dismissedRef.current.has(c.id) &&
       c => c.timestamp != null && !c.deleted && !dismissedRef.current.has(c.id) &&
@@ -97,7 +95,6 @@ export function VideoPlayer({
   const hideTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
   const hideTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
   const redrawAnnotationsRef = useRef<(time: number) => void>(() => {});
   const redrawAnnotationsRef = useRef<(time: number) => void>(() => {});
 
 
-  // Refs so the rVFC callback always sees current values without re-registering
   const fpsRef = useRef(fps);
   const fpsRef = useRef(fps);
   const visibleAnnotationsRef = useRef(visibleAnnotations);
   const visibleAnnotationsRef = useRef(visibleAnnotations);
   const drawModeRef = useRef(drawMode);
   const drawModeRef = useRef(drawMode);
@@ -105,11 +102,10 @@ export function VideoPlayer({
   visibleAnnotationsRef.current = visibleAnnotations;
   visibleAnnotationsRef.current = visibleAnnotations;
   drawModeRef.current = drawMode;
   drawModeRef.current = drawMode;
 
 
-  // ── requestVideoFrameCallback loop — fires every rendered frame ─────────────────
+  // ── rVFC loop ───────────────────────────────────────────────────────────────
   useEffect(() => {
   useEffect(() => {
     const video = videoRef.current;
     const video = videoRef.current;
     if (!video) return;
     if (!video) return;
-
     function onFrame(_now: number, metadata: { mediaTime: number }) {
     function onFrame(_now: number, metadata: { mediaTime: number }) {
       const t = metadata.mediaTime;
       const t = metadata.mediaTime;
       setCurrentTime(t);
       setCurrentTime(t);
@@ -117,22 +113,20 @@ export function VideoPlayer({
       redrawAnnotationsRef.current(t);
       redrawAnnotationsRef.current(t);
       videoCallbackRef.current = (video as any).requestVideoFrameCallback(onFrame);
       videoCallbackRef.current = (video as any).requestVideoFrameCallback(onFrame);
     }
     }
-
     videoCallbackRef.current = (video as any).requestVideoFrameCallback(onFrame);
     videoCallbackRef.current = (video as any).requestVideoFrameCallback(onFrame);
     return () => {
     return () => {
       if (videoCallbackRef.current !== null) {
       if (videoCallbackRef.current !== null) {
-        try { (video as any).cancelVideoFrameCallback(videoCallbackRef.current); } catch { /* ignore */ }
+        try { (video as any).cancelVideoFrameCallback(videoCallbackRef.current); } catch { /* */ }
         videoCallbackRef.current = null;
         videoCallbackRef.current = null;
       }
       }
     };
     };
   // eslint-disable-next-line react-hooks/exhaustive-deps
   // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   }, []);
 
 
-  // HLS setup
+  // HLS
   useEffect(() => {
   useEffect(() => {
     const video = videoRef.current;
     const video = videoRef.current;
     if (!video || !src) return;
     if (!video || !src) return;
-
     if (mimeType === 'application/x-mpegURL' || src.endsWith('.m3u8')) {
     if (mimeType === 'application/x-mpegURL' || src.endsWith('.m3u8')) {
       if (Hls.isSupported()) {
       if (Hls.isSupported()) {
         const hls = new Hls();
         const hls = new Hls();
@@ -156,11 +150,10 @@ export function VideoPlayer({
     return () => obs.disconnect();
     return () => obs.disconnect();
   }, []);
   }, []);
 
 
-  const handleDismissBubble = useCallback((commentId: string) => {
-    setDismissedSet(prev => new Set([...prev, commentId]));
+  const handleDismissBubble = useCallback((id: string) => {
+    setDismissedSet(prev => new Set([...prev, id]));
   }, []);
   }, []);
 
 
-  // ── Annotation draw function (ref-based, callable from rVFC callback) ───────────────
   redrawAnnotationsRef.current = (time: number) => {
   redrawAnnotationsRef.current = (time: number) => {
     const canvas = displayCanvasRef.current;
     const canvas = displayCanvasRef.current;
     if (!canvas) return;
     if (!canvas) return;
@@ -168,18 +161,14 @@ export function VideoPlayer({
     if (!ctx) return;
     if (!ctx) return;
     ctx.clearRect(0, 0, canvas.width, canvas.height);
     ctx.clearRect(0, 0, canvas.width, canvas.height);
     if (drawModeRef.current) return;
     if (drawModeRef.current) return;
-
     const anns = visibleAnnotationsRef.current;
     const anns = visibleAnnotationsRef.current;
     if (!anns || anns.length === 0) return;
     if (!anns || anns.length === 0) return;
     const frameRange = 3 / (fpsRef.current || 30);
     const frameRange = 3 / (fpsRef.current || 30);
     for (const { annotation, timestamp } of anns) {
     for (const { annotation, timestamp } of anns) {
-      if (Math.abs(time - timestamp) <= frameRange) {
-        drawShape(ctx, annotation);
-      }
+      if (Math.abs(time - timestamp) <= frameRange) drawShape(ctx, annotation);
     }
     }
   };
   };
 
 
-  // Resize + initial draw
   useEffect(() => {
   useEffect(() => {
     const canvas = displayCanvasRef.current;
     const canvas = displayCanvasRef.current;
     if (!canvas) return;
     if (!canvas) return;
@@ -189,53 +178,29 @@ export function VideoPlayer({
   // eslint-disable-next-line react-hooks/exhaustive-deps
   // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [dims]);
   }, [dims]);
 
 
-  // Keyboard shortcuts
+  // ── Keyboard shortcuts ──────────────────────────────────────────────────────
   useEffect(() => {
   useEffect(() => {
     const handleKey = (e: KeyboardEvent) => {
     const handleKey = (e: KeyboardEvent) => {
       const video = videoRef.current;
       const video = videoRef.current;
       if (!video || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
       if (!video || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
-
-      if (e.code === 'Space') {
-        e.preventDefault();
-        video.paused ? video.play() : video.pause();
-      }
-      if (e.code === 'ArrowLeft') {
-        e.preventDefault();
-        video.currentTime = Math.max(0, video.currentTime - 5);
-      }
-      if (e.code === 'ArrowRight') {
-        e.preventDefault();
-        video.currentTime = Math.min(duration, video.currentTime + 5);
-      }
-      if (e.code === 'KeyC') {
-        e.preventDefault();
-        if (!drawMode) {
-          video.pause();
-          onDrawModeChange(true);
-        } else {
-          onDrawModeChange(false);
-        }
-      }
+      if (e.code === 'Space') { e.preventDefault(); video.paused ? video.play() : video.pause(); }
+      if (e.code === 'ArrowLeft') { e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 5); }
+      if (e.code === 'ArrowRight') { e.preventDefault(); video.currentTime = Math.min(duration, video.currentTime + 5); }
+      if (e.code === 'KeyC') { e.preventDefault(); onDrawModeChange(!drawMode); }
       if (e.code === 'KeyF') { e.preventDefault(); toggleFullscreen(); }
       if (e.code === 'KeyF') { e.preventDefault(); toggleFullscreen(); }
-      if (e.code === 'KeyM') { e.preventDefault(); video.muted = !video.muted; }
+      if (e.code === 'KeyM') { e.preventDefault(); toggleMute(); }
       if (e.code === 'KeyU') { e.preventDefault(); stepFrame(-1); }
       if (e.code === 'KeyU') { e.preventDefault(); stepFrame(-1); }
       if (e.code === 'KeyI') { e.preventDefault(); stepFrame(1); }
       if (e.code === 'KeyI') { e.preventDefault(); stepFrame(1); }
-      if (e.code === 'Escape' && drawMode) {
-        e.preventDefault();
-        onDrawModeChange(false);
-      }
+      if (e.code === 'Escape' && drawMode) { e.preventDefault(); onDrawModeChange(false); }
     };
     };
     window.addEventListener('keydown', handleKey);
     window.addEventListener('keydown', handleKey);
     return () => window.removeEventListener('keydown', handleKey);
     return () => window.removeEventListener('keydown', handleKey);
   }, [duration, drawMode, onDrawModeChange]);
   }, [duration, drawMode, onDrawModeChange]);
 
 
-  // Auto-hide controls
   const resetHideTimer = useCallback(() => {
   const resetHideTimer = useCallback(() => {
     setShowControls(true);
     setShowControls(true);
     clearTimeout(hideTimer.current);
     clearTimeout(hideTimer.current);
-    if (playing) {
-      hideTimer.current = setTimeout(() => setShowControls(false), 3000);
-    }
+    if (playing) hideTimer.current = setTimeout(() => setShowControls(false), 3000);
   }, [playing]);
   }, [playing]);
 
 
   function stepFrame(dir: 1 | -1) {
   function stepFrame(dir: 1 | -1) {
@@ -263,13 +228,27 @@ export function VideoPlayer({
     video.paused ? video.play() : video.pause();
     video.paused ? video.play() : video.pause();
   }
   }
 
 
-  const handleVolume = (v: number) => {
+  function toggleMute() {
+    const video = videoRef.current;
+    if (!video) return;
+    if (muted || volume === 0) {
+      video.volume = 1;
+      setVolume(1);
+      setMuted(false);
+    } else {
+      video.volume = 0;
+      setVolume(0);
+      setMuted(true);
+    }
+  }
+
+  function handleVolumeSlider(v: number) {
     const video = videoRef.current;
     const video = videoRef.current;
     if (!video) return;
     if (!video) return;
     video.volume = v;
     video.volume = v;
     setVolume(v);
     setVolume(v);
     setMuted(v === 0);
     setMuted(v === 0);
-  };
+  }
 
 
   const handleSpeed = (rate: number) => {
   const handleSpeed = (rate: number) => {
     const video = videoRef.current;
     const video = videoRef.current;
@@ -286,16 +265,19 @@ export function VideoPlayer({
     onTimeUpdate(time);
     onTimeUpdate(time);
   };
   };
 
 
+  // Volume icon: 0 bars = muted, 1 bar = low, 2 = med, 3 = high
+  const volBars = muted || volume === 0 ? 0 : volume <= 0.33 ? 1 : volume <= 0.66 ? 2 : 3;
+
   return (
   return (
     <div className="flex flex-col gap-0">
     <div className="flex flex-col gap-0">
-      {/* ── Video frame (no controls inside) ─────────────────── */}
+
+      {/* ── Video frame ──────────────────────────────────────────────── */}
       <div
       <div
         ref={containerRef}
         ref={containerRef}
         className="relative bg-black rounded-xl overflow-hidden select-none group"
         className="relative bg-black rounded-xl overflow-hidden select-none group"
         onMouseMove={resetHideTimer}
         onMouseMove={resetHideTimer}
         onMouseLeave={() => playing && setShowControls(false)}
         onMouseLeave={() => playing && setShowControls(false)}
       >
       >
-        {/* Video */}
         <video
         <video
           ref={videoRef}
           ref={videoRef}
           className="w-full block"
           className="w-full block"
@@ -306,14 +288,12 @@ export function VideoPlayer({
           playsInline
           playsInline
         />
         />
 
 
-        {/* Annotation display layer */}
         <canvas
         <canvas
           ref={displayCanvasRef}
           ref={displayCanvasRef}
           className="absolute inset-0 z-[5] pointer-events-none"
           className="absolute inset-0 z-[5] pointer-events-none"
           style={{ display: drawMode ? 'none' : 'block' }}
           style={{ display: drawMode ? 'none' : 'block' }}
         />
         />
 
 
-        {/* Annotation drawing layer */}
         <AnnotationCanvas
         <AnnotationCanvas
           isActive={drawMode}
           isActive={drawMode}
           tool={drawTool}
           tool={drawTool}
@@ -324,7 +304,7 @@ export function VideoPlayer({
           onStrokeComplete={onStrokeComplete}
           onStrokeComplete={onStrokeComplete}
         />
         />
 
 
-        {/* Big play button overlay */}
+        {/* Big play button overlay — centered */}
         {!playing && !drawMode && (
         {!playing && !drawMode && (
           <button
           <button
             className="absolute inset-0 flex items-center justify-center z-20"
             className="absolute inset-0 flex items-center justify-center z-20"
@@ -333,10 +313,58 @@ export function VideoPlayer({
           />
           />
         )}
         )}
 
 
-        {/* ── Floating speech bubble — inside video frame, overlays controls area ─ */}
+        {/* Controls overlay (frame step + volume) on top of video */}
+        {showControls && !drawMode && (
+          <div className="absolute inset-0 z-20 flex items-center justify-center pointer-events-none"
+               style={{ background: 'linear-gradient(to top, rgba(0,0,0,0.45) 0%, transparent 60%)' }}>
+            {/* Center: previous frame | Play/Pause | next frame */}
+            <div className="flex items-center gap-4 pointer-events-auto">
+              <button
+                onClick={() => stepFrame(-1)}
+                className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-90"
+                style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(8px)' }}
+                title="Previous frame (U)"
+              >
+                <svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
+                  <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
+                </svg>
+              </button>
+
+              <button
+                onClick={togglePlay}
+                className="w-14 h-14 rounded-full flex items-center justify-center transition-all active:scale-90"
+                style={{ background: 'rgba(99,102,241,0.85)', boxShadow: '0 0 24px rgba(99,102,241,0.4)' }}
+                aria-label={playing ? 'Pause' : 'Play'}
+              >
+                {playing ? (
+                  <svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
+                    <path d="M6 4h4v16H6zM14 4h4v16h-4z" />
+                  </svg>
+                ) : (
+                  <svg className="w-6 h-6 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24">
+                    <path d="M8 5v14l11-7z" />
+                  </svg>
+                )}
+              </button>
+
+              <button
+                onClick={() => stepFrame(1)}
+                className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-90"
+                style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(8px)' }}
+                title="Next frame (I)"
+              >
+                <svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
+                  <path d="M18 18h-2V6h2zm-3.5 0L6 12z" />
+                </svg>
+              </button>
+            </div>
+          </div>
+        )}
+
+        {/* Floating speech bubble — inside video frame */}
         {activeComment && !drawMode && (
         {activeComment && !drawMode && (
           <div
           <div
-            className="absolute bottom-2 left-2 right-2 z-30 pointer-events-none"
+            className="absolute bottom-2 left-2 right-2 z-30"
             style={{ pointerEvents: 'auto' }}
             style={{ pointerEvents: 'auto' }}
           >
           >
             <div className="flex justify-center">
             <div className="flex justify-center">
@@ -352,14 +380,12 @@ export function VideoPlayer({
             </div>
             </div>
           </div>
           </div>
         )}
         )}
-
       </div>
       </div>
 
 
-      {/* ── Controls AREA — outside the video frame ─────────── */}
-      {/* Draw toolbar */}
+      {/* ── Draw toolbar (always above controls on mobile) ──────────── */}
       {drawMode && (
       {drawMode && (
-        <div className="flex items-center gap-2 mt-2 px-1">
-          <span className="text-xs text-[--text-muted]">Draw:</span>
+        <div className="flex flex-wrap items-center gap-2 mt-2 px-1">
+          <span className="text-xs shrink-0" style={{ color: 'var(--text-muted)' }}>Draw:</span>
           {(['pen', 'arrow', 'rect', 'ellipse'] as Tool[]).map(t => (
           {(['pen', 'arrow', 'rect', 'ellipse'] as Tool[]).map(t => (
             <button
             <button
               key={t}
               key={t}
@@ -371,7 +397,7 @@ export function VideoPlayer({
               {t.charAt(0).toUpperCase() + t.slice(1)}
               {t.charAt(0).toUpperCase() + t.slice(1)}
             </button>
             </button>
           ))}
           ))}
-          <div className="w-px h-5 bg-white/20 mx-1" />
+          <div className="w-px h-5 bg-white/20 shrink-0" />
           {COLORS.map(c => (
           {COLORS.map(c => (
             <button
             <button
               key={c.value}
               key={c.value}
@@ -383,10 +409,10 @@ export function VideoPlayer({
               title={c.name}
               title={c.name}
             />
             />
           ))}
           ))}
-          <div className="w-px h-5 bg-white/20 mx-1" />
+          <div className="w-px h-5 bg-white/20 shrink-0" />
           <button
           <button
             onClick={() => onDrawModeChange(false)}
             onClick={() => onDrawModeChange(false)}
-            className="text-xs text-white/60 hover:text-white px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"
+            className="text-xs text-white/60 hover:text-white px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors shrink-0"
           >
           >
             Done
             Done
           </button>
           </button>
@@ -403,61 +429,60 @@ export function VideoPlayer({
         onCommentClick={onCommentClick}
         onCommentClick={onCommentClick}
       />
       />
 
 
-      {/* Bottom controls row */}
-      <div className="flex items-center gap-2 px-1 pb-1">
-        {/* Play/Pause */}
+      {/* ── Bottom controls row ───────────────────────────────────────── */}
+      <div className="flex items-center gap-1 px-1 pb-2">
+
+        {/* Volume — icon only, tap cycles through levels */}
         <button
         <button
-          onClick={togglePlay}
-          className="text-white/80 hover:text-white transition-colors"
-          disabled={drawMode}
+          onClick={toggleMute}
+          className="flex items-center justify-center w-8 h-8 rounded-lg transition-colors shrink-0"
+          style={{ color: 'rgba(255,255,255,0.7)' }}
+          title={muted || volume === 0 ? 'Unmute' : 'Mute'}
         >
         >
-          {playing ? (
-            <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
-              <path d="M6 4h4v16H6zM14 4h4v16h-4z" />
-            </svg>
-          ) : (
-            <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
-              <path d="M8 5v14l11-7z" />
-            </svg>
-          )}
+          {/* Volume bars SVG: 4 bars of decreasing height */}
+          <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+            {volBars === 0 ? (
+              <>
+                <path d="M11 5L6 9H2v6h4l5 4V5z" />
+                <line x1="23" y1="9" x2="17" y2="15" />
+                <line x1="17" y1="9" x2="23" y2="15" />
+              </>
+            ) : volBars === 1 ? (
+              <>
+                <path d="M11 5L6 9H2v6h4l5 4V5z" />
+                <path d="M15.54 8.46a5 5 0 010 7.07" />
+              </>
+            ) : volBars === 2 ? (
+              <>
+                <path d="M11 5L6 9H2v6h4l5 4V5z" />
+                <path d="M15.54 8.46a5 5 0 010 7.07" />
+                <path d="M19.07 4.93a10 10 0 010 14.14" />
+              </>
+            ) : (
+              <>
+                <path d="M11 5L6 9H2v6h4l5 4V5z" />
+                <path d="M15.54 8.46a5 5 0 010 7.07" />
+                <path d="M19.07 4.93a10 10 0 010 14.14" />
+              </>
+            )}
+          </svg>
         </button>
         </button>
 
 
-        {/* Frame step */}
-        <button onClick={() => stepFrame(-1)} className="text-white/50 hover:text-white text-xs" title="Previous frame (U)">⏮</button>
-        <button onClick={() => stepFrame(1)} className="text-white/50 hover:text-white text-xs" title="Next frame (I)">⏭</button>
-
-        {/* Volume */}
-        <button onClick={() => handleVolume(muted || volume === 0 ? 1 : 0)} className="text-white/70 hover:text-white">
-          {muted || volume === 0 ? (
-            <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707A1 1 0 0112 5v14a1 1 0 01-1.707.707L5.586 15z" />
-              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
-            </svg>
-          ) : (
-            <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707A1 1 0 0112 5v14a1 1 0 01-1.707.707L5.586 15z" />
-            </svg>
-          )}
-        </button>
+        {/* Desktop: volume slider (hidden on mobile) */}
         <input
         <input
           type="range" min={0} max={1} step={0.05}
           type="range" min={0} max={1} step={0.05}
           value={muted ? 0 : volume}
           value={muted ? 0 : volume}
-          onChange={e => handleVolume(parseFloat(e.target.value))}
-          className="w-14 h-1 accent-indigo-500"
+          onChange={e => handleVolumeSlider(parseFloat(e.target.value))}
+          className="w-14 h-1 accent-indigo-500 hidden md:block"
         />
         />
 
 
-        {/* Timecode */}
-        <span className="text-xs text-white/50 font-mono ml-1">
-          {formatTimecode(currentTime, fps, duration)} / {formatTimecode(duration, fps, duration)}
-        </span>
-
         <div className="flex-1" />
         <div className="flex-1" />
 
 
-        {/* Speed */}
+        {/* Speed — desktop only */}
         <select
         <select
           value={playbackRate}
           value={playbackRate}
           onChange={e => handleSpeed(parseFloat(e.target.value))}
           onChange={e => handleSpeed(parseFloat(e.target.value))}
-          className="bg-transparent text-xs text-white/70 border border-white/25 rounded px-1.5 py-0.5 cursor-pointer hover:border-white/50"
+          className="bg-transparent text-xs text-white/70 border border-white/25 rounded px-1.5 py-0.5 cursor-pointer hover:border-white/50 hidden md:block"
         >
         >
           {[0.25, 0.5, 0.75, 1, 1.25, 1.5, 2].map(r => (
           {[0.25, 0.5, 0.75, 1, 1.25, 1.5, 2].map(r => (
             <option key={r} value={r} className="text-black">{r}x</option>
             <option key={r} value={r} className="text-black">{r}x</option>
@@ -467,16 +492,10 @@ export function VideoPlayer({
         {/* Draw mode toggle */}
         {/* Draw mode toggle */}
         <button
         <button
           onClick={() => {
           onClick={() => {
-            if (!drawMode) {
-              videoRef.current?.pause();
-              onDrawModeChange(true);
-            } else {
-              onDrawModeChange(false);
-            }
+            if (!drawMode) { videoRef.current?.pause(); onDrawModeChange(true); }
+            else { onDrawModeChange(false); }
           }}
           }}
-          className={`p-1.5 rounded transition-colors ${
-            drawMode ? 'bg-indigo-600 text-white' : 'bg-white/10 text-white/70 hover:bg-white/20'
-          }`}
+          className={`p-1.5 rounded transition-colors ${drawMode ? 'bg-indigo-600 text-white' : 'bg-white/10 text-white/70 hover:bg-white/20'}`}
           title="Toggle draw mode (C)"
           title="Toggle draw mode (C)"
         >
         >
           <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
           <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -485,7 +504,12 @@ export function VideoPlayer({
         </button>
         </button>
 
 
         {/* Fullscreen */}
         {/* Fullscreen */}
-        <button onClick={toggleFullscreen} className="text-white/70 hover:text-white" title="Fullscreen (F)">
+        <button
+          onClick={toggleFullscreen}
+          className="w-8 h-8 flex items-center justify-center rounded-lg transition-colors"
+          style={{ color: 'rgba(255,255,255,0.7)' }}
+          title="Fullscreen (F)"
+        >
           <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
           <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
             {fullscreen ? (
             {fullscreen ? (
               <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
               <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
@@ -497,4 +521,4 @@ export function VideoPlayer({
       </div>
       </div>
     </div>
     </div>
   );
   );
-}
+}