diff --git a/CHANGELOG.md b/CHANGELOG.md index d9026c6f52..db6666aff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Fix iOS RCTSwiftLog naming collision [#2868](https://github.com/react-native-video/react-native-video/issues/2868) - Added "homepage" to package.json [#2882](https://github.com/react-native-video/react-native-video/pull/2882) - Fix: memory leak issue on iOS [#2907](https://github.com/react-native-video/react-native-video/pull/2907) +- Fix: improve looping on iOS [#2922](https://github.com/react-native-video/react-native-video/pull/2922) ### Version 6.0.0-alpha.3 diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift index 68fc56ddf3..f3a41f926e 100644 --- a/ios/Video/Features/RCTPlayerObserver.swift +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -7,6 +7,7 @@ protocol RCTPlayerObserverHandlerObjc { func handleDidFailToFinishPlaying(notification:NSNotification!) func handlePlaybackStalled(notification:NSNotification!) func handlePlayerItemDidReachEnd(notification:NSNotification!) + func handleLooperItemDidReachEnd(notification:NSNotification!) // unused // func handleAVPlayerAccess(notification:NSNotification!) } @@ -14,6 +15,8 @@ protocol RCTPlayerObserverHandlerObjc { protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc { func handleTimeUpdate(time:CMTime) func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) + @available(iOS 10.0, *) + func handleLoopStatusChange(changeObject: Any, change:NSKeyValueObservedChange) func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) @@ -69,6 +72,17 @@ class RCTPlayerObserver: NSObject { } } } + var playerLooper:NSObject? { + willSet { + removePlayerLooperObserver() + } + didSet { + if playerLooper != nil { + addPlayerLooperObserver() + } + } + } + private var _progressUpdateInterval:TimeInterval = 250 private var _timeObserver:Any? @@ -76,6 +90,7 @@ class RCTPlayerObserver: NSObject { private var _playerRateChangeObserver:NSKeyValueObservation? private var _playerExpernalPlaybackActiveObserver:NSKeyValueObservation? private var _playerItemStatusObserver:NSKeyValueObservation? + private var _playerLooperStatusObserver:NSKeyValueObservation? private var _playerPlaybackBufferEmptyObserver:NSKeyValueObservation? private var _playerPlaybackLikelyToKeepUpObserver:NSKeyValueObservation? private var _playerTimedMetadataObserver:NSKeyValueObservation? @@ -137,6 +152,40 @@ class RCTPlayerObserver: NSObject { func removePlayerLayerObserver() { _playerLayerReadyForDisplayObserver?.invalidate() } + + func addPlayerLooperItemsObserver() { + if #available(iOS 10.0, *) { + let looper = playerLooper as! AVPlayerLooper + for item in looper.loopingPlayerItems { + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handleLooperItemDidReachEnd(notification:)), + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:item) + } + } + } + + func addPlayerLooperObserver() { + if #available(iOS 10.0, *) { + let looper = playerLooper as! AVPlayerLooper + _playerLooperStatusObserver = looper.observe(\.status, options: [.new], changeHandler: _handlers.handleLoopStatusChange) + } + } + + func removePlayerLooperObserver() { + if #available(iOS 10.0, *) { + _playerLooperStatusObserver?.invalidate() + if playerLooper != nil { + let looper = playerLooper as! AVPlayerLooper + for item in looper.loopingPlayerItems { + NotificationCenter.default.removeObserver(_handlers, + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:item) + } + } + + } + } func addPlayerTimeObserver() { removePlayerTimeObserver() @@ -202,6 +251,7 @@ class RCTPlayerObserver: NSObject { func clearPlayer() { player = nil playerItem = nil + playerLooper = nil NotificationCenter.default.removeObserver(_handlers) } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 2b4a20dea7..3ad5160fef 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -7,6 +7,7 @@ import Promises class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { private var _player:AVPlayer? + private var _playerLooper:NSObject? // Since AVPlayerLooper is only available from 10.0 private var _playerItem:AVPlayerItem? private var _source:VideoSource? private var _playerBufferEmpty:Bool = true @@ -215,6 +216,42 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } // MARK: - Player and source + + @objc + func setUpPlayer(_ playerItem:AVPlayerItem!) { + _playerObserver.player = nil + _playerObserver.playerItem = nil + + if #available(iOS 10.0, *) { + self._player = AVQueuePlayer(playerItem: playerItem) + self._playerItem = playerItem + self.setUpLooper(playerItem) + self._playerObserver.playerItem = self._playerItem + self._playerObserver.player = self._player + } else { + self._playerItem = playerItem + self._playerObserver.playerItem = self._playerItem + self._player = self._player ?? AVPlayer() + self._playerObserver.player = self._player + DispatchQueue.global(qos: .default).async { + self._player?.replaceCurrentItem(with: playerItem) + } + } + + self._player?.actionAtItemEnd = .none + } + + @objc + func setUpLooper(_ playerItem:AVPlayerItem!) { + _playerObserver.playerLooper = nil + + if #available(iOS 10.0, *) { + if self._player != nil && playerItem != nil { + self._playerLooper = AVPlayerLooper(player: self._player as! AVQueuePlayer, templateItem: playerItem!) + self._playerObserver.playerLooper = self._playerLooper + } + } + } @objc func setSrc(_ source:NSDictionary!) { @@ -228,6 +265,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH removePlayerLayer() _playerObserver.player = nil _playerObserver.playerItem = nil + _playerObserver.playerLooper = nil // perform on next run loop, otherwise other passed react-props may not be set RCTVideoUtils.delay() @@ -271,22 +309,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH }.then{[weak self] (playerItem:AVPlayerItem!) in guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} - self._player?.pause() - self._playerItem = playerItem - self._playerObserver.playerItem = self._playerItem self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) self.setFilter(self._filterName) if let maxBitRate = self._maxBitRate { self._playerItem?.preferredPeakBitRate = Double(maxBitRate) } - self._player = self._player ?? AVPlayer() - DispatchQueue.global(qos: .default).async { - self._player?.replaceCurrentItem(with: playerItem) - } - self._playerObserver.player = self._player + self.setUpPlayer(playerItem) self.applyModifiers() - self._player?.actionAtItemEnd = .none if #available(iOS 10.0, *) { self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) @@ -537,6 +567,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setRepeat(_ `repeat`: Bool) { _repeat = `repeat` + + if _playerLooper != nil { + if _repeat { + if _player?.actionAtItemEnd == .pause { + _player?.actionAtItemEnd = .advance + } + } else { + _player?.actionAtItemEnd = .pause + } + } } @@ -831,6 +871,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH override func removeFromSuperview() { _player?.pause() _player = nil + _playerLooper = nil _playerObserver.clearPlayer() self.removePlayerLayer() @@ -881,6 +922,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "target": reactTag ]) } + + @available(iOS 10.0, *) + func handleLoopStatusChange(changeObject: Any, change:NSKeyValueObservedChange) { + let looper = _playerLooper as! AVPlayerLooper + if looper.status == .ready { + _playerObserver.addPlayerLooperItemsObserver() + } + } // When timeMetadata is read the event onTimedMetadata is triggered func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) { @@ -1055,17 +1104,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH onPlaybackStalled?(["target": reactTag as Any]) _playbackStalled = true } + + @objc func handleLooperItemDidReachEnd(notification:NSNotification!) { + onVideoEnd?(["target": reactTag as Any]) + } @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { onVideoEnd?(["target": reactTag as Any]) - - if _repeat { + if _repeat && self._playerLooper == nil { let item:AVPlayerItem! = notification.object as? AVPlayerItem item.seek(to: CMTime.zero) self.applyModifiers() - } else { - self.setPaused(true); - _playerObserver.removePlayerTimeObserver() } }