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 @@ @@ -1,6 +1,6 @@
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import { isRenderableMediaUrl, isSafeMediaUrl } from '@/lib/url'
import { isRenderableMediaUrl, isSafeMediaUrl, resolvePrimalBlossomPlayableUrl } from '@/lib/url'
import { TImetaInfo } from '@/types'
import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash'
import { decode } from 'blurhash'
@ -102,7 +102,7 @@ export default function Image({ @@ -102,7 +102,7 @@ export default function Image({
const [isLoading, setIsLoading] = useState(urlOk && !effectiveHoldUntilClick)
const [displaySkeleton, setDisplaySkeleton] = useState(urlOk)
const [hasError, setHasError] = useState(!urlOk)
const [imageUrl, setImageUrl] = useState(url)
const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? ''))
const [fallbackIndex, setFallbackIndex] = useState(0)
const loadWatchRef = useRef<number | null>(null)
// 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({ @@ -149,7 +149,7 @@ export default function Image({
}
useEffect(() => {
setImageUrl(url)
setImageUrl(resolvePrimalBlossomPlayableUrl(url ?? ''))
loadSettledRef.current = false
wasInitiallyHeldRef.current = effectiveHoldUntilClick
const shouldHold = effectiveHoldUntilClick
@ -220,7 +220,7 @@ export default function Image({ @@ -220,7 +220,7 @@ export default function Image({
const next = fallback[fallbackIndex]
setFallbackIndex((prev) => prev + 1)
loadSettledRef.current = false
setImageUrl(next)
setImageUrl(resolvePrimalBlossomPlayableUrl(next))
return
}
setIsLoading(false)

38
src/components/MediaPlayer/index.tsx

@ -1,4 +1,4 @@ @@ -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 { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
@ -63,15 +63,17 @@ export default function MediaPlayer({ @@ -63,15 +63,17 @@ export default function MediaPlayer({
const [embedPainted, setEmbedPainted] = useState(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
// would hide the blurhash placeholder in LazyMediaTapPlaceholder.
const imagePoster = useMemo(() => {
const p = poster?.trim()
if (!p) return undefined
return isImage(p) ? p : undefined
return isImage(p) ? resolvePrimalBlossomPlayableUrl(p) : undefined
}, [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. */
const effectiveMediaType = mediaType ?? urlEmbedSurfaceHint
@ -86,7 +88,7 @@ export default function MediaPlayer({ @@ -86,7 +88,7 @@ export default function MediaPlayer({
setEmbedPainted(false)
setMediaType(null)
setProbeFailed(false)
}, [src])
}, [playableSrc])
useEffect(() => {
if (!showEmbed) {
@ -96,7 +98,7 @@ export default function MediaPlayer({ @@ -96,7 +98,7 @@ export default function MediaPlayer({
}
readyOnceRef.current = false
setEmbedPainted(false)
if (!src) {
if (!playableSrc) {
setProbeFailed(true)
return
}
@ -108,17 +110,17 @@ export default function MediaPlayer({ @@ -108,17 +110,17 @@ export default function MediaPlayer({
try {
// 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')
return
}
if (isZapStreamWatchPageUrl(src)) {
if (isZapStreamWatchPageUrl(playableSrc)) {
setMediaType('iframe')
return
}
const url = new URL(src)
const url = new URL(playableSrc)
const extension = url.pathname.split('.').pop()?.toLowerCase()
if (
@ -135,7 +137,7 @@ export default function MediaPlayer({ @@ -135,7 +137,7 @@ export default function MediaPlayer({
}
const video = document.createElement('video')
video.src = src
video.src = playableSrc
video.preload = 'metadata'
video.onloadedmetadata = () => {
@ -156,7 +158,7 @@ export default function MediaPlayer({ @@ -156,7 +158,7 @@ export default function MediaPlayer({
} catch {
setProbeFailed(true)
}
}, [src, showEmbed])
}, [playableSrc, showEmbed])
const onEmbedReady = useCallback(() => {
if (readyOnceRef.current) return
@ -170,21 +172,21 @@ export default function MediaPlayer({ @@ -170,21 +172,21 @@ export default function MediaPlayer({
return t('Preparing player…', { defaultValue: 'Preparing player…' })
}
if (!embedPainted) {
if (isZapStreamWatchPageUrl(src)) {
if (isZapStreamWatchPageUrl(playableSrc)) {
return t('Starting stream…', { defaultValue: 'Starting stream…' })
}
if (isHlsPlaylistUrl(src)) {
if (isHlsPlaylistUrl(playableSrc)) {
return t('Starting stream…', { defaultValue: 'Starting stream…' })
}
return t('Loading media…', { defaultValue: 'Loading media…' })
}
return undefined
}, [showEmbed, effectiveMediaType, embedPainted, src, t])
}, [showEmbed, effectiveMediaType, embedPainted, playableSrc, t])
if (!mustLoad && !showEmbed) {
return (
<LazyMediaTapPlaceholder
src={src}
src={playableSrc}
posterUrl={imagePoster}
blurHash={blurHash}
onActivate={() => setUserClickedLoad(true)}
@ -200,7 +202,7 @@ export default function MediaPlayer({ @@ -200,7 +202,7 @@ export default function MediaPlayer({
if (effectiveMediaType === null) {
return (
<MediaEmbedBlurFrame
src={src}
src={playableSrc}
posterUrl={imagePoster}
blurHash={blurHash}
className={className}
@ -217,7 +219,7 @@ export default function MediaPlayer({ @@ -217,7 +219,7 @@ export default function MediaPlayer({
{!embedPainted ? (
<div className="relative z-10 w-full">
<MediaEmbedBlurFrame
src={src}
src={playableSrc}
posterUrl={imagePoster}
blurHash={blurHash}
className={className}
@ -247,7 +249,7 @@ export default function MediaPlayer({ @@ -247,7 +249,7 @@ export default function MediaPlayer({
/>
) : effectiveMediaType === 'video' ? (
<VideoPlayer
src={src}
src={playableSrc}
className={className}
poster={imagePoster}
onReady={onEmbedReady}
@ -255,7 +257,7 @@ export default function MediaPlayer({ @@ -255,7 +257,7 @@ export default function MediaPlayer({
/>
) : (
<AudioPlayer
src={src}
src={playableSrc}
className={className}
poster={imagePoster}
onReady={onEmbedReady}

18
src/lib/url.ts

@ -455,12 +455,22 @@ export function primalR2aMirrorForBlossomPrimalUrl(url: string | URL): string | @@ -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
* canonical URL in events and usually loads reliably. Use {@link primalR2aMirrorForBlossomPrimalUrl} only
* as a fallback in {@link Image} `onError` when the blossom host fails.
* URL for `<img src>` / `<video src>` / `<audio src>`. For `https://blossom.primal.net/{sha256}.ext`,
* returns the `r2a.primal.net/uploads2/…` mirror when known so the browser loads bytes directly.
* 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 {
return url
return resolvePrimalBlossomPlayableUrl(url)
}
/**

Loading…
Cancel
Save