I have a view that I use to display video sequences. I often use this component in my code. When I move from the screen where this View is displayed to another screen with the same View but other videos, a crash occurs. Below is my swift code
import SwiftUI
import AVKit
import AVFoundation
struct PlayerView<VideoOverlay: View>: View {
@StateObject private var playerManager: QueuePlayerManager
@ViewBuilder var videoOverlay: () -> VideoOverlay
private let placeholder: String
init(
_ sequence: [String],
endAction: PlayerEndAction = .none,
@ViewBuilder videoOverlay: @escaping () -> VideoOverlay
) {
_playerManager = StateObject(
wrappedValue: QueuePlayerManager(
sequence: sequence,
endAction: endAction
)
)
self.placeholder = sequence.first ?? .init()
self.videoOverlay = videoOverlay
}
var body: some View {
GeometryReader { geometry in
if self.playerManager.isReadyToPlay {
VideoPlayer(
player: playerManager.player,
videoOverlay: videoOverlay
)
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.height * (DeviceUtils.isMediumScreen ? 1.25 : 0.75), height: geometry.size.height)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
.disabled(true)
.clipped()
.onAppear { self.playerManager.player.play() }
} else {
Image(placeholder)
.resizable()
.scaledToFill()
.clipped()
.transition(.opacity)
}
}
.edgesIgnoringSafeArea(.all)
}
private class QueuePlayerManager: ObservableObject {
let player: AVQueuePlayer
private var isRemovedItems: Bool = false
@Published private(set) var isReadyToPlay = false
init(
sequence: [String],
endAction: PlayerEndAction
) {
var sequence = sequence
if let lastItem = sequence.last {
sequence.append(lastItem)
}
let urls: [URL] = sequence.map {
Bundle.main.url(
forResource: $0,
withExtension: "mp4"
)!
}
let playerItems = urls.map { AVPlayerItem(url: $0) }
player = AVQueuePlayer(items: playerItems)
player.actionAtItemEnd = .none
if endAction != .none {
//MARK: - Add observer loop
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: nil,
queue: nil
) { [weak self] notification in
guard let self = self else { return }
let currentItem = notification.object as? AVPlayerItem
//MARK: - Loop last item
if endAction == .loop, let currentItem = currentItem {
if self.isRemovedItems {
self.player.seek(to: .zero)
}
self.player.advanceToNextItem()
if self.player.canInsert(currentItem, after: nil) {
self.player.insert(currentItem, after: nil)
}
if !self.isRemovedItems {
self.player.seek(to: .zero)
}
}
//MARK: - Remove other items
guard !self.isRemovedItems else { return }
let lastTwoItems = playerItems.suffix(2)
playerItems.forEach { item in
if !lastTwoItems.contains(item) {
self.player.remove(item)
}
}
self.isRemovedItems.toggle()
}
}
NotificationCenter.default.addObserver(
forName: .AVPlayerItemTimeJumped,
object: player.currentItem,
queue: .main
) { [weak self] _ in
guard let self = self else { return }
withAnimation {
self.isReadyToPlay = true
}
}
}
deinit {
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemDidPlayToEndTime,
object: nil
)
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemTimeJumped,
object: nil
)
}
}
}
enum PlayerEndAction: Equatable {
case none, loop
}
extension PlayerView where VideoOverlay == EmptyView {
init(
_ sequence: [String],
endAction: PlayerEndAction
) {
self.init(
sequence,
endAction: endAction
) { EmptyView() }
}
}
I tried to experiment with the init parameters. There was a thought to create a new instance of AVPlayer, but I couldn't do it