fix(uploads): keep HEVC inline playback for Safari/WebKit
The server's `playable` flag is computed Chromium-first, but HEVC web-playability is browser-dependent: WebKit (Safari on macOS/iOS) decodes HEVC via the OS while Chromium/Firefox/Electron can't. Treating the flag as global wrongly showed Safari users the download fallback for files they can play inline. The client now treats `playable === false` as "needs a capability check": it pre-renders the fallback only when the current browser also can't decode the format, gated on a one-time canPlayType probe (BROWSER_SUPPORTS_HEVC). Capable browsers attempt inline playback; the <video> onError handler remains the safety net for genuine failures.
This commit is contained in:
@@ -164,6 +164,7 @@ Processing occurs inline during upload, before the response is sent. All process
|
|||||||
- `1` / `true` — web-standard codec (H.264/AVC, VP8/VP9, AV1, Theora) in a web container (`video/mp4`, `video/webm`, `video/ogg`). Plays inline.
|
- `1` / `true` — web-standard codec (H.264/AVC, VP8/VP9, AV1, Theora) in a web container (`video/mp4`, `video/webm`, `video/ogg`). Plays inline.
|
||||||
- `NULL` — unknown / optimistic. Codec couldn't be probed (ffmpeg absent or probe failed), or it's a web-safe codec in a container with inconsistent cross-browser support (H.264 in .mov). The client attempts inline playback and degrades via the `<video>` `onError` handler.
|
- `NULL` — unknown / optimistic. Codec couldn't be probed (ffmpeg absent or probe failed), or it's a web-safe codec in a container with inconsistent cross-browser support (H.264 in .mov). The client attempts inline playback and degrades via the `<video>` `onError` handler.
|
||||||
- `false` is never widened beyond codecs known to fail everywhere, so an instance without ffmpeg keeps prior behaviour (attempt playback) rather than regressing every video to "unplayable".
|
- `false` is never widened beyond codecs known to fail everywhere, so an instance without ffmpeg keeps prior behaviour (attempt playback) rather than regressing every video to "unplayable".
|
||||||
|
- **The verdict is computed Chromium-first and is not authoritative per-client.** Web-playability of HEVC is browser-dependent — WebKit (Safari/iOS) decodes it via the OS while Chromium/Firefox/Electron don't. The client therefore treats `playable === false` as "needs a capability check": it pre-renders the fallback only when the current browser also can't decode the format (`BROWSER_SUPPORTS_HEVC` in `AttachmentRenderer.tsx`). See §9 Attachment Rendering.
|
||||||
|
|
||||||
### Audio Processing
|
### Audio Processing
|
||||||
|
|
||||||
@@ -532,7 +533,7 @@ URL resolution for attachment/thumbnail:
|
|||||||
| MIME category | Rendering |
|
| MIME category | Rendering |
|
||||||
|---------------|-----------|
|
|---------------|-----------|
|
||||||
| `image/*` | `<img>` with click-to-preview, lazy loading, aspect ratio from width/height, max 400x300px, uses thumbnail if available |
|
| `image/*` | `<img>` with click-to-preview, lazy loading, aspect ratio from width/height, max 400x300px, uses thumbnail if available |
|
||||||
| `video/*` | `VideoAttachment` sub-component. Playable (`playable !== false`): `<video src>` with native controls, poster from thumbnail, preload `none` (with dimensions) or `metadata` (without), max 400px wide / 300px tall, with an `onError` handler that falls back to the download card. Unplayable (`playable === false`, e.g. HEVC .mov): renders the download card directly — poster (if any) under a "Can't play here — download" overlay, plus filename, duration, size and a one-tap download. Never a silently broken player. |
|
| `video/*` | `VideoAttachment` sub-component. Playable (`playable !== false`): `<video src>` with native controls, poster from thumbnail, preload `none` (with dimensions) or `metadata` (without), max 400px wide / 300px tall, with an `onError` handler that falls back to the download card. Unplayable (`playable === false`, e.g. HEVC .mov): renders the download card directly — poster (if any) under a "Can't play here — download" overlay, plus filename, duration, size and a one-tap download. **Browser-aware:** the `playable === false` verdict is computed Chromium-first, so the client only pre-fails when *this* browser also can't decode the format. WebKit (Safari/iOS) decodes HEVC via the OS, so a Safari user still gets inline playback (gated on `BROWSER_SUPPORTS_HEVC`, a one-time `canPlayType` probe for `hvc1`/`hev1`); the `onError` handler remains the safety net. Never a silently broken player. |
|
||||||
| `audio/*` | Audio card with icon, filename, size, `<audio>` with native controls, preload `metadata`, max 420px wide |
|
| `audio/*` | Audio card with icon, filename, size, `<audio>` with native controls, preload `metadata`, max 420px wide |
|
||||||
| Other | Download link card with file icon, filename (link-styled), size |
|
| Other | Download link card with file icon, filename (link-styled), size |
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ function formatDuration(seconds: number): string {
|
|||||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether *this* browser can decode HEVC (H.265) in a <video> element.
|
||||||
|
*
|
||||||
|
* Web-playability of HEVC is browser-dependent, so the server's `playable`
|
||||||
|
* flag (computed Chromium-first) can't be authoritative for every client:
|
||||||
|
* WebKit (Safari on macOS/iOS) decodes HEVC via the OS, while Chromium,
|
||||||
|
* Firefox and stock Electron cannot. HEVC is the codec that the server's
|
||||||
|
* "unplayable" verdict effectively hinges on, so we probe for it here and let
|
||||||
|
* capable browsers attempt playback rather than blindly showing the fallback.
|
||||||
|
*
|
||||||
|
* Computed once at module load. `canPlayType` returns '' when unsupported and
|
||||||
|
* 'maybe'/'probably' otherwise; Safari reports support for the hvc1/hev1 tags.
|
||||||
|
*/
|
||||||
|
const BROWSER_SUPPORTS_HEVC: boolean = (() => {
|
||||||
|
if (typeof document === 'undefined') return false;
|
||||||
|
const v = document.createElement('video');
|
||||||
|
return v.canPlayType('video/mp4; codecs="hvc1"') !== '' ||
|
||||||
|
v.canPlayType('video/mp4; codecs="hev1"') !== '';
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the displayable URL for an attachment. Same logic used by inline
|
* Resolves the displayable URL for an attachment. Same logic used by inline
|
||||||
* `<img>`/`<video>`/`<audio>` rendering and the file-card download button —
|
* `<img>`/`<video>`/`<audio>` rendering and the file-card download button —
|
||||||
@@ -49,16 +69,23 @@ interface VideoAttachmentProps {
|
|||||||
*
|
*
|
||||||
* 1. Proactive — the server classifies web-playability from the probed codec
|
* 1. Proactive — the server classifies web-playability from the probed codec
|
||||||
* (`attachment.playable === false`), so we render the download card
|
* (`attachment.playable === false`), so we render the download card
|
||||||
* directly with no flash of a dead player.
|
* directly with no flash of a dead player. This is honoured only when the
|
||||||
* 2. Reactive — for the optimistic/unknown cases, the `<video>` `onError`
|
* current browser also can't decode the format: the server flag is
|
||||||
* handler flips to the same card if playback actually fails at runtime.
|
* Chromium-first, but WebKit (Safari/iOS) decodes HEVC, so a Safari user
|
||||||
|
* still gets inline playback (see `BROWSER_SUPPORTS_HEVC`).
|
||||||
|
* 2. Reactive — for the optimistic/unknown cases, and for capable browsers
|
||||||
|
* attempting a flagged file, the `<video>` `onError` handler flips to the
|
||||||
|
* same card if playback actually fails at runtime.
|
||||||
*
|
*
|
||||||
* The fallback card surfaces the poster (still a useful preview), filename,
|
* The fallback card surfaces the poster (still a useful preview), filename,
|
||||||
* duration and size, and a one-tap download — never a silently broken player.
|
* duration and size, and a one-tap download — never a silently broken player.
|
||||||
*/
|
*/
|
||||||
function VideoAttachment({ attachment, attUrl, thumbUrl, federationInlineBadge }: VideoAttachmentProps) {
|
function VideoAttachment({ attachment, attUrl, thumbUrl, federationInlineBadge }: VideoAttachmentProps) {
|
||||||
const startDownload = useTransferStore((s) => s.startDownload);
|
const startDownload = useTransferStore((s) => s.startDownload);
|
||||||
const [failed, setFailed] = useState(attachment.playable === false);
|
// Pre-fail only when the server flagged it unplayable AND this browser can't
|
||||||
|
// decode it anyway. Capable browsers (Safari/WebKit ⇒ HEVC) attempt playback
|
||||||
|
// and fall back via onError if it genuinely fails.
|
||||||
|
const [failed, setFailed] = useState(attachment.playable === false && !BROWSER_SUPPORTS_HEVC);
|
||||||
|
|
||||||
const { width, height, originalName, mimetype, size, duration } = attachment;
|
const { width, height, originalName, mimetype, size, duration } = attachment;
|
||||||
const hasDimensions = !!(width && height);
|
const hasDimensions = !!(width && height);
|
||||||
|
|||||||
Reference in New Issue
Block a user