[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
This commit is contained in:
Samuel Newman
2024-09-23 20:30:34 +01:00
committed by GitHub
parent 5eb294488f
commit 91853ed538
@@ -1,7 +1,7 @@
import React, {useEffect, useId, useRef, useState} from 'react' import React, {useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyEmbedVideo} from '@atproto/api' 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 {atoms as a} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
@@ -19,7 +19,7 @@ export function VideoEmbedInnerWeb({
onScreen: boolean onScreen: boolean
}) { }) {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const ref = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const [focused, setFocused] = useState(false) const [focused, setFocused] = useState(false)
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
const figId = useId() const figId = useId()
@@ -31,13 +31,13 @@ export function VideoEmbedInnerWeb({
} }
const hlsRef = useRef<Hls | undefined>(undefined) const hlsRef = useRef<Hls | undefined>(undefined)
const [lowQualityFragments, setLowQualityFragments] = useState<Fragment[]>([])
useEffect(() => { useEffect(() => {
if (!ref.current) return if (!videoRef.current) return
if (!Hls.isSupported()) throw new HLSUnsupportedError() if (!Hls.isSupported()) throw new HLSUnsupportedError()
const hls = new Hls({ const hls = new Hls({
capLevelToPlayerSize: true,
maxMaxBufferLength: 10, // only load 10s ahead maxMaxBufferLength: 10, // only load 10s ahead
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize // note: the amount buffered is affected by both maxBufferLength and maxBufferSize
// it will buffer until it it's greater than *both* of those values // it will buffer until it it's greater than *both* of those values
@@ -45,18 +45,36 @@ export function VideoEmbedInnerWeb({
}) })
hlsRef.current = hls hlsRef.current = hls
hls.attachMedia(ref.current) hls.attachMedia(videoRef.current)
hls.loadSource(embed.playlist) hls.loadSource(embed.playlist)
// initial value, later on it's managed by Controls // initial value, later on it's managed by Controls
hls.autoLevelCapping = 0 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) => { hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => {
if (data.subtitleTracks.length > 0) { if (data.subtitleTracks.length > 0) {
setHasSubtitleTrack(true) 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) => { hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) { if (data.fatal) {
if ( if (
@@ -67,6 +85,8 @@ export function VideoEmbedInnerWeb({
} else { } else {
setError(data.error) setError(data.error)
} }
} else {
console.error(data.error)
} }
}) })
@@ -74,20 +94,61 @@ export function VideoEmbedInnerWeb({
hlsRef.current = undefined hlsRef.current = undefined
hls.detachMedia() hls.detachMedia()
hls.destroy() hls.destroy()
abortController.abort()
} }
}, [embed.playlist]) }, [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 ( return (
<View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
<div ref={containerRef} style={{height: '100%', width: '100%'}}> <div ref={containerRef} style={{height: '100%', width: '100%'}}>
<figure style={{margin: 0, position: 'absolute', inset: 0}}> <figure style={{margin: 0, position: 'absolute', inset: 0}}>
<video <video
ref={ref} ref={videoRef}
poster={embed.thumbnail} poster={embed.thumbnail}
style={{width: '100%', height: '100%', objectFit: 'contain'}} style={{width: '100%', height: '100%', objectFit: 'contain'}}
playsInline playsInline
preload="none" preload="none"
loop
muted={!focused} muted={!focused}
aria-labelledby={embed.alt ? figId : undefined} aria-labelledby={embed.alt ? figId : undefined}
/> />
@@ -110,7 +171,7 @@ export function VideoEmbedInnerWeb({
)} )}
</figure> </figure>
<Controls <Controls
videoRef={ref} videoRef={videoRef}
hlsRef={hlsRef} hlsRef={hlsRef}
active={active} active={active}
setActive={setActive} setActive={setActive}