damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 5ea522d306100d05ad9c795fc9fd231803f7c2ae
parent 54d6161acd9ef754fccaa179beb1881baee6b625
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Fri, 18 Apr 2025 20:09:22 -0700

Reinitialize videos if they enter an error state

This is a palliative fix for an issue where videos become unplayable
after a long user session.

The fix works by detecting the error state anytime the video gets
played, and reinitializes the video and corresponding player views in
order to clear the error.

Changelog-Fixed: Fixed issue where some videos would become unplayable after some time using the app
Closes: https://github.com/damus-io/damus/issues/2878
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>

Diffstat:
Mdamus/Views/Video/DamusVideoPlayer.swift | 64++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 46 insertions(+), 18 deletions(-)

diff --git a/damus/Views/Video/DamusVideoPlayer.swift b/damus/Views/Video/DamusVideoPlayer.swift @@ -25,10 +25,14 @@ import SwiftUI /// The URL of the video let url: URL + + + // MARK: Internal state + /// The underlying AVPlayer that we are wrapping. /// This is not public because we don't want any callers of this class controlling the `AVPlayer` directly, we want them to go through our interface /// This measure helps avoid state inconsistencies and other flakiness. DO NOT USE THIS OUTSIDE `DamusVideoPlayer` - private let player: AVPlayer + private var player: AVPlayer // MARK: SwiftUI-friendly interface @@ -100,16 +104,39 @@ import SwiftUI private var videoIsPlayingObserver: NSKeyValueObservation? - // MARK: - Initialization + // MARK: - Initialization, deinitialization and reinitialization public init(url: URL) { self.url = url self.player = AVPlayer(playerItem: AVPlayerItem(url: url)) self.video_size = nil + Task { await self.load() } + } + + func reinitializePlayer() { + Log.info("DamusVideoPlayer: Reinitializing internal player…", for: .video_coordination) + + // Tear down + videoSizeObserver?.invalidate() + videoDurationObserver?.invalidate() + videoIsPlayingObserver?.invalidate() + + // Reset player + self.player = AVPlayer(playerItem: AVPlayerItem(url: url)) + + // Load once again Task { await load() } + } + + /// Internally loads this class + private func load() async { + Task { + has_audio = await self.video_has_audio() + is_loading = false + } player.isMuted = is_muted @@ -126,6 +153,13 @@ import SwiftUI observeVideoIsPlaying() } + deinit { + // These cannot be moved into their own functions due to contraints on structured concurrency + videoSizeObserver?.invalidate() + videoDurationObserver?.invalidate() + videoIsPlayingObserver?.invalidate() + } + // MARK: - Observers // Functions that allow us to observe certain variables and publish their changes for view updates // These are all private because they are part of the internal logic @@ -175,11 +209,6 @@ import SwiftUI // MARK: - Other internal logic functions - private func load() async { - has_audio = await self.video_has_audio() - is_loading = false - } - private func video_has_audio() async -> Bool { do { let hasAudibleTracks = ((try await player.currentItem?.asset.loadMediaSelectionGroup(for: .audible)) != nil) @@ -196,17 +225,16 @@ import SwiftUI player.play() } - // MARK: - Deinit - - deinit { - videoSizeObserver?.invalidate() - videoDurationObserver?.invalidate() - videoIsPlayingObserver?.invalidate() - } - // MARK: - Convenience interface functions func play() { + switch self.player.status { + case .failed: + Log.error("DamusVideoPlayer: Failed to play video. Error: '%s'", for: .video_coordination, self.player.error?.localizedDescription ?? "no error") + self.reinitializePlayer() + default: + break + } self.is_playing = true } @@ -236,9 +264,9 @@ extension DamusVideoPlayer { } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { - if uiViewController.player == nil { - uiViewController.player = player.player - } + /// - If `player.player` is changed (e.g. `DamusVideoPlayer` gets reinitialized), this will refresh the video player to the new working one. + /// - If `player.player` is unchanged, this is basically a very low cost no-op (Because `AVPlayer` is a class type, this assignment only copies a pointer, not a large structure) + uiViewController.player = player.player } static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: ()) {