Browse Source

fix video rendering

imwald
Silberengel 4 months ago
parent
commit
02290eb682
  1. 19
      src/components/Content/index.tsx
  2. 6
      src/components/MediaPlayer/index.tsx
  3. 5
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  4. 30
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 3
      src/components/VideoPlayer/index.tsx
  6. 19
      src/lib/tag.ts
  7. 1
      src/types/index.d.ts

19
src/components/Content/index.tsx

@ -148,14 +148,16 @@ export default function Content({ @@ -148,14 +148,16 @@ export default function Content({
const imageMap = new Map<string, TImetaInfo>()
const mediaMap = new Map<string, TImetaInfo>()
extractedMedia.all.forEach((img: TImetaInfo) => {
const cleaned = cleanUrl(img.url)
if (!cleaned) return
if (img.m?.startsWith('image/')) {
imageMap.set(img.url, img)
imageMap.set(cleaned, img)
} else if (img.m?.startsWith('video/') || img.m?.startsWith('audio/') || img.m === 'media/*') {
mediaMap.set(img.url, img)
} else if (isImage(img.url)) {
imageMap.set(img.url, img)
} else if (isMedia(img.url)) {
mediaMap.set(img.url, img)
mediaMap.set(cleaned, img)
} else if (isImage(cleaned)) {
imageMap.set(cleaned, img)
} else if (isMedia(cleaned)) {
mediaMap.set(cleaned, img)
}
})
@ -335,12 +337,15 @@ export default function Content({ @@ -335,12 +337,15 @@ export default function Content({
// Check video/audio first - never put them in ImageGallery
if (isVideoUrl || isAudioUrl || mediaMap.has(cleanedUrl)) {
renderedUrls.add(cleanedUrl)
const mediaInfo = mediaMap.get(cleanedUrl)
const poster = mediaInfo?.image
return (
<MediaPlayer
className="mt-2"
key={`url-media-${index}`}
src={cleanedUrl}
mustLoad={mustLoadMedia}
mustLoad={mustLoadMedia}
poster={poster}
/>
)
}

6
src/components/MediaPlayer/index.tsx

@ -8,11 +8,13 @@ import ExternalLink from '../ExternalLink' @@ -8,11 +8,13 @@ import ExternalLink from '../ExternalLink'
export default function MediaPlayer({
src,
className,
mustLoad = false
mustLoad = false,
poster
}: {
src: string
className?: string
mustLoad?: boolean
poster?: string
}) {
const { t } = useTranslation()
const { autoLoadMedia } = useContentPolicy()
@ -83,7 +85,7 @@ export default function MediaPlayer({ @@ -83,7 +85,7 @@ export default function MediaPlayer({
}
if (mediaType === 'video') {
return <VideoPlayer src={src} className={className} />
return <VideoPlayer src={src} className={className} poster={poster} />
}
return <AudioPlayer src={src} className={className} />

5
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -75,7 +75,7 @@ export default function AsciidocArticle({ @@ -75,7 +75,7 @@ export default function AsciidocArticle({
// Extract media from tags only (for display at top)
const tagMedia = useMemo(() => {
const seenUrls = new Set<string>()
const media: Array<{ url: string; type: 'image' | 'video' | 'audio' }> = []
const media: Array<{ url: string; type: 'image' | 'video' | 'audio'; poster?: string }> = []
// Extract from imeta tags
const imetaInfos = getImetaInfosFromEvent(event)
@ -88,7 +88,7 @@ export default function AsciidocArticle({ @@ -88,7 +88,7 @@ export default function AsciidocArticle({
if (info.m?.startsWith('image/') || isImage(cleaned)) {
media.push({ url: info.url, type: 'image' })
} else if (info.m?.startsWith('video/') || isVideo(cleaned)) {
media.push({ url: info.url, type: 'video' })
media.push({ url: info.url, type: 'video', poster: info.image })
} else if (info.m?.startsWith('audio/') || isAudio(cleaned)) {
media.push({ url: info.url, type: 'audio' })
}
@ -877,6 +877,7 @@ export default function AsciidocArticle({ @@ -877,6 +877,7 @@ export default function AsciidocArticle({
src={media.url}
className="max-w-[400px]"
mustLoad={true}
poster={media.poster}
/>
</div>
)

30
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -55,9 +55,10 @@ function parseMarkdownContent( @@ -55,9 +55,10 @@ function parseMarkdownContent(
openLightbox: (index: number) => void
navigateToHashtag: (href: string) => void
navigateToRelay: (url: string) => void
videoPosterMap?: Map<string, string>
}
): { nodes: React.ReactNode[]; hashtagsInContent: Set<string>; footnotes: Map<string, string> } {
const { eventPubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay } = options
const { eventPubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap } = options
const parts: React.ReactNode[] = []
const hashtagsInContent = new Set<string>()
const footnotes = new Map<string, string>()
@ -484,12 +485,14 @@ function parseMarkdownContent( @@ -484,12 +485,14 @@ function parseMarkdownContent(
</div>
)
} else if (isVideo(cleaned) || isAudio(cleaned)) {
const poster = videoPosterMap?.get(cleaned)
parts.push(
<div key={`media-${patternIdx}`} className="my-2">
<MediaPlayer
src={cleaned}
className="max-w-[400px]"
mustLoad={false}
poster={poster}
/>
</div>
)
@ -1127,7 +1130,7 @@ export default function MarkdownArticle({ @@ -1127,7 +1130,7 @@ export default function MarkdownArticle({
// Extract media from tags only (for display at top)
const tagMedia = useMemo(() => {
const seenUrls = new Set<string>()
const media: Array<{ url: string; type: 'image' | 'video' | 'audio' }> = []
const media: Array<{ url: string; type: 'image' | 'video' | 'audio'; poster?: string }> = []
// Extract from imeta tags
const imetaInfos = getImetaInfosFromEvent(event)
@ -1140,7 +1143,7 @@ export default function MarkdownArticle({ @@ -1140,7 +1143,7 @@ export default function MarkdownArticle({
if (info.m?.startsWith('image/') || isImage(cleaned)) {
media.push({ url: info.url, type: 'image' })
} else if (info.m?.startsWith('video/') || isVideo(cleaned)) {
media.push({ url: info.url, type: 'video' })
media.push({ url: info.url, type: 'video', poster: info.image })
} else if (info.m?.startsWith('audio/') || isAudio(cleaned)) {
media.push({ url: info.url, type: 'audio' })
}
@ -1349,6 +1352,21 @@ export default function MarkdownArticle({ @@ -1349,6 +1352,21 @@ export default function MarkdownArticle({
return preprocessMarkdownMediaLinks(event.content)
}, [event.content])
// Create video poster map from imeta tags
const videoPosterMap = useMemo(() => {
const map = new Map<string, string>()
const imetaInfos = getImetaInfosFromEvent(event)
imetaInfos.forEach((info) => {
if (info.image && (info.m?.startsWith('video/') || isVideo(info.url))) {
const cleaned = cleanUrl(info.url)
if (cleaned) {
map.set(cleaned, info.image)
}
}
})
return map
}, [event.id, JSON.stringify(event.tags)])
// Parse markdown content with post-processing for nostr: links and hashtags
const { nodes: parsedContent, hashtagsInContent } = useMemo(() => {
const result = parseMarkdownContent(preprocessedContent, {
@ -1356,11 +1374,12 @@ export default function MarkdownArticle({ @@ -1356,11 +1374,12 @@ export default function MarkdownArticle({
imageIndexMap,
openLightbox,
navigateToHashtag,
navigateToRelay
navigateToRelay,
videoPosterMap
})
// Return nodes and hashtags (footnotes are already included in nodes)
return { nodes: result.nodes, hashtagsInContent: result.hashtagsInContent }
}, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay])
}, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap])
// Filter metadata tags to only show what's not already in content
const leftoverMetadataTags = useMemo(() => {
@ -1453,6 +1472,7 @@ export default function MarkdownArticle({ @@ -1453,6 +1472,7 @@ export default function MarkdownArticle({
src={media.url}
className="max-w-[400px]"
mustLoad={true}
poster={media.poster}
/>
</div>
)

3
src/components/VideoPlayer/index.tsx

@ -6,7 +6,7 @@ import ExternalLink from '../ExternalLink' @@ -6,7 +6,7 @@ import ExternalLink from '../ExternalLink'
import { MediaErrorBoundary } from '../MediaErrorBoundary'
import logger from '@/lib/logger'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
export default function VideoPlayer({ src, className, poster }: { src: string; className?: string; poster?: string }) {
const { autoplay } = useContentPolicy()
const [error, setError] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
@ -64,6 +64,7 @@ export default function VideoPlayer({ src, className }: { src: string; className @@ -64,6 +64,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
playsInline
className={cn('rounded-lg max-h-[80vh] sm:max-h-[60vh] border', className)}
src={src}
poster={poster}
onClick={(e) => e.stopPropagation()}
onPlay={(event) => {
mediaManager.play(event.currentTarget)

19
src/lib/tag.ts

@ -180,6 +180,25 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta @@ -180,6 +180,25 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta
imeta.fallback = fallbackUrls
}
// Parse image/poster URL (for videos)
let imageUrl: string | undefined
// First try the space-separated format
const imageItem = tag.find((item) => item.startsWith('image '))
if (imageItem) {
imageUrl = imageItem.slice(6)
} else {
// Try the separate element format
const imageIndex = tag.findIndex((item) => item === 'image')
if (imageIndex !== -1 && imageIndex + 1 < tag.length) {
imageUrl = tag[imageIndex + 1]
}
}
if (imageUrl) {
imeta.image = cleanUrl(imageUrl)
}
return imeta
}

1
src/types/index.d.ts vendored

@ -123,6 +123,7 @@ export type TImetaInfo = { @@ -123,6 +123,7 @@ export type TImetaInfo = {
alt?: string // Alternative text
x?: string // SHA256 hash as specified in NIP 94
fallback?: string[] // Array of fallback URLs
image?: string // Poster/thumbnail image URL (for videos)
}
export type TPublishOptions = {

Loading…
Cancel
Save