Skip to content

Commit b08ffae

Browse files
committed
frontend: improvements to videoplayer
1 parent 956dbcc commit b08ffae

5 files changed

Lines changed: 178 additions & 41 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "floorpov",
33
"private": true,
4-
"version": "0.1.3-beta",
4+
"version": "0.1.4-beta",
55
"type": "module",
66
"scripts": {
77
"prepare:ffmpeg": "powershell -ExecutionPolicy Bypass -File ./scripts/fetch-ffmpeg.ps1",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "FloorPoV"
3-
version = "0.1.3-beta"
3+
version = "0.1.4-beta"
44
description = "A World of Warcraft Gameplay recording and analyzing tool"
55
authors = ["RobDeFlop"]
66
edition = "2021"

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "FloorPoV",
4-
"version": "0.1.3-beta",
4+
"version": "0.1.4-beta",
55
"identifier": "FloorPoV",
66
"build": {
77
"beforeDevCommand": "bun run dev",

src/components/playback/VideoPlayer.tsx

Lines changed: 174 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ export function VideoPlayer() {
4444
const fileInputRef = useRef<HTMLInputElement>(null);
4545
const progressRef = useRef<HTMLDivElement>(null);
4646
const speedMenuRef = useRef<HTMLDivElement>(null);
47+
const immersiveSurfaceRef = useRef<HTMLDivElement>(null);
4748
const [showSpeedMenu, setShowSpeedMenu] = useState(false);
4849
const [volumeBeforeMute, setVolumeBeforeMute] = useState(1);
4950
const [isImmersiveMode, setIsImmersiveMode] = useState(false);
51+
const [videoNativeSize, setVideoNativeSize] = useState({ width: 0, height: 0 });
52+
const [devicePixelRatio, setDevicePixelRatio] = useState(() => window.devicePixelRatio || 1);
53+
const [immersiveViewportSize, setImmersiveViewportSize] = useState({ width: 0, height: 0 });
5054

5155
const showVideo = Boolean(videoSrc) && !isRecording;
5256

@@ -70,6 +74,30 @@ export function VideoPlayer() {
7074
};
7175

7276
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
77+
const immersiveVideoStyle =
78+
isImmersiveMode &&
79+
videoNativeSize.width > 0 &&
80+
videoNativeSize.height > 0 &&
81+
immersiveViewportSize.width > 0 &&
82+
immersiveViewportSize.height > 0
83+
? (() => {
84+
const safeDevicePixelRatio = Math.max(1, devicePixelRatio);
85+
const nativeCssWidth = Math.max(1, Math.floor(videoNativeSize.width / safeDevicePixelRatio));
86+
const nativeCssHeight = Math.max(1, Math.floor(videoNativeSize.height / safeDevicePixelRatio));
87+
const widthScale = immersiveViewportSize.width / nativeCssWidth;
88+
const heightScale = immersiveViewportSize.height / nativeCssHeight;
89+
const scale = Math.min(widthScale, heightScale, 1);
90+
91+
return {
92+
width: `${Math.max(1, Math.floor(nativeCssWidth * scale))}px`,
93+
height: `${Math.max(1, Math.floor(nativeCssHeight * scale))}px`,
94+
};
95+
})()
96+
: undefined;
97+
const immersiveControlsStyle =
98+
isImmersiveMode && immersiveVideoStyle?.width
99+
? { width: immersiveVideoStyle.width }
100+
: undefined;
73101

74102
useEffect(() => {
75103
if (!showSpeedMenu) {
@@ -113,6 +141,94 @@ export function VideoPlayer() {
113141
};
114142
}, [isImmersiveMode]);
115143

144+
useEffect(() => {
145+
if (!showVideo) {
146+
syncIsPlaying(false);
147+
return;
148+
}
149+
150+
const syncPlaybackState = () => {
151+
const videoElement = videoRef.current;
152+
if (!videoElement) {
153+
return;
154+
}
155+
156+
syncIsPlaying(!videoElement.paused && !videoElement.ended);
157+
};
158+
159+
syncPlaybackState();
160+
const syncTimeout = window.setTimeout(syncPlaybackState, 0);
161+
const syncFrame = window.requestAnimationFrame(syncPlaybackState);
162+
163+
return () => {
164+
window.clearTimeout(syncTimeout);
165+
window.cancelAnimationFrame(syncFrame);
166+
};
167+
}, [isImmersiveMode, showVideo, syncIsPlaying, videoRef]);
168+
169+
useEffect(() => {
170+
if (!videoSrc) {
171+
setVideoNativeSize({ width: 0, height: 0 });
172+
}
173+
}, [videoSrc]);
174+
175+
useEffect(() => {
176+
const handleResize = () => {
177+
setDevicePixelRatio(window.devicePixelRatio || 1);
178+
};
179+
180+
window.addEventListener("resize", handleResize);
181+
return () => {
182+
window.removeEventListener("resize", handleResize);
183+
};
184+
}, []);
185+
186+
useEffect(() => {
187+
if (!isImmersiveMode || !showVideo) {
188+
setImmersiveViewportSize({ width: 0, height: 0 });
189+
return;
190+
}
191+
192+
const updateViewportSize = () => {
193+
const surfaceRect = immersiveSurfaceRef.current?.getBoundingClientRect();
194+
if (!surfaceRect) {
195+
return;
196+
}
197+
198+
const nextWidth = Math.max(0, Math.floor(surfaceRect.width));
199+
const nextHeight = Math.max(0, Math.floor(surfaceRect.height));
200+
201+
setImmersiveViewportSize((currentSize) =>
202+
currentSize.width === nextWidth && currentSize.height === nextHeight
203+
? currentSize
204+
: { width: nextWidth, height: nextHeight }
205+
);
206+
};
207+
208+
updateViewportSize();
209+
210+
if (typeof ResizeObserver === "undefined") {
211+
window.addEventListener("resize", updateViewportSize);
212+
return () => {
213+
window.removeEventListener("resize", updateViewportSize);
214+
};
215+
}
216+
217+
const resizeObserver = new ResizeObserver(() => {
218+
updateViewportSize();
219+
});
220+
221+
if (immersiveSurfaceRef.current) {
222+
resizeObserver.observe(immersiveSurfaceRef.current);
223+
}
224+
225+
window.addEventListener("resize", updateViewportSize);
226+
return () => {
227+
resizeObserver.disconnect();
228+
window.removeEventListener("resize", updateViewportSize);
229+
};
230+
}, [isImmersiveMode, showVideo]);
231+
116232
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
117233
if (!progressRef.current || duration === 0) return;
118234
const rect = progressRef.current.getBoundingClientRect();
@@ -122,52 +238,66 @@ export function VideoPlayer() {
122238

123239
const playerSurface = (
124240
<div
241+
ref={isImmersiveMode ? immersiveSurfaceRef : undefined}
125242
className={
126243
isImmersiveMode
127-
? "fixed inset-0 z-[200] flex h-screen w-screen items-center justify-center overflow-hidden bg-neutral-950"
244+
? "fixed inset-0 z-[200] flex items-center justify-center overflow-hidden bg-neutral-950"
128245
: "relative h-full w-full overflow-hidden bg-neutral-950/90"
129246
}
130247
aria-busy={isVideoLoading}
131248
>
132249
{showVideo && (
133-
<video
134-
ref={videoRef}
135-
src={videoSrc || undefined}
250+
<div
136251
className={
137252
isImmersiveMode
138-
? "block h-auto w-auto max-h-full max-w-full object-contain"
139-
: "h-full w-full object-contain"
253+
? "flex h-full w-full items-center justify-center overflow-hidden"
254+
: "h-full w-full"
140255
}
141-
controls={false}
142-
playsInline
143-
disablePictureInPicture
144-
preload="metadata"
145-
onLoadStart={() => {
146-
setVideoLoading(true);
147-
}}
148-
onCanPlay={() => {
149-
setVideoLoading(false);
150-
}}
151-
onError={(event) => {
152-
setVideoLoading(false);
153-
const mediaError = event.currentTarget.error;
154-
console.error("[VideoPlayer] Video load error", {
155-
code: mediaError?.code,
156-
message: mediaError?.message,
157-
networkState: event.currentTarget.networkState,
158-
readyState: event.currentTarget.readyState,
159-
src: videoSrc,
160-
});
161-
}}
162-
onTimeUpdate={(e) => updateTime(e.currentTarget.currentTime)}
163-
onLoadedMetadata={(e) => {
164-
setVideoLoading(false);
165-
updateDuration(e.currentTarget.duration);
166-
}}
167-
onPlay={() => syncIsPlaying(true)}
168-
onPause={() => syncIsPlaying(false)}
169-
onEnded={() => syncIsPlaying(false)}
170-
/>
256+
>
257+
<video
258+
ref={videoRef}
259+
src={videoSrc || undefined}
260+
className={
261+
isImmersiveMode
262+
? "block h-auto w-auto max-h-full max-w-full object-contain"
263+
: "h-full w-full object-contain"
264+
}
265+
style={immersiveVideoStyle}
266+
controls={false}
267+
playsInline
268+
disablePictureInPicture
269+
preload="metadata"
270+
onLoadStart={() => {
271+
setVideoLoading(true);
272+
}}
273+
onCanPlay={() => {
274+
setVideoLoading(false);
275+
}}
276+
onError={(event) => {
277+
setVideoLoading(false);
278+
const mediaError = event.currentTarget.error;
279+
console.error("[VideoPlayer] Video load error", {
280+
code: mediaError?.code,
281+
message: mediaError?.message,
282+
networkState: event.currentTarget.networkState,
283+
readyState: event.currentTarget.readyState,
284+
src: videoSrc,
285+
});
286+
}}
287+
onTimeUpdate={(e) => updateTime(e.currentTarget.currentTime)}
288+
onLoadedMetadata={(e) => {
289+
setVideoLoading(false);
290+
updateDuration(e.currentTarget.duration);
291+
setVideoNativeSize({
292+
width: e.currentTarget.videoWidth,
293+
height: e.currentTarget.videoHeight,
294+
});
295+
}}
296+
onPlay={() => syncIsPlaying(true)}
297+
onPause={() => syncIsPlaying(false)}
298+
onEnded={() => syncIsPlaying(false)}
299+
/>
300+
</div>
171301
)}
172302

173303
{showVideo && isVideoLoading && (
@@ -204,7 +334,14 @@ export function VideoPlayer() {
204334
)}
205335

206336
{showVideo && (
207-
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-neutral-950/95 via-neutral-950/70 to-transparent p-3 sm:p-4">
337+
<div
338+
className={
339+
isImmersiveMode
340+
? "absolute bottom-0 left-1/2 w-full -translate-x-1/2 bg-gradient-to-t from-neutral-950/95 via-neutral-950/70 to-transparent p-3 sm:p-4"
341+
: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-neutral-950/95 via-neutral-950/70 to-transparent p-3 sm:p-4"
342+
}
343+
style={immersiveControlsStyle}
344+
>
208345
<div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-3">
209346
<div className="flex items-center gap-2 sm:gap-3 md:shrink-0">
210347
<ControlIconButton

0 commit comments

Comments
 (0)