Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: repeat video on iOS with AVPlayerLooper #2922

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 50 additions & 0 deletions ios/Video/Features/RCTPlayerObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ protocol RCTPlayerObserverHandlerObjc {
func handleDidFailToFinishPlaying(notification:NSNotification!)
func handlePlaybackStalled(notification:NSNotification!)
func handlePlayerItemDidReachEnd(notification:NSNotification!)
func handleLooperItemDidReachEnd(notification:NSNotification!)
// unused
// func handleAVPlayerAccess(notification:NSNotification!)
}

protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
func handleTimeUpdate(time:CMTime)
func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange<Bool>)
@available(iOS 10.0, *)
func handleLoopStatusChange(changeObject: Any, change:NSKeyValueObservedChange<AVPlayerLooper.Status>)
func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>)
func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<AVPlayerItem.Status>)
func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<Bool>)
Expand Down Expand Up @@ -69,13 +72,25 @@ class RCTPlayerObserver: NSObject {
}
}
}
var playerLooper:NSObject? {
willSet {
removePlayerLooperObserver()
}
didSet {
if playerLooper != nil {
addPlayerLooperObserver()
}
}
}


private var _progressUpdateInterval:TimeInterval = 250
private var _timeObserver:Any?

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?
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -202,6 +251,7 @@ class RCTPlayerObserver: NSObject {
func clearPlayer() {
player = nil
playerItem = nil
playerLooper = nil
NotificationCenter.default.removeObserver(_handlers)
}
}
77 changes: 63 additions & 14 deletions ios/Video/RCTVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Contributor

@tironiigor tironiigor Mar 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swemail @freeboub
Don't we need the same check here to keep the old player if it's available like in line 234?

Suggested change
self._player = AVQueuePlayer(playerItem: playerItem)
self._player = 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!) {
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}
}


Expand Down Expand Up @@ -831,6 +871,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
override func removeFromSuperview() {
_player?.pause()
_player = nil
_playerLooper = nil
_playerObserver.clearPlayer()

self.removePlayerLayer()
Expand Down Expand Up @@ -881,6 +922,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
"target": reactTag
])
}

@available(iOS 10.0, *)
func handleLoopStatusChange(changeObject: Any, change:NSKeyValueObservedChange<AVPlayerLooper.Status>) {
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]?>) {
Expand Down Expand Up @@ -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()
}
}

Expand Down