diff --git a/ios/Runner/Views/VideoPlayerView.swift b/ios/Runner/Views/VideoPlayerView.swift deleted file mode 100644 index ee943fa..0000000 --- a/ios/Runner/Views/VideoPlayerView.swift +++ /dev/null @@ -1,200 +0,0 @@ -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() - } -}