diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index a26e65f9..9f8e9445 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 { @@ -779,7 +799,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/PlayerModel.swift b/Model/Player/PlayerModel.swift index 4474aba7..610873d0 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -881,26 +881,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 @@ -924,22 +927,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) @@ -1038,18 +1041,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, @@ -1057,7 +1064,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 { @@ -1261,7 +1268,7 @@ final class PlayerModel: ObservableObject { } private func destroyKeyPressMonitor() { - if let keyPressMonitor = keyPressMonitor { + if let keyPressMonitor { NSEvent.removeMonitor(keyPressMonitor) } } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index c3247e72..a883355b 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,22 @@ 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) + } catch { + logger.error("Failed to set audio session category: \(error)") + } + + // Begin receiving remote control events + UIApplication.shared.beginReceivingRemoteControlEvents() #endif + return true }