🐛 Fix bugs and don't know what has been fixed
This commit is contained in:
		
							
								
								
									
										200
									
								
								ios/Runner/Views/VideoPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								ios/Runner/Views/VideoPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | ||||
| import Flutter | ||||
| import UIKit | ||||
| import AVKit | ||||
|  | ||||
| // Factory to create the native video player view | ||||
| class VideoPlayerViewFactory: NSObject, FlutterPlatformViewFactory { | ||||
|     private var messenger: FlutterBinaryMessenger | ||||
|  | ||||
|     init(messenger: FlutterBinaryMessenger) { | ||||
|         self.messenger = messenger | ||||
|         super.init() | ||||
|     } | ||||
|  | ||||
|     func create( | ||||
|         withFrame frame: CGRect, | ||||
|         viewIdentifier viewId: Int64, | ||||
|         arguments args: Any? | ||||
|     ) -> FlutterPlatformView { | ||||
|         return VideoPlayerView( | ||||
|             frame: frame, | ||||
|             viewIdentifier: viewId, | ||||
|             arguments: args, | ||||
|             binaryMessenger: messenger) | ||||
|     } | ||||
|  | ||||
|     // This is required by the protocol, but we don't need to implement it for this case | ||||
|     public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { | ||||
|         return FlutterStandardMessageCodec.sharedInstance() | ||||
|     } | ||||
| } | ||||
|  | ||||
| // The actual PlatformView that holds the AVPlayerViewController | ||||
| class VideoPlayerView: NSObject, FlutterPlatformView { | ||||
|     private var playerViewController: AVPlayerViewController? | ||||
|     private var player: AVPlayer? | ||||
|     private var activityIndicator: UIActivityIndicatorView! | ||||
|     private var progressLabel: UILabel! | ||||
|     private var progressStack: UIStackView! | ||||
|  | ||||
|     // KVO contexts | ||||
|     private var playerStatusContext = 0 | ||||
|     private var playerLoadedTimeRangesContext = 0 | ||||
|  | ||||
|     init( | ||||
|         frame: CGRect, | ||||
|         viewIdentifier viewId: Int64, | ||||
|         arguments args: Any?, | ||||
|         binaryMessenger messenger: FlutterBinaryMessenger? | ||||
|     ) { | ||||
|         super.init() | ||||
|  | ||||
|         // Ensure we have a valid URL from Flutter | ||||
|         guard let args = args as? [String: Any], | ||||
|               let videoUrlString = args["videoUrl"] as? String, | ||||
|               let videoUrl = URL(string: videoUrlString) else { | ||||
|             // Initialize playerViewController even if URL is invalid | ||||
|             playerViewController = AVPlayerViewController() | ||||
|             playerViewController!.showsPlaybackControls = false // Hide controls for invalid URL | ||||
|  | ||||
|             let label = UILabel() | ||||
|             label.text = "Invalid video URL" | ||||
|             label.textAlignment = .center | ||||
|             label.textColor = .white | ||||
|             label.translatesAutoresizingMaskIntoConstraints = false | ||||
|             playerViewController!.contentOverlayView?.addSubview(label) | ||||
|              | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 label.centerXAnchor.constraint(equalTo: playerViewController!.contentOverlayView!.centerXAnchor), | ||||
|                 label.centerYAnchor.constraint(equalTo: playerViewController!.contentOverlayView!.centerYAnchor) | ||||
|             ]) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         // --- Player --- | ||||
|         player = AVPlayer(url: videoUrl) | ||||
|  | ||||
|         // --- PlayerViewController --- | ||||
|         playerViewController = AVPlayerViewController() | ||||
|         playerViewController!.player = player | ||||
|         playerViewController!.view.frame = frame // Set the frame directly | ||||
|         playerViewController!.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] | ||||
|         playerViewController!.showsPlaybackControls = true | ||||
|          | ||||
|         // --- Loading Indicator (spinner) --- | ||||
|         activityIndicator = UIActivityIndicatorView(style: .large) | ||||
|         activityIndicator.color = .white | ||||
|         activityIndicator.translatesAutoresizingMaskIntoConstraints = false | ||||
|         activityIndicator.isUserInteractionEnabled = false // Allow touches to pass through | ||||
|  | ||||
|         // --- Progress Percentage Label --- | ||||
|         progressLabel = UILabel() | ||||
|         progressLabel.textColor = .white | ||||
|         progressLabel.textAlignment = .center | ||||
|         progressLabel.translatesAutoresizingMaskIntoConstraints = false | ||||
|         progressLabel.isUserInteractionEnabled = false // Allow touches to pass through | ||||
|  | ||||
|         playerViewController!.contentOverlayView?.addSubview(activityIndicator) | ||||
|         playerViewController!.contentOverlayView?.addSubview(progressLabel) | ||||
|          | ||||
|         // Center the activity indicator and place the progress label below it | ||||
|         NSLayoutConstraint.activate([ | ||||
|             activityIndicator.centerXAnchor.constraint(equalTo: playerViewController!.contentOverlayView!.centerXAnchor), | ||||
|             activityIndicator.centerYAnchor.constraint(equalTo: playerViewController!.contentOverlayView!.centerYAnchor), | ||||
|  | ||||
|             progressLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 16), | ||||
|             progressLabel.centerXAnchor.constraint(equalTo: playerViewController!.contentOverlayView!.centerXAnchor), | ||||
|         ]) | ||||
|  | ||||
|         // Add Key-Value Observers | ||||
|         addObservers() | ||||
|  | ||||
|         activityIndicator.startAnimating() | ||||
|     } | ||||
|  | ||||
|     func view() -> UIView { | ||||
|         return playerViewController!.view | ||||
|     } | ||||
|  | ||||
|     private func addObservers() { | ||||
|         player?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.new, .initial], context: &playerStatusContext) | ||||
|         player?.currentItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), options: [.new], context: &playerLoadedTimeRangesContext) | ||||
|     } | ||||
|  | ||||
|     private func removeObservers() { | ||||
|         // Check if observers are registered before removing them to avoid crashes. | ||||
|         // A simple way is to use a flag or check the player object, but for KVO, | ||||
|         // it's often safer to just ensure they are added once and removed once. | ||||
|         // Given the lifecycle here, direct removal in deinit is okay. | ||||
|         player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status), context: &playerStatusContext) | ||||
|         player?.currentItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), context: &playerLoadedTimeRangesContext) | ||||
|     } | ||||
|  | ||||
|     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | ||||
|         // Dispatch to main queue to ensure thread safety | ||||
|         DispatchQueue.main.async { | ||||
|             guard self.player != nil else { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             if context == &self.playerStatusContext { | ||||
|                 self.handlePlayerStatusChange(change: change) | ||||
|             } else if context == &self.playerLoadedTimeRangesContext { | ||||
|                 self.handleLoadedTimeRangesChange() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func handlePlayerStatusChange(change: [NSKeyValueChangeKey : Any]?) { | ||||
|         guard let statusValue = change?[.newKey] as? Int, let status = AVPlayer.Status(rawValue: statusValue) else { return } | ||||
|          | ||||
|         DispatchQueue.main.async { | ||||
|             switch status { | ||||
|             case .readyToPlay: | ||||
|                 self.activityIndicator.stopAnimating() | ||||
|                 self.progressLabel.isHidden = true | ||||
|                 self.player?.play() | ||||
|             case .failed: | ||||
|                 self.activityIndicator.stopAnimating() | ||||
|                 // Optionally: show an error message to the user | ||||
|                 let label = UILabel() | ||||
|                 label.text = "Failed to load video" | ||||
|                 label.textColor = .white | ||||
|                 label.textAlignment = .center | ||||
|                 label.frame = self.playerViewController!.view.bounds | ||||
|                 self.playerViewController!.view.addSubview(label) | ||||
|             case .unknown: | ||||
|                 self.activityIndicator.startAnimating() | ||||
|             @unknown default: | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func handleLoadedTimeRangesChange() { | ||||
|         guard let playerItem = player?.currentItem, | ||||
|               let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue, | ||||
|               !CMTIME_IS_INDEFINITE(playerItem.duration) else { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let startSeconds = CMTimeGetSeconds(timeRange.start) | ||||
|         let durationSeconds = CMTimeGetSeconds(timeRange.duration) | ||||
|         let totalBuffer = startSeconds + durationSeconds | ||||
|         let totalDuration = CMTimeGetSeconds(playerItem.duration) | ||||
|          | ||||
|         let progress = Float(totalBuffer / totalDuration) | ||||
|          | ||||
|         DispatchQueue.main.async { | ||||
|             self.progressLabel.text = "\(Int(progress * 100))%" | ||||
|  | ||||
|             if progress >= 0.99 { | ||||
|                 self.progressLabel.isHidden = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     deinit { | ||||
|         removeObservers() | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user