Fix gifv playing pausing audio from other apps

Closes #505
This commit is contained in:
Shadowfacts 2024-07-08 21:48:35 -07:00
parent 1f6644b703
commit a818457f8c
5 changed files with 113 additions and 10 deletions

View File

@ -361,6 +361,7 @@
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; }; D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; }; D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; }; D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
D6EEDE932C3CF21800E10E51 /* AudioSessionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */; };
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; }; D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; }; D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; }; D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
@ -803,6 +804,7 @@
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; }; D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; }; D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; }; D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; };
D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionCoordinator.swift; sourceTree = "<group>"; };
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; }; D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; }; D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenSuggestionCollectionViewCell.swift; sourceTree = "<group>"; }; D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenSuggestionCollectionViewCell.swift; sourceTree = "<group>"; };
@ -1607,6 +1609,7 @@
D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */, D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */,
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */, D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */,
D6D79F582A13293200AB2315 /* BackgroundManager.swift */, D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
D69261262BB3BA610023152C /* Box.swift */, D69261262BB3BA610023152C /* Box.swift */,
D61F75B6293C119700C0B37F /* Filterer.swift */, D61F75B6293C119700C0B37F /* Filterer.swift */,
@ -2147,6 +2150,7 @@
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */, D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */, D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
D6EEDE932C3CF21800E10E51 /* AudioSessionCoordinator.swift in Sources */,
D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */, D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */,
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */, D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */, D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,

View File

@ -0,0 +1,87 @@
//
// AudioSessionCoordinator.swift
// Tusker
//
// Created by Shadowfacts on 7/8/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import Foundation
import AVFoundation
final class AudioSessionCoordinator {
static let shared = AudioSessionCoordinator()
private init() {}
private let queue = DispatchQueue(label: "AudioSessionCoordinator", qos: .userInitiated)
private var videoCount = 0
private var gifvCount = 0
func beginPlayback(mode: Mode, completionHandler: (() -> Void)? = nil) -> Token {
let token = Token(mode: mode)
queue.async {
switch mode {
case .video:
self.videoCount += 1
case .gifv:
self.gifvCount += 1
}
self.update(completionHandler: completionHandler)
}
return token
}
func endPlayback(token: Token, completionHandler: (() -> Void)? = nil) {
// the enqueued block can't retain token, since it may be being dealloc'd right now
let mode = token.mode
queue.async {
switch mode {
case .video:
self.videoCount -= 1
case .gifv:
self.gifvCount -= 1
}
self.update(completionHandler: completionHandler)
}
}
private func update(completionHandler: (() -> Void)?) {
let currentCategory = AVAudioSession.sharedInstance().category
if videoCount > 0 {
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
} else if gifvCount > 0 {
// if we're transitioning from video to gifv, fully deactivate first
// in order to let other (music) apps resume, then activate with the
// ambient category to "mix" with others
if currentCategory == .playback {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
// only gifv modes requested, allow mixing with others
try? AVAudioSession.sharedInstance().setCategory(.ambient)
try? AVAudioSession.sharedInstance().setActive(true)
} else {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
completionHandler?()
}
final class Token {
let mode: Mode
init(mode: Mode) {
self.mode = mode
}
deinit {
AudioSessionCoordinator.shared.endPlayback(token: self)
}
}
enum Mode {
case video
case gifv
}
}

View File

@ -29,6 +29,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
private var rateObservation: NSKeyValueObservation? private var rateObservation: NSKeyValueObservation?
private var isFirstAppearance = true private var isFirstAppearance = true
private var hideControlsWorkItem: DispatchWorkItem? private var hideControlsWorkItem: DispatchWorkItem?
private var audioSessionToken: AudioSessionCoordinator.Token?
init(url: URL, caption: String?) { init(url: URL, caption: String?) {
self.url = url self.url = url
@ -172,10 +173,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
let wasFirstAppearance = isFirstAppearance let wasFirstAppearance = isFirstAppearance
isFirstAppearance = false isFirstAppearance = false
DispatchQueue.global(qos: .userInitiated).async { audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
if wasFirstAppearance { if wasFirstAppearance {
DispatchQueue.main.async { DispatchQueue.main.async {
self.player.play() self.player.play()
@ -187,8 +185,8 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
func galleryContentWillDisappear() { func galleryContentWillDisappear() {
player.pause() player.pause()
DispatchQueue.global(qos: .userInitiated).async { if let audioSessionToken {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
} }
} }

View File

@ -17,6 +17,7 @@ class GifvController {
let player: AVPlayer let player: AVPlayer
private var isGrayscale = false private var isGrayscale = false
private var audioSessionToken: AudioSessionCoordinator.Token?
let presentationSizeSubject = PassthroughSubject<CGSize, Never>() let presentationSizeSubject = PassthroughSubject<CGSize, Never>()
private var presentationSizeObservation: NSKeyValueObservation? private var presentationSizeObservation: NSKeyValueObservation?
@ -29,7 +30,9 @@ class GifvController {
self.isGrayscale = Preferences.shared.grayscaleImages self.isGrayscale = Preferences.shared.grayscaleImages
player.isMuted = true player.isMuted = true
#if !os(visionOS) #if os(visionOS)
player.preventsAutomaticBackgroundingDuringVideoPlayback = false
#else
player.preventsDisplaySleepDuringVideoPlayback = false player.preventsDisplaySleepDuringVideoPlayback = false
#endif #endif
@ -41,12 +44,19 @@ class GifvController {
func play() { func play() {
if player.rate == 0 { if player.rate == 0 {
player.play() audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .gifv) {
DispatchQueue.main.async {
self.player.play()
}
}
} }
} }
func pause() { func pause() {
player.pause() player.pause()
if let audioSessionToken {
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
}
} }
private func updatePresentationSizeObservation() { private func updatePresentationSizeObservation() {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
class WeakHolder<T: AnyObject> { final class WeakHolder<T: AnyObject> {
weak var object: T? weak var object: T?
init(_ object: T?) { init(_ object: T?) {
@ -16,7 +16,7 @@ class WeakHolder<T: AnyObject> {
} }
} }
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection { struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection, RandomAccessCollection, BidirectionalCollection {
private var array: [WeakHolder<Element>] private var array: [WeakHolder<Element>]
var startIndex: Int { array.startIndex } var startIndex: Int { array.startIndex }
@ -47,6 +47,10 @@ struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollect
return array.index(after: i) return array.index(after: i)
} }
func index(before i: Int) -> Int {
return array.index(before: i)
}
mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element { mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
array.replaceSubrange(subrange, with: newElements.map { WeakHolder($0) }) array.replaceSubrange(subrange, with: newElements.map { WeakHolder($0) })
} }