import Foundation import SwiftSignalKit import AVFoundation import MobileCoreServices import Display import Postbox import TelegramCore import MediaPlayer import TelegramAudio import UniversalMediaPlayer import TelegramPresentationData import TelegramUIPreferences import AccountContext import TelegramUniversalVideoContent import DeviceProximity import MediaResources import PhotoResources enum SharedMediaPlayerGroup: Int { case music = 0 case voiceAndInstantVideo = 1 } private let sharedAudioSession: ManagedAudioSession = { let audioSession = ManagedAudioSession() let _ = (audioSession.headsetConnected() |> deliverOnMainQueue).start(next: { value in DeviceProximityManager.shared().setGloballyEnabled(!value) }) return audioSession }() private struct GlobalControlOptions: OptionSet { var rawValue: Int32 init(rawValue: Int32 = 0) { self.rawValue = rawValue } static let play = GlobalControlOptions(rawValue: 1 << 0) static let pause = GlobalControlOptions(rawValue: 1 << 1) static let previous = GlobalControlOptions(rawValue: 1 << 2) static let next = GlobalControlOptions(rawValue: 1 << 3) static let playPause = GlobalControlOptions(rawValue: 1 << 4) static let seek = GlobalControlOptions(rawValue: 1 << 5) } public final class MediaManagerImpl: NSObject, MediaManager { public static var globalAudioSession: ManagedAudioSession { return sharedAudioSession } private let isCurrentPromise = ValuePromise(false) var isCurrent: Bool = false { didSet { if self.isCurrent != oldValue { self.isCurrentPromise.set(self.isCurrent) } } } private let queue = Queue.mainQueue() private let accountManager: AccountManager private let inForeground: Signal private let presentationData: Signal public let audioSession: ManagedAudioSession public let overlayMediaManager: OverlayMediaManager = OverlayMediaManager() let sharedVideoContextManager = SharedVideoContextManager() private var nextPlayerIndex: Int32 = 0 private let voiceMediaPlayerStateDisposable = MetaDisposable() private var voiceMediaPlayer: SharedMediaPlayer? { didSet { if self.voiceMediaPlayer !== oldValue { if let voiceMediaPlayer = self.voiceMediaPlayer { let account = voiceMediaPlayer.account self.voiceMediaPlayerStateDisposable.set((voiceMediaPlayer.playbackState |> deliverOnMainQueue).start(next: { [weak self, weak voiceMediaPlayer] state in guard let strongSelf = self else { return } guard let state = state, let voiceMediaPlayer = voiceMediaPlayer else { strongSelf.voiceMediaPlayerStateValue.set(.single(nil)) return } if case let .item(item) = state { strongSelf.voiceMediaPlayerStateValue.set(.single((account, .state(item)))) let audioLevelValue: (AccountRecordId, SharedMediaPlaylistId, SharedMediaPlaylistItemId, Signal)? = (account.id, item.playlistId, item.item.id, voiceMediaPlayer.audioLevel) strongSelf.voiceMediaPlayerAudioLevelEvents.set(.single(audioLevelValue)) } else { strongSelf.voiceMediaPlayerStateValue.set(.single((account, .loading))) strongSelf.voiceMediaPlayerAudioLevelEvents.set(.single(nil)) } })) } else { self.voiceMediaPlayerStateDisposable.set(nil) self.voiceMediaPlayerStateValue.set(.single(nil)) self.voiceMediaPlayerAudioLevelEvents.set(.single(nil)) } } } } private let voiceMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?>(nil) var voiceMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError> { return self.voiceMediaPlayerStateValue.get() } private let voiceMediaPlayerAudioLevelEvents = Promise<(AccountRecordId, SharedMediaPlaylistId, SharedMediaPlaylistItemId, Signal)?>(nil) private var musicMediaPlayer: SharedMediaPlayer? { didSet { if self.musicMediaPlayer !== oldValue { if let musicMediaPlayer = self.musicMediaPlayer { let type = musicMediaPlayer.type let account = musicMediaPlayer.account self.musicMediaPlayerStateValue.set(musicMediaPlayer.playbackState |> map { state -> (Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)? in guard let state = state else { return nil } if case let .item(item) = state { return (account, .state(item), type) } else { return (account, .loading, type) } } |> deliverOnMainQueue) } else { self.musicMediaPlayerStateValue.set(.single(nil)) } } } } private let musicMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?>(nil) public var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { return self.musicMediaPlayerStateValue.get() } private let globalMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?>() public var globalMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { return self.globalMediaPlayerStateValue.get() } public var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { return self.globalMediaPlayerStateValue.get() |> map { state -> (AccountRecordId, Bool)? in return state.flatMap { state -> (AccountRecordId, Bool) in var isPlaying = false if case let .state(value) = state.1 { switch value.status.status { case .playing: isPlaying = true case .buffering(_, true, _, _): isPlaying = true default: break } } return (state.0.id, isPlaying) } } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs?.0 != rhs?.0 { return false } if lhs?.1 != rhs?.1 { return false } return true }) } private let setPlaylistByTypeDisposables = DisposableDict() private var mediaPlaybackStateDisposable = MetaDisposable() private let sharedPlayerByGroup: [SharedMediaPlayerGroup: SharedMediaPlayer] = [:] private var currentOverlayVideoNode: OverlayMediaItemNode? private let globalControlsStatus = Promise(nil) private let globalControlsDisposable = MetaDisposable() private let globalControlsArtworkDisposable = MetaDisposable() private let globalControlsArtwork = Promise<(Account, SharedMediaPlaybackAlbumArt)?>(nil) private let globalControlsStatusDisposable = MetaDisposable() private let globalAudioSessionForegroundDisposable = MetaDisposable() public let universalVideoManager: UniversalVideoManager = UniversalVideoManagerImpl() public let galleryHiddenMediaManager: GalleryHiddenMediaManager = GalleryHiddenMediaManagerImpl() init(accountManager: AccountManager, inForeground: Signal, presentationData: Signal) { self.accountManager = accountManager self.inForeground = inForeground self.presentationData = presentationData self.audioSession = sharedAudioSession super.init() let combinedPlayersSignal: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> = combineLatest(queue: Queue.mainQueue(), self.voiceMediaPlayerState, self.musicMediaPlayerState) |> map { voice, music -> (Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)? in if let voice = voice { return (voice.0, voice.1, .voice) } else if let music = music { return (music.0, music.1, music.2) } else { return nil } } self.globalMediaPlayerStateValue.set(combinedPlayersSignal |> distinctUntilChanged(isEqual: { lhs, rhs in return lhs?.0 === rhs?.0 && lhs?.1 == rhs?.1 && lhs?.2 == rhs?.2 })) var baseNowPlayingInfo: [String: Any]? var previousState: SharedMediaPlayerItemPlaybackState? var previousDisplayData: SharedMediaPlaybackDisplayData? let globalControlsArtwork = self.globalControlsArtwork let globalControlsStatus = self.globalControlsStatus var currentGlobalControlsOptions = GlobalControlOptions() self.globalControlsDisposable.set((combineLatest(self.globalMediaPlayerState, self.presentationData) |> deliverOnMainQueue).start(next: { stateAndType, presentationData in var updatedGlobalControlOptions = GlobalControlOptions() if let (_, stateOrLoading, type) = stateAndType, case let .state(state) = stateOrLoading { if type == .music { updatedGlobalControlOptions.insert(.previous) updatedGlobalControlOptions.insert(.next) updatedGlobalControlOptions.insert(.seek) switch state.status.status { case .playing, .buffering(_, true, _, _): updatedGlobalControlOptions.insert(.pause) default: updatedGlobalControlOptions.insert(.play) } } } if let (account, stateOrLoading, type) = stateAndType, type == .music, case let .state(state) = stateOrLoading, let displayData = state.item.displayData { if previousDisplayData != displayData { previousDisplayData = displayData var nowPlayingInfo: [String: Any] = [:] var artwork: SharedMediaPlaybackAlbumArt? switch displayData { case let .music(title, performer, artworkValue, _, _): artwork = artworkValue let titleText: String = title ?? presentationData.strings.MediaPlayer_UnknownTrack let subtitleText: String = performer ?? presentationData.strings.MediaPlayer_UnknownArtist nowPlayingInfo[MPMediaItemPropertyTitle] = titleText nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText case let .voice(author, _): let titleText: String = author?.debugDisplayTitle ?? "" nowPlayingInfo[MPMediaItemPropertyTitle] = titleText case let .instantVideo(author, _, _): let titleText: String = author?.debugDisplayTitle ?? "" nowPlayingInfo[MPMediaItemPropertyTitle] = titleText } globalControlsArtwork.set(.single(artwork.flatMap({ (account, $0) }))) baseNowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } if previousState != state { previousState = state globalControlsStatus.set(.single(state.status)) } } else { previousState = nil previousDisplayData = nil globalControlsStatus.set(.single(nil)) globalControlsArtwork.set(.single(nil)) if baseNowPlayingInfo != nil { baseNowPlayingInfo = nil MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } } if currentGlobalControlsOptions != updatedGlobalControlOptions { let commandCenter = MPRemoteCommandCenter.shared() var optionsAndCommands: [(GlobalControlOptions, MPRemoteCommand, Selector)] = [ (.play, commandCenter.playCommand, #selector(self.playCommandEvent(_:))), (.pause, commandCenter.pauseCommand, #selector(self.pauseCommandEvent(_:))), (.previous, commandCenter.previousTrackCommand, #selector(self.previousTrackCommandEvent(_:))), (.next, commandCenter.nextTrackCommand, #selector(self.nextTrackCommandEvent(_:))), ([.play, .pause], commandCenter.togglePlayPauseCommand, #selector(self.togglePlayPauseCommandEvent(_:))) ] if #available(iOSApplicationExtension 9.1, iOS 9.1, *) { optionsAndCommands.append((.seek, commandCenter.changePlaybackPositionCommand, #selector(self.changePlaybackPositionCommandEvent(_:)))) } for (option, command, selector) in optionsAndCommands { let previousValue = !currentGlobalControlsOptions.intersection(option).isEmpty let updatedValue = !updatedGlobalControlOptions.intersection(option).isEmpty if previousValue != updatedValue { if updatedValue { command.isEnabled = true command.addTarget(self, action: selector) } else { command.isEnabled = false command.removeTarget(self, action: selector) } } } currentGlobalControlsOptions = updatedGlobalControlOptions } })) self.globalControlsArtworkDisposable.set((self.globalControlsArtwork.get() |> distinctUntilChanged(isEqual: { $0?.0 === $1?.0 && $0?.1 == $1?.1 }) |> mapToSignal { value -> Signal in if let (account, value) = value { return playerAlbumArt(postbox: account.postbox, engine: TelegramEngine(account: account), fileReference: value.fullSizeResource.file, albumArt: value, thumbnail: false) |> map { generator -> UIImage? in let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640, height: 640), boundingSize: CGSize(width: 640, height: 640), intrinsicInsets: .zero) return generator(arguments)?.generateImage() } } else { return .single(nil) } } |> deliverOnMainQueue).start(next: { image in if var nowPlayingInfo = baseNowPlayingInfo { if let image = image { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in return image }) } else { nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image) } } else { nowPlayingInfo.removeValue(forKey: MPMediaItemPropertyArtwork) } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo baseNowPlayingInfo = nowPlayingInfo } })) self.globalControlsStatusDisposable.set((self.globalControlsStatus.get() |> deliverOnMainQueue).start(next: { next in if let next = next { if var nowPlayingInfo = baseNowPlayingInfo { nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = next.duration as NSNumber switch next.status { case .playing: nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 as NSNumber case .buffering, .paused: nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 as NSNumber } nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = next.timestamp as NSNumber MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } } })) let shouldKeepAudioSession: Signal = combineLatest(queue: Queue.mainQueue(), self.globalMediaPlayerState, inForeground) |> map { stateAndType, inForeground -> Bool in var isPlaying = false if let (_, stateOrLoading, _) = stateAndType, case let .state(state) = stateOrLoading { switch state.status.status { case .playing: isPlaying = true case let .buffering(_, whilePlaying, _, _): isPlaying = whilePlaying default: break } } if !inForeground { if !isPlaying { return true } } return false } |> distinctUntilChanged |> mapToSignal { value -> Signal in if value { return .single(true) |> delay(0.8, queue: Queue.mainQueue()) } else { return .single(false) } } let throttledSignal = self.globalMediaPlayerState |> mapToThrottled { next -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in return .single(next) |> then(.complete() |> delay(2.0, queue: Queue.concurrentDefaultQueue())) } self.mediaPlaybackStateDisposable.set(throttledSignal.start(next: { accountStateAndType in let minimumStoreDuration: Double? if let (account, stateOrLoading, type) = accountStateAndType { switch type { case .music: minimumStoreDuration = 10.0 * 60.0 case .voice: minimumStoreDuration = 5.0 * 60.0 case .file: minimumStoreDuration = nil } if let minimumStoreDuration = minimumStoreDuration, case let .state(state) = stateOrLoading, state.status.duration >= minimumStoreDuration, case .playing = state.status.status { if let item = state.item as? MessageMediaPlaylistItem { var storedState: MediaPlaybackStoredState? if state.status.timestamp > 5.0 && state.status.timestamp < state.status.duration - 5.0 { storedState = MediaPlaybackStoredState(timestamp: state.status.timestamp, playbackRate: state.status.baseRate > 1.0 ? .x2 : .x1) } let _ = updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine(account: account), messageId: item.message.id, state: storedState).start() } } } })) self.globalAudioSessionForegroundDisposable.set((shouldKeepAudioSession |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } if strongSelf.isCurrent && value { strongSelf.audioSession.dropAll() } })) } deinit { self.globalControlsDisposable.dispose() self.globalControlsArtworkDisposable.dispose() self.globalControlsStatusDisposable.dispose() self.setPlaylistByTypeDisposables.dispose() self.mediaPlaybackStateDisposable.dispose() self.globalAudioSessionForegroundDisposable.dispose() self.voiceMediaPlayerStateDisposable.dispose() } public func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { let audioRecorder = ManagedAudioRecorderImpl(mediaManager: self, pushIdleTimerExtension: { [weak applicationBindings] in return applicationBindings?.pushIdleTimerExtension() ?? EmptyDisposable }, beginWithTone: beginWithTone, beganWithTone: beganWithTone) subscriber.putNext(audioRecorder) disposable.set(ActionDisposable { }) } return disposable } } public func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) { assert(Queue.mainQueue().isCurrent()) let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> if let (account, playlist) = playlist { inputData = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.musicPlaybackSettings]) |> take(1) |> mapToSignal { sharedData -> Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.musicPlaybackSettings]?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings if let location = playlist.location as? PeerMessagesPlaylistLocation, let messageId = location.messageId { return mediaPlaybackStoredState(engine: TelegramEngine(account: account), messageId: messageId) |> map { storedState in return (account, playlist, settings, storedState) } } else { return .single((account, playlist, settings, nil)) } } } else { inputData = .single(nil) } self.setPlaylistByTypeDisposables.set((inputData |> deliverOnMainQueue).start(next: { [weak self] inputData in if let strongSelf = self { let nextPlayerIndex = strongSelf.nextPlayerIndex strongSelf.nextPlayerIndex += 1 switch type { case .voice: strongSelf.musicMediaPlayer?.control(.playback(.pause)) strongSelf.voiceMediaPlayer?.stop() if let (account, playlist, settings, storedState) = inputData { let voiceMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: true, type: type) strongSelf.voiceMediaPlayer = voiceMediaPlayer voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer { voiceMediaPlayer.stop() strongSelf.voiceMediaPlayer = nil } } voiceMediaPlayer.cancelled = { [weak voiceMediaPlayer] in if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer { voiceMediaPlayer.stop() strongSelf.voiceMediaPlayer = nil } } var control = control if let timestamp = storedState?.timestamp { control = .seek(timestamp) } voiceMediaPlayer.control(control) } else { strongSelf.voiceMediaPlayer = nil } case .music, .file: strongSelf.musicMediaPlayer?.stop() strongSelf.voiceMediaPlayer?.control(.playback(.pause)) if let (account, playlist, settings, storedState) = inputData { let musicMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type) strongSelf.musicMediaPlayer = musicMediaPlayer musicMediaPlayer.cancelled = { [weak musicMediaPlayer] in if let strongSelf = self, let musicMediaPlayer = musicMediaPlayer, musicMediaPlayer === strongSelf.musicMediaPlayer { musicMediaPlayer.stop() strongSelf.musicMediaPlayer = nil } } var control = control if let timestamp = storedState?.timestamp { control = .seek(timestamp) } strongSelf.musicMediaPlayer?.control(control) } else { strongSelf.musicMediaPlayer = nil } } } }), forKey: type) } public func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) { assert(Queue.mainQueue().isCurrent()) let selectedType: MediaManagerPlayerType if let type = type { selectedType = type } else if self.voiceMediaPlayer != nil { selectedType = .voice } else { selectedType = .music } switch selectedType { case .voice: self.voiceMediaPlayer?.control(control) case .music, .file: if self.voiceMediaPlayer != nil { switch control { case .playback(.play), .playback(.togglePlayPause): self.setPlaylist(nil, type: .voice, control: .playback(.pause)) default: break } } self.musicMediaPlayer?.control(control) } } public func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal { let signal: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError> switch type { case .voice: signal = self.voiceMediaPlayerState case .music, .file: signal = self.musicMediaPlayerState |> map { value in return value.flatMap { ($0.0, $0.1) } } } return signal |> map { stateOrLoading -> SharedMediaPlayerItemPlaybackState? in if let (account, stateOrLoading) = stateOrLoading, account.id == accountId, case let .state(state) = stateOrLoading { if state.playlistId.isEqual(to: playlistId) && state.item.id.isEqual(to: itemId) { return state } } return nil } |> distinctUntilChanged(isEqual: { lhs, rhs in return lhs == rhs }) } public func filteredPlayerAudioLevelEvents(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal { switch type { case .voice: return self.voiceMediaPlayerAudioLevelEvents.get() |> mapToSignal { value -> Signal in guard let value = value else { return .never() } let (accountIdValue, playlistIdValue, itemIdValue, signal) = value if accountIdValue == accountId && playlistId.isEqual(to: playlistIdValue) && itemId.isEqual(to: itemIdValue) { return signal } else { return .never() } } case .music, .file: return .never() } } @objc func playCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus { self.playlistControl(.playback(.play), type: nil) return .success } @objc func pauseCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus { self.playlistControl(.playback(.pause), type: nil) return .success } @objc func previousTrackCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus { self.playlistControl(.previous, type: nil) return .success } @objc func nextTrackCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus { self.playlistControl(.next, type: nil) return .success } @objc func togglePlayPauseCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus { self.playlistControl(.playback(.togglePlayPause), type: nil) return .success } @objc func changePlaybackPositionCommandEvent(_ event: MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus { self.playlistControl(.seek(event.positionTime), type: nil) return .success } public func setOverlayVideoNode(_ node: OverlayMediaItemNode?) { if let currentOverlayVideoNode = self.currentOverlayVideoNode { self.overlayMediaManager.controller?.removeNode(currentOverlayVideoNode, customTransition: true) self.currentOverlayVideoNode = nil } if let node = node { self.currentOverlayVideoNode = node self.overlayMediaManager.controller?.addNode(node, customTransition: true) } } public func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool { return self.currentOverlayVideoNode === node } }