Mute switch monitoring

This commit is contained in:
Ali 2023-07-03 20:28:01 +02:00
parent e316f0521b
commit f59df97175
11 changed files with 218 additions and 46 deletions

View File

@ -23,7 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject {
func setSoundEnabled(_ value: Bool)
func seek(_ timestamp: Double)
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func continueWithOverridingAmbientMode()
func continueWithOverridingAmbientMode(isAmbient: Bool)
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
@ -284,10 +284,10 @@ public final class UniversalVideoNode: ASDisplayNode {
})
}
public func continueWithOverridingAmbientMode() {
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.continueWithOverridingAmbientMode()
contentNode.continueWithOverridingAmbientMode(isAmbient: isAmbient)
}
})
}

View File

@ -601,10 +601,27 @@ private final class MediaPlayerContext {
self.stoppedAtEnd = false
}
fileprivate func continueWithOverridingAmbientMode() {
if self.ambient {
fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) {
if !isAmbient {
self.ambient = false
var loadedState: MediaPlayerLoadedState?
switch self.state {
case .empty:
break
case let .playing(currentLoadedState):
loadedState = currentLoadedState
case let .paused(currentLoadedState):
loadedState = currentLoadedState
case .seeking:
break
}
if let loadedState = loadedState {
let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
self.seek(timestamp: timestamp, action: .play)
}
} else {
self.ambient = true
var loadedState: MediaPlayerLoadedState?
switch self.state {
case .empty:
@ -1169,10 +1186,10 @@ public final class MediaPlayer {
}
}
public func continueWithOverridingAmbientMode() {
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() {
context.continueWithOverridingAmbientMode()
context.continueWithOverridingAmbientMode(isAmbient: isAmbient)
}
}
}

View File

@ -19,6 +19,7 @@ import simd
import VolumeButtons
import TooltipUI
import ChatEntityKeyboardInputNode
import notify
func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder {
@ -32,6 +33,49 @@ func hasFirstResponder(_ view: UIView) -> Bool {
return false
}
private final class MuteMonitor {
private let updated: (Bool) -> Void
private var token: Int32 = NOTIFY_TOKEN_INVALID
private(set) var currentValue: Bool = false
init(updated: @escaping (Bool) -> Void) {
self.updated = updated
let status = notify_register_dispatch("com.apple.springboard.ringerstate", &self.token, DispatchQueue.main, { [weak self] value in
guard let self else {
return
}
let value = self.refresh()
if self.currentValue != value {
self.currentValue = value
self.updated(value)
}
})
let _ = status
//print("Notify status: \(status)")
self.currentValue = self.refresh()
}
private func refresh() -> Bool {
var state: UInt64 = 0
if self.token != NOTIFY_TOKEN_INVALID {
let status = notify_get_state(self.token, &state)
let _ = status
//print("Notify refresh status: \(status)")
}
return state != 0
}
deinit {
if self.token != NOTIFY_TOKEN_INVALID {
notify_cancel(self.token)
}
}
}
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
var shouldBegin: ((UITouch) -> Bool)?
var updateIsTracking: ((Bool) -> Void)?
@ -189,7 +233,20 @@ private final class StoryContainerScreenComponent: Component {
private var transitionCloneMasterView: UIView
private var volumeButtonsListener: VolumeButtonsListener?
private let volumeButtonsListenerShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
private let contentWantsVolumeButtonMonitoring = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isMuteSwitchOnPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let volumeButtonsListenerShouldBeActive = Promise<Bool>()
private var volumeButtonsListenerShouldBeActiveDisposable: Disposable?
private var isMuteSwitchOn: Bool = false
private var muteMonitor: MuteMonitor?
private var audioMode: StoryContentItem.AudioMode = .ambient {
didSet {
self.audioModePromise.set(self.audioMode)
}
}
private let audioModePromise = ValuePromise<StoryContentItem.AudioMode>(.ambient, ignoreRepeated: true)
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
@ -287,6 +344,70 @@ private final class StoryContainerScreenComponent: Component {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.backgroundEffectView.addGestureRecognizer(tapGestureRecognizer)
let muteMonitor = MuteMonitor(updated: { [weak self] isMuteSwitchOn in
Queue.mainQueue().async {
guard let self else {
return
}
if self.isMuteSwitchOn != isMuteSwitchOn {
let changedToOff = self.isMuteSwitchOn && !isMuteSwitchOn
self.isMuteSwitchOn = isMuteSwitchOn
self.isMuteSwitchOnPromise.set(self.isMuteSwitchOn)
if changedToOff {
switch self.audioMode {
case .on:
self.audioMode = .ambient
for (_, itemSetView) in self.visibleItemSetViews {
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
componentView.enterAmbientMode(ambient: !self.isMuteSwitchOn)
}
}
default:
break
}
}
self.state?.updated(transition: .immediate)
}
}
})
self.muteMonitor = muteMonitor
self.isMuteSwitchOn = muteMonitor.currentValue
self.isMuteSwitchOnPromise.set(self.isMuteSwitchOn)
self.volumeButtonsListenerShouldBeActiveDisposable = (combineLatest(queue: .mainQueue(),
self.contentWantsVolumeButtonMonitoring.get(),
self.isMuteSwitchOnPromise.get(),
self.audioModePromise.get()
)
|> map { contentWantsVolumeButtonMonitoring, isMuteSwitchOn, audioMode -> Bool in
if !contentWantsVolumeButtonMonitoring {
return false
}
switch audioMode {
case .ambient:
if isMuteSwitchOn {
return false
} else {
return true
}
case .on:
return false
case .off:
return true
}
}
|> distinctUntilChanged).start(next: { [weak self] enable in
guard let self else {
return
}
self.volumeButtonsListenerShouldBeActive.set(.single(enable))
self.updateVolumeButtonMonitoring()
})
}
required init?(coder: NSCoder) {
@ -295,6 +416,7 @@ private final class StoryContainerScreenComponent: Component {
deinit {
self.contentUpdatedDisposable?.dispose()
self.volumeButtonsListenerShouldBeActiveDisposable?.dispose()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
@ -665,17 +787,24 @@ private final class StoryContainerScreenComponent: Component {
private func updateVolumeButtonMonitoring() {
if self.volumeButtonsListener == nil {
let buttonAction = { [weak self] in
guard let self, self.storyItemSharedState.useAmbientMode else {
guard let self else {
return
}
self.storyItemSharedState.useAmbientMode = false
self.volumeButtonsListenerShouldBeActive.set(false)
switch self.audioMode {
case .off, .ambient:
break
case .on:
return
}
self.audioMode = .on
for (_, itemSetView) in self.visibleItemSetViews {
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
componentView.leaveAmbientMode()
}
}
self.state?.updated(transition: .immediate)
}
self.volumeButtonsListener = VolumeButtonsListener(
shouldBeActive: self.volumeButtonsListenerShouldBeActive.get(),
@ -740,12 +869,7 @@ private final class StoryContainerScreenComponent: Component {
}
self.focusedItem.set(focusedItemId)
if self.storyItemSharedState.useAmbientMode {
self.volumeButtonsListenerShouldBeActive.set(isVideo)
if isVideo {
self.updateVolumeButtonMonitoring()
}
}
self.contentWantsVolumeButtonMonitoring.set(isVideo)
if component.content.stateValue?.slice == nil {
self.environment?.controller()?.dismiss()
@ -898,6 +1022,8 @@ private final class StoryContainerScreenComponent: Component {
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isProgressPaused: isProgressPaused || i != focusedIndex,
isAudioMuted: self.audioMode == .off || (self.audioMode == .ambient && !self.isMuteSwitchOn),
useAmbientMode: self.audioMode == .ambient,
hideUI: (i == focusedIndex && (self.itemSetPanState?.didBegin == false || self.itemSetPinchState != nil)),
visibilityFraction: 1.0 - abs(panFraction + cubeAdditionalRotationFraction),
isPanning: self.itemSetPanState?.didBegin == true,
@ -995,25 +1121,31 @@ private final class StoryContainerScreenComponent: Component {
return
}
if self.storyItemSharedState.useAmbientMode {
self.storyItemSharedState.useAmbientMode = false
self.volumeButtonsListenerShouldBeActive.set(false)
switch self.audioMode {
case .ambient:
self.audioMode = .on
for (_, itemSetView) in self.visibleItemSetViews {
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
componentView.leaveAmbientMode()
}
}
} else {
self.storyItemSharedState.useAmbientMode = true
self.volumeButtonsListenerShouldBeActive.set(true)
case .on:
self.audioMode = .off
for (_, itemSetView) in self.visibleItemSetViews {
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
componentView.enterAmbientMode()
componentView.enterAmbientMode(ambient: !self.isMuteSwitchOn)
}
}
case .off:
self.audioMode = .on
for (_, itemSetView) in self.visibleItemSetViews {
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
componentView.leaveAmbientMode()
}
}
}
self.state?.updated(transition: .immediate)
},
keyboardInputData: self.inputMediaNodeDataPromise.get(),
sharedViewListsContext: self.sharedViewListsContext

View File

@ -13,9 +13,13 @@ public final class StoryContentItem: Equatable {
}
}
public enum AudioMode {
case ambient
case on
case off
}
public final class SharedState {
public var useAmbientMode: Bool = true
public init() {
}
}
@ -30,7 +34,7 @@ public final class StoryContentItem: Equatable {
open func leaveAmbientMode() {
}
open func enterAmbientMode() {
open func enterAmbientMode(ambient: Bool) {
}
open var videoPlaybackPosition: Double? {

View File

@ -29,11 +29,13 @@ final class StoryItemContentComponent: Component {
let context: AccountContext
let peer: EnginePeer
let item: EngineStoryItem
let useAmbientMode: Bool
init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem) {
init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem, useAmbientMode: Bool) {
self.context = context
self.peer = peer
self.item = item
self.useAmbientMode = useAmbientMode
}
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
@ -118,7 +120,7 @@ final class StoryItemContentComponent: Component {
return
}
guard let component = self.component, let environment = self.environment, let currentMessageMedia = self.currentMessageMedia else {
guard let component = self.component, let currentMessageMedia = self.currentMessageMedia else {
return
}
@ -137,7 +139,7 @@ final class StoryItemContentComponent: Component {
streamVideo: .story,
loopVideo: true,
enableSound: true,
beginWithAmbientSound: environment.sharedState.useAmbientMode,
beginWithAmbientSound: component.useAmbientMode,
mixWithOthers: true,
useLargeThumbnail: false,
autoFetchFullSizeThumbnail: false,
@ -216,13 +218,17 @@ final class StoryItemContentComponent: Component {
override func leaveAmbientMode() {
if let videoNode = self.videoNode {
videoNode.setSoundEnabled(true)
videoNode.continueWithOverridingAmbientMode()
videoNode.continueWithOverridingAmbientMode(isAmbient: false)
}
}
override func enterAmbientMode() {
override func enterAmbientMode(ambient: Bool) {
if let videoNode = self.videoNode {
videoNode.setSoundEnabled(false)
if ambient {
videoNode.continueWithOverridingAmbientMode(isAmbient: true)
} else {
videoNode.setSoundEnabled(false)
}
}
}

View File

@ -85,6 +85,8 @@ public final class StoryItemSetContainerComponent: Component {
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let isProgressPaused: Bool
public let isAudioMuted: Bool
public let useAmbientMode: Bool
public let hideUI: Bool
public let visibilityFraction: CGFloat
public let isPanning: Bool
@ -115,6 +117,8 @@ public final class StoryItemSetContainerComponent: Component {
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
isProgressPaused: Bool,
isAudioMuted: Bool,
useAmbientMode: Bool,
hideUI: Bool,
visibilityFraction: CGFloat,
isPanning: Bool,
@ -144,6 +148,8 @@ public final class StoryItemSetContainerComponent: Component {
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.isProgressPaused = isProgressPaused
self.isAudioMuted = isAudioMuted
self.useAmbientMode = useAmbientMode
self.hideUI = hideUI
self.visibilityFraction = visibilityFraction
self.isPanning = isPanning
@ -192,6 +198,12 @@ public final class StoryItemSetContainerComponent: Component {
if lhs.isProgressPaused != rhs.isProgressPaused {
return false
}
if lhs.isAudioMuted != rhs.isAudioMuted {
return false
}
if lhs.useAmbientMode != rhs.useAmbientMode {
return false
}
if lhs.hideUI != rhs.hideUI {
return false
}
@ -605,7 +617,7 @@ public final class StoryItemSetContainerComponent: Component {
self.state?.updated(transition: .immediate)
}
func enterAmbientMode() {
func enterAmbientMode(ambient: Bool) {
guard let component = self.component else {
return
}
@ -613,7 +625,7 @@ public final class StoryItemSetContainerComponent: Component {
return
}
if let itemView = visibleItem.view.view as? StoryContentItem.View {
itemView.enterAmbientMode()
itemView.enterAmbientMode(ambient: ambient)
}
self.state?.updated(transition: .immediate)
@ -1027,7 +1039,8 @@ public final class StoryItemSetContainerComponent: Component {
component: AnyComponent(StoryItemContentComponent(
context: component.context,
peer: component.slice.peer,
item: item.storyItem
item: item.storyItem,
useAmbientMode: component.useAmbientMode
)),
environment: {
itemEnvironment
@ -2163,7 +2176,7 @@ public final class StoryItemSetContainerComponent: Component {
}
let soundImage: String
if isSilentVideo || component.storyItemSharedState.useAmbientMode {
if isSilentVideo || component.isAudioMuted {
soundImage = "Stories/SoundOff"
} else {
soundImage = "Stories/SoundOn"

View File

@ -115,7 +115,7 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
self.videoNode.playOnceWithSound(playAndRecord: playAndRecord)
}
func continueWithOverridingAmbientMode() {
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func pause() {

View File

@ -422,7 +422,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
func setSoundEnabled(_ value: Bool) {
assert(Queue.mainQueue().isCurrent())
if value {
self.player.playOnceWithSound(playAndRecord: true, seek: .none)
self.player.playOnceWithSound(playAndRecord: false, seek: .none)
} else {
self.player.continuePlayingWithoutSound(seek: .none)
}
@ -475,8 +475,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.player.setForceAudioToSpeaker(forceAudioToSpeaker)
}
func continueWithOverridingAmbientMode() {
self.player.continueWithOverridingAmbientMode()
func continueWithOverridingAmbientMode(isAmbient: Bool) {
self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient)
}
func setBaseRate(_ baseRate: Double) {

View File

@ -430,7 +430,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
}
func continueWithOverridingAmbientMode() {
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {

View File

@ -267,7 +267,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
}
func continueWithOverridingAmbientMode() {
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {

View File

@ -164,7 +164,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
}
}
func continueWithOverridingAmbientMode() {
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {