Dispatcharr/frontend/src/utils/components/FloatingVideoUtils.js
SergeantPanda 2b00bd2630
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
Bug Fixes:
- Floating video player resize continuing after the mouse button was released outside the browser window. Because the `mouseup` event is not delivered when the pointer leaves the viewport, the `endResize` handler was never called and the `mousemove` listener remained active indefinitely. Fixed by checking `event.buttons === 0` at the top of `handleResizeMove`; when no button is held the resize session is torn down immediately and all move listeners are removed.
- Floating video player drag continuing after the mouse button was released outside the browser window, for the same reason as the resize bug above. Fixed by detecting `event.buttons === 0` in the `onDrag` handler and dispatching a synthetic `mouseup` event so react-draggable cleanly ends the drag session.
- Floating video player could be dragged off any edge of the screen, making the header (and therefore the drag handle) unreachable. The player is now fully bounded: the left and top edges are clamped to `x ≥ 0` / `y ≥ 0` so the header is always visible and reachable, and the right/bottom edges are clamped so the player never extends beyond the viewport. The player is also re-clamped automatically when the browser window is resized and proportionally scaled down if the saved size is larger than the new viewport.
2026-03-12 17:36:38 -05:00

151 lines
3.9 KiB
JavaScript

export const PLAYER_PREFS_KEY = 'dispatcharr-player-prefs';
export const getPlayerPrefs = () => {
try {
return JSON.parse(localStorage.getItem(PLAYER_PREFS_KEY) || '{}');
} catch {
return {};
}
};
export const savePlayerPrefs = (updates) => {
try {
localStorage.setItem(
PLAYER_PREFS_KEY,
JSON.stringify({ ...getPlayerPrefs(), ...updates })
);
} catch {}
};
export const getLivePlayerErrorMessage = (errorType, errorDetail) => {
if (errorType !== 'MediaError') {
return errorDetail
? `Error: ${errorType} - ${errorDetail}`
: `Error: ${errorType}`;
}
const errorString = errorDetail?.toLowerCase() || '';
if (
errorString.includes('audio') ||
errorString.includes('ac3') ||
errorString.includes('ac-3')
) {
return 'Audio codec not supported by your browser. Try Chrome or Edge for better audio codec support.';
}
if (
errorString.includes('video') ||
errorString.includes('h264') ||
errorString.includes('h.264')
) {
return 'Video codec not supported by your browser. Try Chrome or Edge for better video codec support.';
}
if (errorString.includes('mse')) {
return "Your browser doesn't support the codecs used in this stream. Try Chrome or Edge for better compatibility.";
}
return 'Media codec not supported by your browser. This may be due to unsupported audio (AC3) or video codecs. Try Chrome or Edge.';
};
export const getVODPlayerErrorMessage = (error) => {
if (!error) return 'Video playback error';
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
return 'Video playback was aborted';
case error.MEDIA_ERR_NETWORK:
return 'Network error while loading video';
case error.MEDIA_ERR_DECODE:
return 'Video codec not supported by your browser';
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
return 'Video format not supported by your browser';
default:
return error.message || 'Unknown video error';
}
};
export const getClientCoordinates = (event) => ({
clientX: event.touches?.[0]?.clientX ?? event.clientX,
clientY: event.touches?.[0]?.clientY ?? event.clientY,
});
export const calculateNewDimensions = (
deltaX,
deltaY,
startWidth,
startHeight,
handle,
ratio
) => {
const widthDelta = deltaX * handle.xDir;
const heightDelta = deltaY * handle.yDir;
let width = startWidth + widthDelta;
let height = width / ratio;
// Use vertical-driven resize if user drags mostly vertically
if (Math.abs(deltaY) > Math.abs(deltaX)) {
height = startHeight + heightDelta;
width = height * ratio;
}
return { width, height };
};
export const applyConstraints = (
width,
height,
ratio,
startPos,
handle,
minWidth,
minHeight,
visibleMargin
) => {
// Apply minimum constraints
if (width < minWidth) {
width = minWidth;
height = width / ratio;
}
if (height < minHeight) {
height = minHeight;
width = height * ratio;
}
// Apply viewport constraints
const posX = startPos?.x ?? 0;
const posY = startPos?.y ?? 0;
// Absolute caps ensure the player can never exceed the viewport even when
// its position is negative (partially off-screen on the opposite edge).
const maxWidth = !handle.isLeft
? Math.min(
window.innerWidth - visibleMargin,
Math.max(minWidth, window.innerWidth - posX - visibleMargin)
)
: null;
const maxHeight = !handle.isTop
? Math.min(
window.innerHeight - visibleMargin,
Math.max(minHeight, window.innerHeight - posY - visibleMargin)
)
: null;
if (maxWidth && width > maxWidth) {
width = maxWidth;
height = width / ratio;
}
if (maxHeight && height > maxHeight) {
height = maxHeight;
width = height * ratio;
}
// Final adjustment to maintain aspect ratio
if (maxWidth && width > maxWidth) {
width = maxWidth;
height = width / ratio;
}
return { width, height };
};