diff --git a/packages/react-native-video/ios/view/VideoComponentView.swift b/packages/react-native-video/ios/view/VideoComponentView.swift index 1cf2423a..f5cb87d5 100644 --- a/packages/react-native-video/ios/view/VideoComponentView.swift +++ b/packages/react-native-video/ios/view/VideoComponentView.swift @@ -5,10 +5,10 @@ // Created by Krzysztof Moch on 30/09/2024. // -import Foundation -import UIKit import AVFoundation import AVKit +import Foundation +import UIKit @objc public class VideoComponentView: UIView { public weak var player: HybridVideoPlayerSpec? = nil { @@ -17,17 +17,17 @@ import AVKit configureAVPlayerViewController(with: player.player) } } - + var delegate: VideoViewDelegate? private var playerView: UIView? = nil - + private var observer: VideoComponentViewObserver? { didSet { playerViewController?.delegate = observer observer?.updatePlayerViewControllerObservers() } } - + private var _keepScreenAwake: Bool = false var keepScreenAwake: Bool { get { @@ -40,7 +40,7 @@ import AVKit _keepScreenAwake = newValue } } - + var playerViewController: AVPlayerViewController? { didSet { guard let observer, let playerViewController else { return } @@ -48,7 +48,7 @@ import AVKit observer.updatePlayerViewControllerObservers() } } - + public var controls: Bool = false { didSet { DispatchQueue.main.async { [weak self] in @@ -57,29 +57,30 @@ import AVKit } } } - + public var allowsPictureInPicturePlayback: Bool = false { didSet { DispatchQueue.main.async { [weak self] in guard let self = self, let playerViewController = self.playerViewController else { return } - + VideoManager.shared.requestAudioSessionUpdate() playerViewController.allowsPictureInPicturePlayback = self.allowsPictureInPicturePlayback } } } - + public var autoEnterPictureInPicture: Bool = false { didSet { DispatchQueue.main.async { [weak self] in guard let self = self, let playerViewController = self.playerViewController else { return } - + VideoManager.shared.requestAudioSessionUpdate() - playerViewController.canStartPictureInPictureAutomaticallyFromInline = self.allowsPictureInPicturePlayback + playerViewController.canStartPictureInPictureAutomaticallyFromInline = + self.autoEnterPictureInPicture } } } - + public var resizeMode: ResizeMode = .none { didSet { DispatchQueue.main.async { [weak self] in @@ -88,35 +89,36 @@ import AVKit } } } - + @objc public var nitroId: NSNumber = -1 { didSet { VideoComponentView.globalViewsMap.setObject(self, forKey: nitroId) } } - - @objc public static var globalViewsMap: NSMapTable = .strongToWeakObjects() - + + @objc public static var globalViewsMap: NSMapTable = + .strongToWeakObjects() + @objc public override init(frame: CGRect) { super.init(frame: frame) VideoManager.shared.register(view: self) setupPlayerView() observer = VideoComponentViewObserver(view: self) } - + deinit { VideoManager.shared.unregister(view: self) } - + @objc public required init?(coder: NSCoder) { super.init(coder: coder) setupPlayerView() } - - func setNitroId(nitroId: NSNumber) -> Void { + + func setNitroId(nitroId: NSNumber) { self.nitroId = nitroId } - + private func setupPlayerView() { // Create a UIView to hold the video player layer playerView = UIView(frame: self.bounds) @@ -127,19 +129,27 @@ import AVKit playerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), playerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), playerView.topAnchor.constraint(equalTo: self.topAnchor), - playerView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + playerView.bottomAnchor.constraint(equalTo: self.bottomAnchor), ]) } } - + public func configureAVPlayerViewController(with player: AVPlayer) { DispatchQueue.main.async { [weak self] in guard let self = self, let playerView = self.playerView else { return } + + // Skip reconfiguration if player hasn't changed and controller already exists + if let existingController = self.playerViewController, + existingController.player === player + { + return + } + // Remove previous controller if any self.playerViewController?.willMove(toParent: nil) self.playerViewController?.view.removeFromSuperview() self.playerViewController?.removeFromParent() - + let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = controls @@ -147,16 +157,16 @@ import AVKit controller.view.frame = playerView.bounds controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] controller.view.backgroundColor = .clear - + // We manage this manually in NowPlayingInfoCenterManager controller.updatesNowPlayingInfoCenter = false - + if #available(iOS 16.0, *) { if let initialSpeed = controller.speeds.first(where: { $0.rate == player.rate }) { controller.selectSpeed(initialSpeed) } } - + // Find nearest UIViewController if let parentVC = self.findViewController() { parentVC.addChild(controller) @@ -166,7 +176,7 @@ import AVKit } } } - + // Helper to find nearest UIViewController private func findViewController() -> UIViewController? { var responder: UIResponder? = self @@ -178,20 +188,20 @@ import AVKit } return nil } - + public override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) - + if newSuperview == nil { PluginsRegistry.shared.notifyVideoViewDestroyed(view: self) - + // We want to disable this when view is about to unmount if keepScreenAwake { keepScreenAwake = false } } else { PluginsRegistry.shared.notifyVideoViewCreated(view: self) - + // We want to restore keepScreenAwake after component remount if _keepScreenAwake { keepScreenAwake = true @@ -199,7 +209,6 @@ import AVKit } } - public override func layoutSubviews() { super.layoutSubviews() @@ -210,48 +219,48 @@ import AVKit subview.frame = playerView?.bounds ?? .zero } } - + public func enterFullscreen() throws { guard let playerViewController else { throw VideoViewError.viewIsDeallocated.error() } - + DispatchQueue.main.async { playerViewController.enterFullscreen(animated: true) } } - + public func exitFullscreen() throws { guard let playerViewController else { throw VideoViewError.viewIsDeallocated.error() } - + DispatchQueue.main.async { playerViewController.exitFullscreen(animated: true) } } - + public func startPictureInPicture() throws { guard let playerViewController else { throw VideoViewError.viewIsDeallocated.error() } - + guard AVPictureInPictureController.isPictureInPictureSupported() else { throw VideoViewError.pictureInPictureNotSupported.error() } - + DispatchQueue.main.async { // Here we skip error handling for simplicity // We do check for PiP support earlier in the code try? playerViewController.startPictureInPicture() } } - + public func stopPictureInPicture() throws { guard let playerViewController else { throw VideoViewError.viewIsDeallocated.error() } - + DispatchQueue.main.async { // Here we skip error handling for simplicity // We do check for PiP support earlier in the code