You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
HybridVideoPlayer.swift calls AVPlayer.replaceCurrentItem(with:) from Task.detached / Promise.async background contexts in 5 locations. This violates Apple's requirement that AVPlayer API access be serialized on the main queue, causing crashes under concurrent access (e.g., rapid scrolling through a video feed).
Environment
react-native-video: v7.0.0-alpha.12 (also confirmed on latest master as of 2026-02-20)
React Native: 0.81.5
iOS: 18.6.2
Device: iPhone 11 (iPhone12,1)
Architecture: New Architecture (Fabric + NitroModules)
The crash occurs because replaceSourceAsync calls AVPlayer.replaceCurrentItem(with:) on Thread 13 (a Task.detached worker thread) while the main thread is processing UIScrollView scroll events that read from the same AVPlayer instance via KVO observers.
Reproduction
Use VideoPlayer with initializeOnCreation: false in a pool of 3 players
Bind players to a vertical paging FlatList (TikTok-style feed)
Call player.replaceSourceAsync(...) when the active index changes
Scroll rapidly back and forth through the feed
Crash — typically within 15-30 seconds of aggressive scrolling
Example Implementation (JS-side)
Below is a minimal reproduction of the player pool pattern that triggers this crash. This is a standard approach for short-form video feeds (TikTok, Reels, Shorts) — 3 players are rotated via modulo arithmetic so that the current, previous, and next videos each get a dedicated player:
import{useCallback,useEffect,useRef}from"react";import{InteractionManager}from"react-native";import{typeVideoConfig,VideoPlayer,VideoView}from"react-native-video";// ── Pool setup ────────────────────────────────────────────────────────// Deferred-init: creates a native player without buffering anything.constPOOL_INIT_CONFIG={uri: "about:blank",initializeOnCreation: false};constBUFFER_CONFIG: VideoConfig["bufferConfig"]={bufferForPlaybackMs: 500,backBufferDurationMs: 10_000,preferredForwardBufferDurationMs: 10_000,};typePoolSlot=0|1|2;typePlayerTuple=[VideoPlayer,VideoPlayer,VideoPlayer];/** Positive modulo — maps any feed index to one of the 3 pool slots. */constmod3=(n: number): PoolSlot=>(((n%3)+3)%3)asPoolSlot;// ── Hook ──────────────────────────────────────────────────────────────exportfunctionusePlayerPool(){constplayersRef=useRef<PlayerTuple|null>(null);constsourcesRef=useRef<(string|null)[]>([null,null,null]);constactiveIndexRef=useRef(-1);// Lazily create 3 looping playersconstensurePlayers=useCallback((): PlayerTuple=>{if(playersRef.current)returnplayersRef.current;consttuple: PlayerTuple=[newVideoPlayer(POOL_INIT_CONFIG),newVideoPlayer(POOL_INIT_CONFIG),newVideoPlayer(POOL_INIT_CONFIG),];tuple.forEach((p)=>(p.loop=true));playersRef.current=tuple;returntuple;},[]);// Cleanup on unmountuseEffect(()=>{return()=>{playersRef.current?.forEach((p)=>{try{p.release();}catch{}});playersRef.current=null;};},[]);// Preload an adjacent (prev/next) slotconstpreloadSlot=useCallback((players: PlayerTuple,idx: number,getUrl: (i: number)=>string|null)=>{constslot=mod3(idx);constplayer=players[slot];consturl=getUrl(idx);if(!url||url===sourcesRef.current[slot])return;sourcesRef.current[slot]=url;player.pause();// This triggers the crash inside HybridVideoPlayer.replaceSourceAsync()// because the native side calls AVPlayer.replaceCurrentItem(with:) from// a Task.detached background thread.player.replaceSourceAsync({uri: url,bufferConfig: BUFFER_CONFIG}).then(()=>{try{player.preload();}catch{}}).catch(()=>{});},[],);// Activate a feed index — play current, preload prev/nextconstactivateIndex=useCallback((index: number,getUrl: (i: number)=>string|null)=>{constplayers=ensurePlayers();activeIndexRef.current=index;// Phase 1 — current slot (this frame)constslot=mod3(index);constplayer=players[slot];consturl=getUrl(index);if(!url){player.pause();return;}if(url===sourcesRef.current[slot]){// Already preloaded — instant playbackplayer.currentTime=0;player.play();}else{// New source — replaceSourceAsync then playsourcesRef.current[slot]=url;player.pause();constcaptured=index;player.replaceSourceAsync({uri: url,bufferConfig: BUFFER_CONFIG}).then(()=>{if(activeIndexRef.current!==captured)return;player.play();}).catch(()=>{});}// Phase 2 — adjacent slots (deferred until scroll settles)InteractionManager.runAfterInteractions(()=>{if(activeIndexRef.current!==index)return;preloadSlot(players,index-1,getUrl);preloadSlot(players,index+1,getUrl);});},[ensurePlayers,preloadSlot],);// Get player for a visible index (only current ± 1)constgetPlayer=useCallback((index: number): VideoPlayer|null=>{constplayers=playersRef.current;if(!players||activeIndexRef.current<0)returnnull;if(Math.abs(index-activeIndexRef.current)>1)returnnull;returnplayers[mod3(index)];},[]);return{ activateIndex, getPlayer };}// ── Usage in a FlatList ───────────────────────────────────────────────//// const { activateIndex, getPlayer } = usePlayerPool();//// // On viewable items change (debounced):// activateIndex(newIndex, (i) => feedData[i]?.videoUrl ?? null);//// // In renderItem:// const player = getPlayer(index);// return player ? <VideoView player={player} style={{ flex: 1 }} /> : null;
Key points about this JS code:
Each replaceSourceAsync call targets a different pool slot (guaranteed by mod3) — there are no concurrent calls on the same player
Adjacent preloading is deferred via InteractionManager.runAfterInteractions
All promises are properly .catch()-ed
Players are properly release()-d on unmount
Despite this correct usage, the crash occurs because the native implementation dispatches AVPlayer.replaceCurrentItem(with:) from Task.detached background threads inside HybridVideoPlayer.swift.
"AVFoundation serializes the access to AVPlayer and PlayerItem on the main queue. It's safe to access and register and unregister for observers for these objects on the main queue."
Additionally, iOS 18 introduced stricter runtime enforcement of this requirement — developers report "Incorrect actor executor assumption" crashes when calling replaceCurrentItem from non-main contexts, even when using @MainActor.
The Bug
HybridVideoPlayer is a plain class (not @MainActor-isolated, not an actor). Five methods call AVPlayer.replaceCurrentItem(with:) from background threads:
Meanwhile, VideoPlayerObserver registers KVO observers on the main queue and receives callbacks on the main queue. When replaceCurrentItem mutates currentItem from a background thread while KVO callbacks fire on the main thread, AVFoundation hits a thread-safety violation.
Proposed Fix
Wrap all AVPlayer.replaceCurrentItem(with:) calls in await MainActor.run { } (for async contexts) or dispatch to the main thread (for synchronous contexts):
For async call sites (init, initialize, preload, replaceSourceAsync):
// Before (crashes):
self.player.replaceCurrentItem(with:self.playerItem)
// After (safe):
awaitMainActor.run{self.player.replaceCurrentItem(with:self.playerItem)}
For synchronous call sites (release):
// Before (crashes if called from background):
self.player.replaceCurrentItem(with:nil)
// After (safe):
letplayerRef=self.player
ifThread.isMainThread {
playerRef.replaceCurrentItem(with:nil)}else{DispatchQueue.main.async{
playerRef.replaceCurrentItem(with:nil)}}
Alternative (long-term)
Mark HybridVideoPlayer as @MainActor to enforce main-thread isolation at the type level. This would require all callers to dispatch appropriately but would prevent future threading regressions.
Summary
HybridVideoPlayer.swiftcallsAVPlayer.replaceCurrentItem(with:)fromTask.detached/Promise.asyncbackground contexts in 5 locations. This violates Apple's requirement that AVPlayer API access be serialized on the main queue, causing crashes under concurrent access (e.g., rapid scrolling through a video feed).Environment
masteras of 2026-02-20)Crash Report
Thread 13 (crashed):
Exception:
Swift runtime failure: unhandled C++ / Objective-C exception(EXC_BREAKPOINT / SIGTRAP)
Thread 0 (main) at the time of crash:
The crash occurs because
replaceSourceAsynccallsAVPlayer.replaceCurrentItem(with:)on Thread 13 (aTask.detachedworker thread) while the main thread is processing UIScrollView scroll events that read from the same AVPlayer instance via KVO observers.Reproduction
VideoPlayerwithinitializeOnCreation: falsein a pool of 3 playersFlatList(TikTok-style feed)player.replaceSourceAsync(...)when the active index changesExample Implementation (JS-side)
Below is a minimal reproduction of the player pool pattern that triggers this crash. This is a standard approach for short-form video feeds (TikTok, Reels, Shorts) — 3 players are rotated via modulo arithmetic so that the current, previous, and next videos each get a dedicated player:
Key points about this JS code:
replaceSourceAsynccall targets a different pool slot (guaranteed bymod3) — there are no concurrent calls on the same playersourcesRef) avoids redundantreplaceSourceAsynccallsInteractionManager.runAfterInteractions.catch()-edrelease()-d on unmountDespite this correct usage, the crash occurs because the native implementation dispatches
AVPlayer.replaceCurrentItem(with:)fromTask.detachedbackground threads insideHybridVideoPlayer.swift.Root Cause Analysis
Apple's Threading Requirement
From WWDC 2014 Session 503 — Mastering Modern Media Playback:
Additionally, iOS 18 introduced stricter runtime enforcement of this requirement — developers report "Incorrect actor executor assumption" crashes when calling
replaceCurrentItemfrom non-main contexts, even when using@MainActor.The Bug
HybridVideoPlayeris a plain class (not@MainActor-isolated, not an actor). Five methods callAVPlayer.replaceCurrentItem(with:)from background threads:1.
init(line ~52)2.
initialize()(line ~194)3.
release()(line ~209)4.
preload()(line ~253)5.
replaceSourceAsync()(line ~329) — crash siteMeanwhile,
VideoPlayerObserverregisters KVO observers on the main queue and receives callbacks on the main queue. WhenreplaceCurrentItemmutatescurrentItemfrom a background thread while KVO callbacks fire on the main thread, AVFoundation hits a thread-safety violation.Proposed Fix
Wrap all
AVPlayer.replaceCurrentItem(with:)calls inawait MainActor.run { }(for async contexts) or dispatch to the main thread (for synchronous contexts):For async call sites (init, initialize, preload, replaceSourceAsync):
For synchronous call sites (release):
Alternative (long-term)
Mark
HybridVideoPlayeras@MainActorto enforce main-thread isolation at the type level. This would require all callers to dispatch appropriately but would prevent future threading regressions.Relationship to PR #4828
PR #4828 (merged 2026-02-03, included in v7.0.0-beta.6) fixes a different crash in
release()related to KVO observer removal ordering:Cannot remove an observer for the key path 'currentItem.status'caused by settingplayerItem = nilandplayerObserver = nilwhile observers were still registered.replaceCurrentItem(with:)called from background threads across all 5 call sites, causing thread-safety violations in AVFoundation.These are separate issues. PR #4828 addresses KVO lifecycle ordering; this issue addresses thread confinement of AVPlayer API calls.
Diff