在onChange事件中设置后,AVPlayer的currentItem始终为nil的问题排查
AVPlayer渲染阶段currentItem始终为nil的原因及解决方法
我尝试播放从onChange事件中设置的流媒体URL,闭包内打印的URL正常(非nil),但在View的body渲染阶段,AVPlayer的currentItem却始终为nil。相关代码如下:
struct VideoPlayerContainerView: View { @EnvironmentObject private var store: AppStore private var player = AVPlayer() @State private var urlToPlay: URL? @State private var playingUrl: URL? var body: some View { let currentPlayerUrl = ((player.currentItem?.asset) as? AVURLAsset)?.url print("asset loaded in the VideoPlayer ⬇") print(currentPlayerUrl ?? "no asset!") // 始终为nil return ZStack { VideoPlayerView(player: player) } .onChange(of: self.store.state.videos.playingVideo?.stream?.best.url) { [oldValue = self.store.state.videos.playingVideo?.stream?.best.url] newValue in print("- - - - - - - - - - - - ") self.urlToPlay = newValue if (self.urlToPlay != self.playingUrl) { let newPlayerItem = self.urlToPlay != nil ? AVPlayerItem(url: self.urlToPlay!) : nil player.replaceCurrentItem(with: newPlayerItem) self.playingUrl = self.urlToPlay print("newPlayerItem ⬇") print(newPlayerItem ?? "nil") print("url that should play now ⬇") let currentPlayerUrl = ((player.currentItem?.asset) as? AVURLAsset)?.url print(currentPlayerUrl ?? "nil") // 不为nil } } } } struct VideoPlayerView: View { let player: AVPlayer var body: some View { VideoPlayer(player: player) } }
原因分析
核心问题在于你声明的private var player = AVPlayer()是普通存储属性。在SwiftUI的View生命周期里,每次View因为状态变化重新渲染时,这个普通属性都会被重新初始化——也就是说,你在onChange闭包里给旧player设置的currentItem,到了渲染阶段已经变成了一个全新的player实例,自然没有任何currentItem,所以打印结果始终是nil。
拆解来看:
onChange触发时,你操作的是当前View实例的player,设置完currentItem后打印正常;- 但
onChange执行后,playingUrl或urlToPlay的更新会触发View重新计算body,此时新的player实例被创建,旧的player被丢弃,渲染阶段打印的是新player的状态,所以显示nil。
解决方案
要解决这个问题,关键是让player实例在View重新渲染时保持不变,有两种常用实现方式:
方法一:用@StateObject包装AVPlayer
因为AVPlayer本身不是ObservableObject,所以需要先做一层包装:
class PlayerWrapper: ObservableObject { let player = AVPlayer() } struct VideoPlayerContainerView: View { @EnvironmentObject private var store: AppStore @StateObject private var playerWrapper = PlayerWrapper() // @StateObject保证实例不被重置 @State private var playingUrl: URL? var body: some View { let currentPlayerUrl = ((playerWrapper.player.currentItem?.asset) as? AVURLAsset)?.url print("asset loaded in the VideoPlayer ⬇") print(currentPlayerUrl ?? "no asset!") return ZStack { VideoPlayerView(player: playerWrapper.player) } .onChange(of: store.state.videos.playingVideo?.stream?.best.url) { newValue in guard let newValue = newValue, newValue != playingUrl else { return } let newPlayerItem = AVPlayerItem(url: newValue) playerWrapper.player.replaceCurrentItem(with: newPlayerItem) playingUrl = newValue print("- - - - - - - - - - - - ") print("newPlayerItem ⬇") print(newPlayerItem) print("url that should play now ⬇") let currentPlayerUrl = ((playerWrapper.player.currentItem?.asset) as? AVURLAsset)?.url print(currentPlayerUrl ?? "nil") } } }
方法二:直接用@State持有AVPlayer
利用@State可以在View生命周期中保持引用类型实例的特性,直接把player声明为@State属性:
struct VideoPlayerContainerView: View { @EnvironmentObject private var store: AppStore @State private var player = AVPlayer() // @State保证实例不被重置 @State private var playingUrl: URL? var body: some View { let currentPlayerUrl = ((player.currentItem?.asset) as? AVURLAsset)?.url print("asset loaded in the VideoPlayer ⬇") print(currentPlayerUrl ?? "no asset!") return ZStack { VideoPlayerView(player: player) } .onChange(of: store.state.videos.playingVideo?.stream?.best.url) { newValue in guard let newValue = newValue, newValue != playingUrl else { return } let newPlayerItem = AVPlayerItem(url: newValue) player.replaceCurrentItem(with: newPlayerItem) playingUrl = newValue print("- - - - - - - - - - - - ") print("newPlayerItem ⬇") print(newPlayerItem) print("url that should play now ⬇") let currentPlayerUrl = ((player.currentItem?.asset) as? AVURLAsset)?.url print(currentPlayerUrl ?? "nil") } } }
内容的提问来源于stack exchange,提问作者zakdances
相关产品推荐
相关产品推荐

