From 91853ed538ddd1be0ec8cd22ec3799b343be8f9f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 23 Sep 2024 20:30:34 +0100 Subject: [PATCH] [Video] Flush low quality segments once focused (#5430) * Update VideoEmbedInnerWeb.tsx * keep proper track and flush properly * consistent current * use current in listener * manually loop --- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index fa577fb509..82b2503eb1 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' -import Hls from 'hls.js' +import Hls, {Events, FragChangedData, Fragment} from 'hls.js' import {atoms as a} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' @@ -19,7 +19,7 @@ export function VideoEmbedInnerWeb({ onScreen: boolean }) { const containerRef = useRef(null) - const ref = useRef(null) + const videoRef = useRef(null) const [focused, setFocused] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) const figId = useId() @@ -31,13 +31,13 @@ export function VideoEmbedInnerWeb({ } const hlsRef = useRef(undefined) + const [lowQualityFragments, setLowQualityFragments] = useState([]) useEffect(() => { - if (!ref.current) return + if (!videoRef.current) return if (!Hls.isSupported()) throw new HLSUnsupportedError() const hls = new Hls({ - capLevelToPlayerSize: true, maxMaxBufferLength: 10, // only load 10s ahead // note: the amount buffered is affected by both maxBufferLength and maxBufferSize // it will buffer until it it's greater than *both* of those values @@ -45,18 +45,36 @@ export function VideoEmbedInnerWeb({ }) hlsRef.current = hls - hls.attachMedia(ref.current) + hls.attachMedia(videoRef.current) hls.loadSource(embed.playlist) // initial value, later on it's managed by Controls hls.autoLevelCapping = 0 + // manually loop, so if we've flushed the first buffer it doesn't get confused + const abortController = new AbortController() + const {signal} = abortController + videoRef.current.addEventListener( + 'ended', + function () { + this.currentTime = 0 + this.play() + }, + {signal}, + ) + 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 ( @@ -67,6 +85,8 @@ export function VideoEmbedInnerWeb({ } else { setError(data.error) } + } else { + console.error(data.error) } }) @@ -74,20 +94,61 @@ export function VideoEmbedInnerWeb({ hlsRef.current = undefined hls.detachMedia() hls.destroy() + abortController.abort() } }, [embed.playlist]) + // purge low quality segments from buffer on next frag change + useEffect(() => { + if (!hlsRef.current) return + + const current = hlsRef.current + + if (focused) { + function fragChanged( + _event: Events.FRAG_CHANGED, + {frag}: FragChangedData, + ) { + // if the current quality level goes above 0, flush the low quality segments + if (current.nextAutoLevel > 0) { + const flushed: Fragment[] = [] + + for (const lowQualFrag of lowQualityFragments) { + // avoid if close to the current fragment + if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { + return + } + + current.trigger(Hls.Events.BUFFER_FLUSHING, { + startOffset: lowQualFrag.start, + endOffset: lowQualFrag.end, + type: 'video', + }) + + flushed.push(lowQualFrag) + } + + setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) + } + } + current.on(Hls.Events.FRAG_CHANGED, fragChanged) + + return () => { + current.off(Hls.Events.FRAG_CHANGED, fragChanged) + } + } + }, [focused, lowQualityFragments]) + return (