Browse Source

fix blossom images

imwald
Silberengel 2 weeks ago
parent
commit
78a0d1e946
  1. 8
      src/components/Image/index.tsx
  2. 38
      src/components/MediaPlayer/index.tsx
  3. 18
      src/lib/url.ts

8
src/components/Image/index.tsx

@ -1,6 +1,6 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { isRenderableMediaUrl, isSafeMediaUrl } from '@/lib/url' import { isRenderableMediaUrl, isSafeMediaUrl, resolvePrimalBlossomPlayableUrl } from '@/lib/url'
import { TImetaInfo } from '@/types' import { TImetaInfo } from '@/types'
import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash' import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash'
import { decode } from 'blurhash' import { decode } from 'blurhash'
@ -102,7 +102,7 @@ export default function Image({
const [isLoading, setIsLoading] = useState(urlOk && !effectiveHoldUntilClick) const [isLoading, setIsLoading] = useState(urlOk && !effectiveHoldUntilClick)
const [displaySkeleton, setDisplaySkeleton] = useState(urlOk) const [displaySkeleton, setDisplaySkeleton] = useState(urlOk)
const [hasError, setHasError] = useState(!urlOk) const [hasError, setHasError] = useState(!urlOk)
const [imageUrl, setImageUrl] = useState(url) const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? ''))
const [fallbackIndex, setFallbackIndex] = useState(0) const [fallbackIndex, setFallbackIndex] = useState(0)
const loadWatchRef = useRef<number | null>(null) const loadWatchRef = useRef<number | null>(null)
// Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active. // Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active.
@ -149,7 +149,7 @@ export default function Image({
} }
useEffect(() => { useEffect(() => {
setImageUrl(url) setImageUrl(resolvePrimalBlossomPlayableUrl(url ?? ''))
loadSettledRef.current = false loadSettledRef.current = false
wasInitiallyHeldRef.current = effectiveHoldUntilClick wasInitiallyHeldRef.current = effectiveHoldUntilClick
const shouldHold = effectiveHoldUntilClick const shouldHold = effectiveHoldUntilClick
@ -220,7 +220,7 @@ export default function Image({
const next = fallback[fallbackIndex] const next = fallback[fallbackIndex]
setFallbackIndex((prev) => prev + 1) setFallbackIndex((prev) => prev + 1)
loadSettledRef.current = false loadSettledRef.current = false
setImageUrl(next) setImageUrl(resolvePrimalBlossomPlayableUrl(next))
return return
} }
setIsLoading(false) setIsLoading(false)

38
src/components/MediaPlayer/index.tsx

@ -1,4 +1,4 @@
import { isHlsPlaylistUrl, isImage, isZapStreamWatchPageUrl } from '@/lib/url' import { isHlsPlaylistUrl, isImage, isZapStreamWatchPageUrl, resolvePrimalBlossomPlayableUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
@ -63,15 +63,17 @@ export default function MediaPlayer({
const [embedPainted, setEmbedPainted] = useState(false) const [embedPainted, setEmbedPainted] = useState(false)
const readyOnceRef = useRef(false) const readyOnceRef = useRef(false)
const playableSrc = useMemo(() => resolvePrimalBlossomPlayableUrl(src), [src])
// imeta `thumb` / `image` are sometimes the same .mp4 as `url` — <img> cannot use that, and it // imeta `thumb` / `image` are sometimes the same .mp4 as `url` — <img> cannot use that, and it
// would hide the blurhash placeholder in LazyMediaTapPlaceholder. // would hide the blurhash placeholder in LazyMediaTapPlaceholder.
const imagePoster = useMemo(() => { const imagePoster = useMemo(() => {
const p = poster?.trim() const p = poster?.trim()
if (!p) return undefined if (!p) return undefined
return isImage(p) ? p : undefined return isImage(p) ? resolvePrimalBlossomPlayableUrl(p) : undefined
}, [poster]) }, [poster])
const urlEmbedSurfaceHint = useMemo(() => embedMediaSurfaceHintFromUrl(src), [src]) const urlEmbedSurfaceHint = useMemo(() => embedMediaSurfaceHintFromUrl(playableSrc), [playableSrc])
/** Probe result wins when set (e.g. audio-only mp4); URL hint avoids a blank frame before useEffect runs. */ /** Probe result wins when set (e.g. audio-only mp4); URL hint avoids a blank frame before useEffect runs. */
const effectiveMediaType = mediaType ?? urlEmbedSurfaceHint const effectiveMediaType = mediaType ?? urlEmbedSurfaceHint
@ -86,7 +88,7 @@ export default function MediaPlayer({
setEmbedPainted(false) setEmbedPainted(false)
setMediaType(null) setMediaType(null)
setProbeFailed(false) setProbeFailed(false)
}, [src]) }, [playableSrc])
useEffect(() => { useEffect(() => {
if (!showEmbed) { if (!showEmbed) {
@ -96,7 +98,7 @@ export default function MediaPlayer({
} }
readyOnceRef.current = false readyOnceRef.current = false
setEmbedPainted(false) setEmbedPainted(false)
if (!src) { if (!playableSrc) {
setProbeFailed(true) setProbeFailed(true)
return return
} }
@ -108,17 +110,17 @@ export default function MediaPlayer({
try { try {
// Firefox/Chrome do not expose HLS via <video> metadata probe — it fails and looked like “no player”. // Firefox/Chrome do not expose HLS via <video> metadata probe — it fails and looked like “no player”.
if (isHlsPlaylistUrl(src)) { if (isHlsPlaylistUrl(playableSrc)) {
setMediaType('video') setMediaType('video')
return return
} }
if (isZapStreamWatchPageUrl(src)) { if (isZapStreamWatchPageUrl(playableSrc)) {
setMediaType('iframe') setMediaType('iframe')
return return
} }
const url = new URL(src) const url = new URL(playableSrc)
const extension = url.pathname.split('.').pop()?.toLowerCase() const extension = url.pathname.split('.').pop()?.toLowerCase()
if ( if (
@ -135,7 +137,7 @@ export default function MediaPlayer({
} }
const video = document.createElement('video') const video = document.createElement('video')
video.src = src video.src = playableSrc
video.preload = 'metadata' video.preload = 'metadata'
video.onloadedmetadata = () => { video.onloadedmetadata = () => {
@ -156,7 +158,7 @@ export default function MediaPlayer({
} catch { } catch {
setProbeFailed(true) setProbeFailed(true)
} }
}, [src, showEmbed]) }, [playableSrc, showEmbed])
const onEmbedReady = useCallback(() => { const onEmbedReady = useCallback(() => {
if (readyOnceRef.current) return if (readyOnceRef.current) return
@ -170,21 +172,21 @@ export default function MediaPlayer({
return t('Preparing player…', { defaultValue: 'Preparing player…' }) return t('Preparing player…', { defaultValue: 'Preparing player…' })
} }
if (!embedPainted) { if (!embedPainted) {
if (isZapStreamWatchPageUrl(src)) { if (isZapStreamWatchPageUrl(playableSrc)) {
return t('Starting stream…', { defaultValue: 'Starting stream…' }) return t('Starting stream…', { defaultValue: 'Starting stream…' })
} }
if (isHlsPlaylistUrl(src)) { if (isHlsPlaylistUrl(playableSrc)) {
return t('Starting stream…', { defaultValue: 'Starting stream…' }) return t('Starting stream…', { defaultValue: 'Starting stream…' })
} }
return t('Loading media…', { defaultValue: 'Loading media…' }) return t('Loading media…', { defaultValue: 'Loading media…' })
} }
return undefined return undefined
}, [showEmbed, effectiveMediaType, embedPainted, src, t]) }, [showEmbed, effectiveMediaType, embedPainted, playableSrc, t])
if (!mustLoad && !showEmbed) { if (!mustLoad && !showEmbed) {
return ( return (
<LazyMediaTapPlaceholder <LazyMediaTapPlaceholder
src={src} src={playableSrc}
posterUrl={imagePoster} posterUrl={imagePoster}
blurHash={blurHash} blurHash={blurHash}
onActivate={() => setUserClickedLoad(true)} onActivate={() => setUserClickedLoad(true)}
@ -200,7 +202,7 @@ export default function MediaPlayer({
if (effectiveMediaType === null) { if (effectiveMediaType === null) {
return ( return (
<MediaEmbedBlurFrame <MediaEmbedBlurFrame
src={src} src={playableSrc}
posterUrl={imagePoster} posterUrl={imagePoster}
blurHash={blurHash} blurHash={blurHash}
className={className} className={className}
@ -217,7 +219,7 @@ export default function MediaPlayer({
{!embedPainted ? ( {!embedPainted ? (
<div className="relative z-10 w-full"> <div className="relative z-10 w-full">
<MediaEmbedBlurFrame <MediaEmbedBlurFrame
src={src} src={playableSrc}
posterUrl={imagePoster} posterUrl={imagePoster}
blurHash={blurHash} blurHash={blurHash}
className={className} className={className}
@ -247,7 +249,7 @@ export default function MediaPlayer({
/> />
) : effectiveMediaType === 'video' ? ( ) : effectiveMediaType === 'video' ? (
<VideoPlayer <VideoPlayer
src={src} src={playableSrc}
className={className} className={className}
poster={imagePoster} poster={imagePoster}
onReady={onEmbedReady} onReady={onEmbedReady}
@ -255,7 +257,7 @@ export default function MediaPlayer({
/> />
) : ( ) : (
<AudioPlayer <AudioPlayer
src={src} src={playableSrc}
className={className} className={className}
poster={imagePoster} poster={imagePoster}
onReady={onEmbedReady} onReady={onEmbedReady}

18
src/lib/url.ts

@ -455,12 +455,22 @@ export function primalR2aMirrorForBlossomPrimalUrl(url: string | URL): string |
} }
/** /**
* Display URL for note/imeta image `src`. Keep `https://blossom.primal.net/{sha256}.ext` as-is: it is the * URL for `<img src>` / `<video src>` / `<audio src>`. For `https://blossom.primal.net/{sha256}.ext`,
* canonical URL in events and usually loads reliably. Use {@link primalR2aMirrorForBlossomPrimalUrl} only * returns the `r2a.primal.net/uploads2/…` mirror when known so the browser loads bytes directly.
* as a fallback in {@link Image} `onError` when the blossom host fails. * The blossom host often answers with redirects; following those cross-origin responses commonly hits
* ORB / hotlink rules and fails to decode in-app even though the file exists.
*/
export function resolvePrimalBlossomPlayableUrl(url: string): string {
const t = url.trim()
if (!t) return t
return primalR2aMirrorForBlossomPrimalUrl(t) ?? t
}
/**
* Display / lightbox URL for note media. Same as {@link resolvePrimalBlossomPlayableUrl} for Primal blossom links.
*/ */
export function preferBlossomPrimalDisplayUrl(url: string): string { export function preferBlossomPrimalDisplayUrl(url: string): string {
return url return resolvePrimalBlossomPlayableUrl(url)
} }
/** /**

Loading…
Cancel
Save