fix(ios): don't recreate AVPlayerViewController if player haven't changed (#4758)

This commit is contained in:
Krzysztof Moch
2025-11-03 14:55:03 +01:00
committed by GitHub
parent 0a6c35784d
commit 649bc1c88e
@@ -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<NSNumber, VideoComponentView> = .strongToWeakObjects()
@objc public static var globalViewsMap: NSMapTable<NSNumber, VideoComponentView> =
.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