diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 11af4a629c..ff7a3619d4 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -18,6 +18,8 @@ body: - type: dropdown id: platforms + validations: + required: true attributes: label: What platforms are you having the problem on? multiple: true @@ -28,6 +30,25 @@ body: - visionOS - Android TV - Apple tvOS + + - type: input + id: system_version + attributes: + label: System Version + description: What version of the system is using device that you are experiencing the issue? + validations: + required: true + + - type: dropdown + id: device + validations: + required: true + attributes: + label: On what device are you experiencing the issue? + multiple: true + options: + - Real device + - Simulator - type: dropdown id: architecture diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 34610acc56..6becaab9b9 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -256,29 +256,37 @@ public class ReactExoplayerView extends FrameLayout implements private long lastBufferDuration = -1; private long lastDuration = -1; + private boolean viewHasDropped = false; + private void updateProgress() { + if (player != null) { + if (playerControlView != null && isPlayingAd() && controls) { + playerControlView.hide(); + } + long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100; + long duration = player.getDuration(); + long pos = player.getCurrentPosition(); + if (pos > duration) { + pos = duration; + } + + if (lastPos != pos + || lastBufferDuration != bufferedDuration + || lastDuration != duration) { + lastPos = pos; + lastBufferDuration = bufferedDuration; + lastDuration = duration; + eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos)); + } + } + } + private final Handler progressHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { if (msg.what == SHOW_PROGRESS) { - if (player != null) { - if (playerControlView != null && isPlayingAd() && controls) { - playerControlView.hide(); - } - long pos = player.getCurrentPosition(); - long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100; - long duration = player.getDuration(); - - if (lastPos != pos - || lastBufferDuration != bufferedDuration - || lastDuration != duration) { - lastPos = pos; - lastBufferDuration = bufferedDuration; - lastDuration = duration; - eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos)); - } - msg = obtainMessage(SHOW_PROGRESS); - sendMessageDelayed(msg, Math.round(mProgressUpdateInterval)); - } + updateProgress(); + msg = obtainMessage(SHOW_PROGRESS); + sendMessageDelayed(msg, Math.round(mProgressUpdateInterval)); } } }; @@ -363,6 +371,8 @@ protected void onDetachedFromWindow() { public void cleanUpResources() { stopPlayback(); themedReactContext.removeLifecycleEventListener(this); + releasePlayer(); + viewHasDropped = true; } //BandwidthMeter.EventListener implementation @@ -635,6 +645,9 @@ private void initializePlayer() { Activity activity = themedReactContext.getCurrentActivity(); // This ensures all props have been settled, to avoid async racing conditions. mainRunnable = () -> { + if (viewHasDropped) { + return; + } try { if (player == null) { exoPlayerView.updateSurfaceView(source.getViewType()); @@ -646,6 +659,10 @@ private void initializePlayer() { // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { + // DRM initialization must run on a different thread + if (viewHasDropped) { + return; + } if (activity == null) { DebugLog.e(TAG, "Failed to initialize Player!, null activity"); eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); @@ -654,12 +671,15 @@ private void initializePlayer() { // Initialize handler to run on the main thread activity.runOnUiThread(() -> { + if (viewHasDropped) { + return; + } try { // Source initialization must run on the main thread initializePlayerSource(); } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 1"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); self.eventEmitter.error(ex.toString(), ex, "1001"); @@ -671,7 +691,7 @@ private void initializePlayer() { } } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 2"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); eventEmitter.error(ex.toString(), ex, "1001"); @@ -1352,6 +1372,7 @@ public void onEvents(@NonNull Player player, Player.Events events) { break; case Player.STATE_ENDED: text += "ended"; + updateProgress(); eventEmitter.end(); onStopPlayback(); setKeepScreenOn(false); @@ -1629,6 +1650,7 @@ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @N // so we need to explicitly detect it. if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION && player.getRepeatMode() == Player.REPEAT_MODE_ONE) { + updateProgress(); eventEmitter.end(); } } diff --git a/ios/Video/NowPlayingInfoCenterManager.swift b/ios/Video/NowPlayingInfoCenterManager.swift index 2307e10b02..6c416c72a1 100644 --- a/ios/Video/NowPlayingInfoCenterManager.swift +++ b/ios/Video/NowPlayingInfoCenterManager.swift @@ -61,11 +61,11 @@ class NowPlayingInfoCenterManager { return } - if let observer = observers[players.hashValue] { + if let observer = observers[player.hashValue] { observer.invalidate() } - observers.removeValue(forKey: players.hashValue) + observers.removeValue(forKey: player.hashValue) players.remove(player) if currentPlayer == player { diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 96e0b95ad2..9ec2340990 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -308,7 +308,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Progress - func sendProgressUpdate() { + func sendProgressUpdate(didEnd: Bool = false) { #if !USE_GOOGLE_IMA // If we dont use Ads and onVideoProgress is not defined we dont need to run this code guard onVideoProgress != nil else { return } @@ -330,11 +330,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } let currentPlaybackTime = _player?.currentItem?.currentDate() let duration = CMTimeGetSeconds(playerDuration) - let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) + var currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) - NotificationCenter.default.post(name: NSNotification.Name("RCTVideo_progress"), object: nil, userInfo: [ - "progress": NSNumber(value: currentTimeSecs / duration), - ]) + if currentTimeSecs > duration || didEnd { + currentTimeSecs = duration + } if currentTimeSecs >= 0 { #if USE_GOOGLE_IMA @@ -344,10 +344,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } #endif onVideoProgress?([ - "currentTime": NSNumber(value: Float(currentTimeSecs)), + "currentTime": currentTimeSecs, "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source), - "atValue": NSNumber(value: currentTime?.value ?? .zero), - "currentPlaybackTime": NSNumber(value: NSNumber(value: Double(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), + "atValue": currentTime?.value ?? .zero, + "currentPlaybackTime": NSNumber(value: Double(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value, "target": reactTag, "seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player), ]) @@ -1232,10 +1232,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Lifecycle override func removeFromSuperview() { + self._player?.replaceCurrentItem(with: nil) if let player = _player { player.pause() NowPlayingInfoCenterManager.shared.removePlayer(player: player) } + _playerItem = nil + _source = nil + _chapters = nil + _drm = nil + _textTracks = nil + _selectedTextTrackCriteria = nil + _selectedAudioTrackCriteria = nil + _presentingViewController = nil _player = nil _resouceLoaderDelegate = nil @@ -1243,6 +1252,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH #if USE_GOOGLE_IMA _imaAdsManager.releaseAds() + _imaAdsManager = nil #endif self.removePlayerLayer() @@ -1561,6 +1571,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func handlePlayerItemDidReachEnd(notification: NSNotification!) { + sendProgressUpdate(didEnd: true) onVideoEnd?(["target": reactTag as Any]) #if USE_GOOGLE_IMA if notification.object as? AVPlayerItem == _player?.currentItem {