From 45f0f7eefecae1922c2f30d4e7760d2b93b1ae56 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Jun 2025 12:05:41 -0500 Subject: Port post embeds to new arch (#7408) * Direct port of embeds to new arch (cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6) * Re-org * Split out ListEmbed and FeedEmbed * Split out ImageEmbed * DRY up a bit * Port over ExternalLinkEmbed * Port over Player and Gif embeds * Migrate ComposerReplyTo * Replace other usages of old post-embeds * Migrate view contexts * Copy pasta VideoEmbed * Copy pasta GifEmbed * Swap in new file location * Clean up * Fix up native * Add back in correct moderation on List and Feed embeds * Format * Prettier * delete old video utils * move bandwidth-estimate.ts * Remove log * Add LazyQuoteEmbed for composer use * Clean up unused things * Remove remaining items * Prettier * Fix imports * Handle nested quotes same as prod * Add back silenced error handling * Fix lint --------- Co-authored-by: Samuel Newman --- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 307 --------------------- 1 file changed, 307 deletions(-) delete mode 100644 src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx (limited to 'src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx') diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx deleted file mode 100644 index ce3a7b2c9..000000000 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import {useEffect, useId, useRef, useState} from 'react' -import {View} from 'react-native' -import {type AppBskyEmbedVideo} from '@atproto/api' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import type * as HlsTypes from 'hls.js' - -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {atoms as a} from '#/alf' -import {MediaInsetBorder} from '#/components/MediaInsetBorder' -import * as BandwidthEstimate from './bandwidth-estimate' -import {Controls} from './web-controls/VideoControls' - -export function VideoEmbedInnerWeb({ - embed, - active, - setActive, - onScreen, - lastKnownTime, -}: { - embed: AppBskyEmbedVideo.View - active: boolean - setActive: () => void - onScreen: boolean - lastKnownTime: React.MutableRefObject -}) { - const containerRef = useRef(null) - const videoRef = useRef(null) - const [focused, setFocused] = useState(false) - const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) - const [hlsLoading, setHlsLoading] = useState(false) - const figId = useId() - const {_} = useLingui() - - // send error up to error boundary - const [error, setError] = useState(null) - if (error) { - throw error - } - - const hlsRef = useHLS({ - playlist: embed.playlist, - setHasSubtitleTrack, - setError, - videoRef, - setHlsLoading, - }) - - useEffect(() => { - if (lastKnownTime.current && videoRef.current) { - videoRef.current.currentTime = lastKnownTime.current - } - }, [lastKnownTime]) - - return ( - -
-
-
- -
- -
- ) -} - -export class HLSUnsupportedError extends Error { - constructor() { - super('HLS is not supported') - } -} - -export class VideoNotFoundError extends Error { - constructor() { - super('Video not found') - } -} - -type CachedPromise = Promise & {value: undefined | T} -const promiseForHls = import( - // @ts-ignore - 'hls.js/dist/hls.min' -).then(mod => mod.default) as CachedPromise -promiseForHls.value = undefined -promiseForHls.then(Hls => { - promiseForHls.value = Hls -}) - -function useHLS({ - playlist, - setHasSubtitleTrack, - setError, - videoRef, - setHlsLoading, -}: { - playlist: string - setHasSubtitleTrack: (v: boolean) => void - setError: (v: Error | null) => void - videoRef: React.RefObject - setHlsLoading: (v: boolean) => void -}) { - const [Hls, setHls] = useState( - () => promiseForHls.value, - ) - useEffect(() => { - if (!Hls) { - setHlsLoading(true) - promiseForHls.then(loadedHls => { - setHls(() => loadedHls) - setHlsLoading(false) - }) - } - }, [Hls, setHlsLoading]) - - const hlsRef = useRef(undefined) - const [lowQualityFragments, setLowQualityFragments] = useState< - HlsTypes.Fragment[] - >([]) - - // purge low quality segments from buffer on next frag change - const handleFragChange = useNonReactiveCallback( - ( - _event: HlsTypes.Events.FRAG_CHANGED, - {frag}: HlsTypes.FragChangedData, - ) => { - if (!Hls) return - if (!hlsRef.current) return - const hls = hlsRef.current - - // if the current quality level goes above 0, flush the low quality segments - if (hls.nextAutoLevel > 0) { - const flushed: HlsTypes.Fragment[] = [] - - for (const lowQualFrag of lowQualityFragments) { - // avoid if close to the current fragment - if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { - continue - } - - hls.trigger(Hls.Events.BUFFER_FLUSHING, { - startOffset: lowQualFrag.start, - endOffset: lowQualFrag.end, - type: 'video', - }) - - flushed.push(lowQualFrag) - } - - setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) - } - }, - ) - - const flushOnLoop = useNonReactiveCallback(() => { - if (!Hls) return - if (!hlsRef.current) return - const hls = hlsRef.current - // the above callback will catch most stale frags, but there's a corner case - - // if there's only one segment in the video, it won't get flushed because it avoids - // flushing the currently active segment. Therefore, we have to catch it when we loop - if ( - hls.nextAutoLevel > 0 && - lowQualityFragments.length === 1 && - lowQualityFragments[0].start === 0 - ) { - const lowQualFrag = lowQualityFragments[0] - - hls.trigger(Hls.Events.BUFFER_FLUSHING, { - startOffset: lowQualFrag.start, - endOffset: lowQualFrag.end, - type: 'video', - }) - setLowQualityFragments([]) - } - }) - - useEffect(() => { - if (!videoRef.current) return - if (!Hls) return - if (!Hls.isSupported()) { - throw new HLSUnsupportedError() - } - - const hls = new Hls({ - maxMaxBufferLength: 10, // only load 10s ahead - // note: the amount buffered is affected by both maxBufferLength and maxBufferSize - // it will buffer until it is greater than *both* of those values - // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead - }) - hlsRef.current = hls - - const latestEstimate = BandwidthEstimate.get() - if (latestEstimate !== undefined) { - hls.bandwidthEstimate = latestEstimate - } - - hls.attachMedia(videoRef.current) - hls.loadSource(playlist) - - // manually loop, so if we've flushed the first buffer it doesn't get confused - const abortController = new AbortController() - const {signal} = abortController - const videoNode = videoRef.current - videoNode.addEventListener( - 'ended', - () => { - flushOnLoop() - videoNode.currentTime = 0 - videoNode.play() - }, - {signal}, - ) - - hls.on(Hls.Events.FRAG_LOADED, () => { - BandwidthEstimate.set(hls.bandwidthEstimate) - }) - - hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => { - if (data.subtitleTracks.length > 0) { - setHasSubtitleTrack(true) - } - }) - - hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => { - if (frag.level === 0) { - setLowQualityFragments(prev => [...prev, frag]) - } - }) - - hls.on(Hls.Events.ERROR, (_event, data) => { - if (data.fatal) { - if ( - data.details === 'manifestLoadError' && - data.response?.code === 404 - ) { - setError(new VideoNotFoundError()) - } else { - setError(data.error) - } - } else { - console.error(data.error) - } - }) - - hls.on(Hls.Events.FRAG_CHANGED, handleFragChange) - - return () => { - hlsRef.current = undefined - hls.detachMedia() - hls.destroy() - abortController.abort() - } - }, [ - playlist, - setError, - setHasSubtitleTrack, - videoRef, - handleFragChange, - flushOnLoop, - Hls, - ]) - - return hlsRef -} -- cgit 1.4.1