mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-25 15:51:33 +00:00
fix(ios): don't recreate AVPlayerViewController if player haven't changed (#4758)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user