From 1a22ac71be6f499bfe2b04a43967cd828f28fa8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Thu, 29 Aug 2024 15:09:16 +0200 Subject: [PATCH 1/3] move AVAudioSession configuration to AppDelegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Toni Förster --- Model/Player/Backends/AVPlayerBackend.swift | 29 ++++++++++++++++----- Model/Player/Backends/MPVBackend.swift | 15 ----------- iOS/AppDelegate.swift | 19 ++++++++++++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 89ccce7d..f1335947 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend { private var frequentTimeObserver: Any? private var infrequentTimeObserver: Any? - private var playerTimeControlStatusObserver: Any? + private var playerTimeControlStatusObserver: NSKeyValueObservation? private var statusObservation: NSKeyValueObservation? @@ -119,6 +119,26 @@ final class AVPlayerBackend: PlayerBackend { #if os(iOS) controller.player = avPlayer #endif + logger.info("AVPlayerBackend initialized.") + } + + deinit { + // Invalidate any observers to avoid memory leaks + statusObservation?.invalidate() + playerTimeControlStatusObserver?.invalidate() + + // Remove any time observers added to AVPlayer + if let frequentObserver = frequentTimeObserver { + avPlayer.removeTimeObserver(frequentObserver) + } + if let infrequentObserver = infrequentTimeObserver { + avPlayer.removeTimeObserver(infrequentObserver) + } + + // Remove notification observers + removeItemDidPlayToEndTimeObserver() + + logger.info("AVPlayerBackend deinitialized.") } func canPlay(_ stream: Stream) -> Bool { @@ -342,11 +362,8 @@ final class AVPlayerBackend: PlayerBackend { self.asset = nil } - let startPlaying = { - #if !os(macOS) - try? AVAudioSession.sharedInstance().setActive(true) - #endif + let startPlaying = { self.setRate(self.model.currentRate) guard let item = self.model.playerItem, self.isAutoplaying(item) else { return } @@ -779,7 +796,7 @@ final class AVPlayerBackend: PlayerBackend { opened = true controller.startPictureInPicture() } else { - print("PiP not possible, waited \(delay) seconds") + self.logger.info("PiP not possible, waited \(delay) seconds") } } } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index daf2c51f..981d416e 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -245,21 +245,6 @@ final class MPVBackend: PlayerBackend { } let startPlaying = { - #if !os(macOS) - do { - try AVAudioSession.sharedInstance().setActive(true) - - NotificationCenter.default.addObserver( - self, - selector: #selector(self.handleAudioSessionInterruption(_:)), - name: AVAudioSession.interruptionNotification, - object: nil - ) - } catch { - self.logger.error("Error setting up audio session: \(error)") - } - #endif - DispatchQueue.main.async { [weak self] in guard let self else { return diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index c3247e72..df3bddf0 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -1,9 +1,12 @@ +import AVFoundation import Foundation +import Logging import UIKit final class AppDelegate: UIResponder, UIApplicationDelegate { var orientationLock = UIInterfaceOrientationMask.all + private var logger = Logger(label: "stream.yattee.app.delegalate") private(set) static var instance: AppDelegate! func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { @@ -12,11 +15,23 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection Self.instance = self - #if os(iOS) - UIViewController.swizzleHomeIndicatorProperty() + #if !os(macOS) + UIViewController.swizzleHomeIndicatorProperty() OrientationTracker.shared.startDeviceOrientationTracking() + + // Configure the audio session for playback + do { + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + logger.error("Failed to set audio session category: \(error)") + } + + // Begin receiving remote control events + UIApplication.shared.beginReceivingRemoteControlEvents() #endif + return true } From 740a2f85ac7d4447786098bbfbb9f34643b2e77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Thu, 29 Aug 2024 15:10:04 +0200 Subject: [PATCH 2/3] updateNowPlayingInfo also with System controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Toni Förster --- Model/Player/PlayerModel.swift | 57 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 343a012b..7ecf2e96 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -880,26 +880,29 @@ final class PlayerModel: ObservableObject { } func updateRemoteCommandCenter() { - let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand - let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand - let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand - let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand + let commandCenter = MPRemoteCommandCenter.shared() + let skipForwardCommand = commandCenter.skipForwardCommand + let skipBackwardCommand = commandCenter.skipBackwardCommand + let previousTrackCommand = commandCenter.previousTrackCommand + let nextTrackCommand = commandCenter.nextTrackCommand if !remoteCommandCenterConfigured { remoteCommandCenterConfigured = true - #if !os(macOS) - try? AVAudioSession.sharedInstance().setCategory( - .playback, - mode: .moviePlayback - ) - - UIApplication.shared.beginReceivingRemoteControlEvents() - #endif - let interval = TimeInterval(systemControlsSeekDuration) ?? 10 let preferredIntervals = [NSNumber(value: interval)] + // Remove existing targets to avoid duplicates + skipForwardCommand.removeTarget(nil) + skipBackwardCommand.removeTarget(nil) + previousTrackCommand.removeTarget(nil) + nextTrackCommand.removeTarget(nil) + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.removeTarget(nil) + commandCenter.togglePlayPauseCommand.removeTarget(nil) + commandCenter.changePlaybackPositionCommand.removeTarget(nil) + + // Re-add targets for handling commands skipForwardCommand.preferredIntervals = preferredIntervals skipBackwardCommand.preferredIntervals = preferredIntervals @@ -923,22 +926,22 @@ final class PlayerModel: ObservableObject { return .success } - MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in + commandCenter.playCommand.addTarget { [weak self] _ in self?.play() return .success } - MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in + commandCenter.pauseCommand.addTarget { [weak self] _ in self?.pause() return .success } - MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in + commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in self?.togglePlay() return .success } - MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } self?.backend.seek(to: event.positionTime, seekType: .userInteracted) @@ -1017,18 +1020,22 @@ final class PlayerModel: ObservableObject { guard activeBackend == .mpv else { return } #endif - #if os(iOS) - if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls { - return - } - #endif - guard let video = currentItem?.video else { MPNowPlayingInfoCenter.default().nowPlayingInfo = .none return } let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0 + + // Determine the media type based on musicMode + let mediaType: NSNumber + if musicMode { + mediaType = MPMediaType.anyAudio.rawValue as NSNumber + } else { + mediaType = MPMediaType.anyVideo.rawValue as NSNumber + } + + // Prepare the Now Playing info dictionary var nowPlayingInfo: [String: AnyObject] = [ MPMediaItemPropertyTitle: video.displayTitle as AnyObject, MPMediaItemPropertyArtist: video.displayAuthor as AnyObject, @@ -1036,7 +1043,7 @@ final class PlayerModel: ObservableObject { MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject, MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject, MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject, - MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject + MPMediaItemPropertyMediaType: mediaType ] if !currentArtwork.isNil { @@ -1240,7 +1247,7 @@ final class PlayerModel: ObservableObject { } private func destroyKeyPressMonitor() { - if let keyPressMonitor = keyPressMonitor { + if let keyPressMonitor { NSEvent.removeMonitor(keyPressMonitor) } } From 633af0257746ef78629908db4f730a594e7524ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sat, 31 Aug 2024 13:19:54 +0200 Subject: [PATCH 3/3] =?UTF-8?q?don=E2=80=99t=20activate=20AVAudioSession?= =?UTF-8?q?=20on=20app=20start?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Toni Förster --- Model/Player/Backends/AVPlayerBackend.swift | 5 ++++- Model/Player/Backends/MPVBackend.swift | 15 +++++++++++++++ iOS/AppDelegate.swift | 1 - 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index f1335947..d0581f8c 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -362,8 +362,11 @@ final class AVPlayerBackend: PlayerBackend { self.asset = nil } - let startPlaying = { + #if !os(macOS) + try? AVAudioSession.sharedInstance().setActive(true) + #endif + self.setRate(self.model.currentRate) guard let item = self.model.playerItem, self.isAutoplaying(item) else { return } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 981d416e..daf2c51f 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -245,6 +245,21 @@ final class MPVBackend: PlayerBackend { } let startPlaying = { + #if !os(macOS) + do { + try AVAudioSession.sharedInstance().setActive(true) + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleAudioSessionInterruption(_:)), + name: AVAudioSession.interruptionNotification, + object: nil + ) + } catch { + self.logger.error("Error setting up audio session: \(error)") + } + #endif + DispatchQueue.main.async { [weak self] in guard let self else { return diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index df3bddf0..a883355b 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -23,7 +23,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Configure the audio session for playback do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) - try AVAudioSession.sharedInstance().setActive(true) } catch { logger.error("Failed to set audio session category: \(error)") }