Files
TREK/client/src/components/Journey/VideoPlayer.tsx
T
Maurice 85b5756e89 fix(client): the audit round over the 4.3.1 release
Ride minutes and late marks follow an edited timetable instead of the routing
that fetched them; a landing day's join band reads as the ride it is rather
than a nine hour drive of no length; the refuel offer is there on the road
out of a terminal or a pick-up desk; the phone's stage map draws the ride;
the share page numbers its stops the way the planner does; the PDF lists a
day the way the day plan lists it, from both shells; a poster-less clip draws
its play badge everywhere, not only in the editor; the hero title has room
for its descenders and the list card keeps its badge clear; the release
notice keeps its close control in reach when it stacks; a link chip's actions
are thumb-sized and its delete asks first; the Google retry line and the
Google-only switch appear only where Google answers.
2026-09-22 13:30:12 +02:00

108 lines
3.2 KiB
TypeScript

import React, { useEffect, useRef } from 'react'
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
// The control icons, served from this origin. Plyr's default points at cdn.plyr.io,
// which connect-src refuses: the player worked but every button in it was blank.
// A file, not a data URL, because Plyr tells same-origin from cross-origin by the
// host and would fetch a data URL over XHR, which connect-src refuses as well.
import plyrSprite from 'plyr/dist/plyr.svg?no-inline'
interface Props {
src: string
poster?: string
autoPlay?: boolean
style?: React.CSSProperties
}
/**
* Video player for gallery/lightbox playback (#823), built on Plyr over a native
* <video>. Local videos stream with HTTP Range (seeking works out of the box) and
* the source carries the correct video MIME from the server. The Plyr instance is
* created once per mounted source and destroyed on unmount, so navigating away in
* the lightbox stops playback.
*
* A new source is a new element: Plyr's destroy() puts a clone of the element as
* it was at construction back into the document, so rebuilding the player on a
* <video> React had already pointed at the next clip left the first clip on
* screen under the next one's name.
*/
export default function VideoPlayer(props: Props): React.ReactElement {
return <PlyrVideo key={props.src} {...props} />
}
function PlyrVideo({ src, poster, autoPlay = true, style }: Props): React.ReactElement {
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
const el = videoRef.current
if (!el) return
const player = new Plyr(el, {
controls: [
'play-large',
'play',
'progress',
'current-time',
'duration',
'mute',
'volume',
'fullscreen',
],
autoplay: autoPlay,
clickToPlay: true,
hideControls: false,
iconUrl: plyrSprite,
// On teardown Plyr points the element at a blank clip to abort the stream,
// by default one on cdn.plyr.io, which media-src refuses. An empty source
// aborts the stream just the same, and the element is out of the document
// by then, so the error it would otherwise report has nowhere to show.
blankVideo: '',
})
return () => {
try {
player.destroy()
} catch {
/* already torn down */
}
}
}, [autoPlay])
return (
<div
className="trek-video-player"
style={{
width: 'min(92vw, 1100px)',
height: 'min(88vh, 900px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#000',
borderRadius: 4,
overflow: 'hidden',
animation: 'fadeIn 0.15s ease',
...style,
}}
>
<video
ref={videoRef}
src={src}
poster={poster}
playsInline
controls
preload="metadata"
style={{
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
background: '#000',
}}
>
{/* Gallery uploads carry no caption file; the empty track keeps the element valid. */}
<track kind="captions" />
</video>
</div>
)
}