簡體   English   中英

在 React 中處理媒體播放 State

[英]Handling Media Playback State in React

目前正在使用 expo + React Native 並遇到有效處理媒體播放 state 的挑戰。 通過 expo 的AV 庫,我們得到了一個圍繞播放資源的命令式接口,帶有一個掛鈎播放 state 更新的參數:

// this runs every 50ms (or faster) during active playback
const handlePlaybackStatusUpdate = (nextStatus: AVPlaybackStatus) => {
    // do something in here
}

const loadedSound = (await Audio.Sound.createAsync({ uri }, playbackConfig, handlePlaybackStatusUpdate)).sound

// ...

await loadedSound.playAsync()

了解“高性能”受到RN 應用程序中線程通信限制的限制,

我的實施目標是:

  1. 依賴播放狀態/功能的組件應該是高性能的(不受基於播放的重新渲染/更新的限制)
  2. 播放功能應該是一致的,理想情況下可以在層次結構中的不同點的消費者之間共享(例如,進度條和播放暫停覆蓋)
  3. 需要更高播放更新率的組件(例如播放進度條)應該能夠選擇加入更頻繁的更新而不影響其他消費者

我一直在努力尋找一種有效處理這些播放狀態更新的方法。 進一步的上下文。 播放狀態暴露了播放的許多維度 state - isPlaying, isBuffering, didJustFinish, durationMillis (播放長度以毫秒為單位), positionMillis (播放中的當前 position) 等。根據實驗和觀察,positionMillis 是絕大多數情況下唯一變化的值更新(除了那些與 playAsync、pauseAsync 等命令相關的更新)因此,以下嘗試公開基於測量 position 的 stream 更新,獨立於上下文 state 的 rest。

我嘗試過的一些事情:

  • 將這些更新與組件 state 耦合並通過上下文共享。 可以理解的是,這會產生大量的重新渲染,以至於觸摸事件似乎沒有被廣播或處理。
const PlaybackProvider = ({ children }: { children: ReactNode }) => {
    const [playbackStatus, setPlaybackStatus] = useState<PlaybackStatus>()
    // ...
    await Audio.Sound.createAsync({ uri }, playbackConfig, setPlaybackStatus)
  • 附加一個 ref 並根據消費者的偏好對其進行輪詢。 這種做法很臭。 對於只消費的最小、深層子組件可能可行,但對於實現不是由 DOM 節點 + 回調引用提供支持的“更新引用”似乎是站不住腳的。
const usePlaybackPosition = (
    onUpdate: (position: number, duration: number) => Promise<void> | void,
    interval = 200
) => {
    const { playbackStatusRef } = usePlayback()
    const [timer, setTimer] = useState(0)
    useInterval(() => {
        const { positionMillis, durationMillis } = playbackStatusRef?.current ?? {}
        onUpdate(positionMillis, durationMillis)
        setTimer(timer + 1)
    }, interval)
}
  • 嘗試在 onPlaybackStatusUpdate 掛鈎中智能地設置上下文 state。 這似乎使大量陳舊的 setPlaybackStatus 調用排隊/通常不會按預期運行。 很難推理為什么會這樣。
const handlePlaybackStatusUpdate = (newStatus?: AVPlaybackStatus): void => {
    // save a ref for components that want to listen to position
    playbackStatusRef.current = newStatus
    
    if (newStatus === undefined) {
        setPlaybackStatus(PlaybackStatus.None)
        return
    }

    if (!newStatus.isLoaded) {
        if (newStatus.error) {
            console.log(`Encountered a fatal error during playback: ${newStatus.error}`)
            setPlaybackStatus(PlaybackStatus.Error)
            return
        }
    } else {
        const { isPlaying, isBuffering, positionMillis, didJustFinish, isLooping } = newStatus

        let nextStatus: PlaybackStatus = PlaybackStatus.Idle

        if (isPlaying) {
            nextStatus = PlaybackStatus.Playing
        } else if (positionMillis > 0) {
            nextStatus = PlaybackStatus.Paused
        } else {
            // not playing and position is 0
            nextStatus = PlaybackStatus.Idle
        }

      if (didJustFinish && !isLooping) {
          nextStatus = PlaybackStatus.Finished
      }

      if (nextStatus !== playbackStatus) {
          // this conditional ends up running TONS of times before playbackStatus actually changes
          setPlaybackStatus(nextStatus)
      }
    }
  }

我希望保持簡單和正統的解決方案 re: React 最佳實踐。 盡管 React 文檔表明媒體播放是對 ref 的有效使用,所以可能只需要提交並強化基於 ref 的接口。 我希望組件層次結構的更智能組合/重構+復制/共享行為的掛鈎/上下文就足夠了。

上面可能有一些不正確的假設。 如果能指出這些,我將不勝感激。 同樣,非常感謝關於 expo/react-native 中媒體播放的已知限制/問題的任何和所有見解。 感謝您的幫助!

我們所做的一項優化是onPlaybackStatusUpdate鈎子的抖動 僅每n毫秒處理一次更新。 分享一段代碼供參考(不是完整的實現):


const debouncedPlaybackStatusUpdate = useMemo(
        () => debounce(onPlaybackStatusUpdate, 100),
        [],
    );

async function playSound(status: boolean) {
        if (!status) {
            await soundObj.playAsync();
            soundObj.setOnPlaybackStatusUpdate(debouncedPlaybackStatusUpdate);
        } else {
            await soundObj.pauseAsync();
        }
    }

這應該減少大部分重新渲染。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM