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:
@@ -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: ()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user