1

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

Daniil
  • 11
  • 2

0 Answers0