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>
This commit is contained in:
Daniel D’Aquino
2025-04-18 20:09:22 -07:00
parent 54d6161acd
commit 5ea522d306

View File

@@ -25,10 +25,14 @@ import SwiftUI
/// The URL of the video /// The URL of the video
let url: URL let url: URL
// MARK: Internal state
/// The underlying AVPlayer that we are wrapping. /// 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 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` /// 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 // MARK: SwiftUI-friendly interface
@@ -100,16 +104,39 @@ import SwiftUI
private var videoIsPlayingObserver: NSKeyValueObservation? private var videoIsPlayingObserver: NSKeyValueObservation?
// MARK: - Initialization // MARK: - Initialization, deinitialization and reinitialization
public init(url: URL) { public init(url: URL) {
self.url = url self.url = url
self.player = AVPlayer(playerItem: AVPlayerItem(url: url)) self.player = AVPlayer(playerItem: AVPlayerItem(url: url))
self.video_size = nil 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 { Task {
await load() 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 player.isMuted = is_muted
@@ -126,6 +153,13 @@ import SwiftUI
observeVideoIsPlaying() observeVideoIsPlaying()
} }
deinit {
// These cannot be moved into their own functions due to contraints on structured concurrency
videoSizeObserver?.invalidate()
videoDurationObserver?.invalidate()
videoIsPlayingObserver?.invalidate()
}
// MARK: - Observers // MARK: - Observers
// Functions that allow us to observe certain variables and publish their changes for view updates // 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 // These are all private because they are part of the internal logic
@@ -175,11 +209,6 @@ import SwiftUI
// MARK: - Other internal logic functions // 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 { private func video_has_audio() async -> Bool {
do { do {
let hasAudibleTracks = ((try await player.currentItem?.asset.loadMediaSelectionGroup(for: .audible)) != nil) let hasAudibleTracks = ((try await player.currentItem?.asset.loadMediaSelectionGroup(for: .audible)) != nil)
@@ -196,17 +225,16 @@ import SwiftUI
player.play() player.play()
} }
// MARK: - Deinit
deinit {
videoSizeObserver?.invalidate()
videoDurationObserver?.invalidate()
videoIsPlayingObserver?.invalidate()
}
// MARK: - Convenience interface functions // MARK: - Convenience interface functions
func play() { 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 self.is_playing = true
} }
@@ -236,9 +264,9 @@ extension DamusVideoPlayer {
} }
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if uiViewController.player == nil { /// - If `player.player` is changed (e.g. `DamusVideoPlayer` gets reinitialized), this will refresh the video player to the new working one.
uiViewController.player = player.player /// - 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: ()) { static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: ()) {