import Foundation import Postbox import TelegramCore import SwiftSignalKit import UIKit import AsyncDisplayKit import TelegramAudio import UniversalMediaPlayer import RangeSet public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId { case peer(PeerId) case recentActions(PeerId) case feed(Int32) case savedMusic(PeerId) case custom public func isEqual(to: SharedMediaPlaylistId) -> Bool { if let to = to as? PeerMessagesMediaPlaylistId { return self == to } return false } } public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation { case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId) case singleMessage(MessageId) case recentActions(Message) case savedMusic(context: ProfileSavedMusicContext, at: Int32, canReorder: Bool) case custom(messages: Signal<([Message], Int32, Bool), NoError>, canReorder: Bool, at: MessageId, loadMore: (() -> Void)?) public var playlistId: PeerMessagesMediaPlaylistId { switch self { case let .messages(chatLocation, _, _): switch chatLocation { case let .peer(peerId): return .peer(peerId) case let .replyThread(replyThreaMessage): return .peer(replyThreaMessage.peerId) case .customChatContents: return .custom } case let .singleMessage(id): return .peer(id.peerId) case let .recentActions(message): return .recentActions(message.id.peerId) case let .savedMusic(context, _, _): return .savedMusic(context.peerId) case .custom: return .custom } } public func effectiveLocation(context: AccountContext) -> PeerMessagesPlaylistLocation { switch self { case let .savedMusic(savedMusicContext, at, canReorder): let peerId = savedMusicContext.peerId return .custom( messages: combineLatest( savedMusicContext.state, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: savedMusicContext.peerId)) ) |> map { state, peer in var messages: [Message] = [] var peers = SimpleDictionary() if let peer { peers[peerId] = peer._asPeer() } for file in state.files { let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) } var canLoadMore = false if case let .ready(canLoadMoreValue) = state.dataState { canLoadMore = canLoadMoreValue } return (messages, Int32(messages.count), canLoadMore) }, canReorder: canReorder, at: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: at), loadMore: { [weak savedMusicContext] in guard let savedMusicContext else { return } savedMusicContext.loadMore() } ) default: return self } } public var messageId: MessageId? { switch self { case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _): return messageId default: return nil } } public func isEqual(to: SharedMediaPlaylistLocation) -> Bool { if let to = to as? PeerMessagesPlaylistLocation { return self == to } else { return false } } public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool { switch lhs { case let .messages(chatLocation, tagMask, at): if case .messages(chatLocation, tagMask, at) = rhs { return true } else { return false } case let .singleMessage(messageId): if case .singleMessage(messageId) = rhs { return true } else { return false } case let .recentActions(lhsMessage): if case let .recentActions(rhsMessage) = rhs, lhsMessage.id == rhsMessage.id { return true } else { return false } case let .savedMusic(lhsContext, lhsAt, _): if case let .savedMusic(rhsContext, rhsAt, _) = rhs { return lhsContext.peerId == rhsContext.peerId && lhsAt == rhsAt } else { return false } case let .custom(_, _, lhsAt, _): if case let .custom(_, _, rhsAt, _) = rhs, lhsAt == rhsAt { return true } else { return false } } } } public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? { func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? { var file: TelegramMediaFile? for media in message.media { if let media = media as? TelegramMediaFile { file = media break } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content, let f = content.file { file = f break } } return file } if let file = extractFileMedia(message) { if file.isVoice || file.isInstantVideo { return .voice } else if file.isMusic { return .music } } return nil } public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { if isSavedMusic { return (PeerMessagesMediaPlaylistId.savedMusic(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else if isGlobalSearch && !isDownloadList { return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else if isRecentActions && !isDownloadList { return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else { return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } } public enum MediaManagerPlayerType { case voice case music case file } public struct AudioRecorderResumeData { public let compressedData: Data public let resumeData: Data public init(compressedData: Data, resumeData: Data) { self.compressedData = compressedData self.resumeData = resumeData } } public protocol MediaManager: AnyObject { var audioSession: ManagedAudioSession { get } var galleryHiddenMediaManager: GalleryHiddenMediaManager { get } var universalVideoManager: UniversalVideoManager { get } var overlayMediaManager: OverlayMediaManager { get } var currentPictureInPictureNode: AnyObject? { get set } var globalMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get } var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get } var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { get } func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal func filteredPlayerAudioLevelEvents(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal func setOverlayVideoNode(_ node: OverlayMediaItemNode?) func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool func audioRecorder( resumeData: AudioRecorderResumeData?, beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void ) -> Signal } public enum GalleryHiddenMediaId: Hashable { case chat(AccountRecordId, MessageId, Media) public static func ==(lhs: GalleryHiddenMediaId, rhs: GalleryHiddenMediaId) -> Bool { switch lhs { case let .chat(lhsAccountId ,lhsMessageId, lhsMedia): if case let .chat(rhsAccountId, rhsMessageId, rhsMedia) = rhs, lhsAccountId == rhsAccountId, lhsMessageId == rhsMessageId, lhsMedia.isEqual(to: rhsMedia) { return true } else { return false } } } public func hash(into hasher: inout Hasher) { switch self { case let .chat(accountId, messageId, _): hasher.combine(accountId) hasher.combine(messageId) } } } public protocol GalleryHiddenMediaTarget: AnyObject { func getTransitionInfo(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))? } public protocol GalleryHiddenMediaManager: AnyObject { func hiddenIds() -> Signal, NoError> func addSource(_ signal: Signal) -> Int func removeSource(_ index: Int) func addTarget(_ target: GalleryHiddenMediaTarget) func removeTarget(_ target: GalleryHiddenMediaTarget) func findTarget(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))? } public protocol UniversalVideoManager: AnyObject { func attachUniversalVideoContent(content: UniversalVideoContent, priority: UniversalVideoPriority, create: () -> UniversalVideoContentNode & ASDisplayNode, update: @escaping (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void) -> (AnyHashable, Int32) func detachUniversalVideoContent(id: AnyHashable, index: Int32) func withUniversalVideoContent(id: AnyHashable, _ f: ((UniversalVideoContentNode & ASDisplayNode)?) -> Void) func addPlaybackCompleted(id: AnyHashable, _ f: @escaping () -> Void) -> Int func removePlaybackCompleted(id: AnyHashable, index: Int) func statusSignal(content: UniversalVideoContent) -> Signal func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(RangeSet, Int64)?, NoError> func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal } public enum AudioRecordingState: Equatable { case paused(duration: Double) case recording(duration: Double, durationMediaTimestamp: Double) case stopped } public struct RecordedAudioData { public let compressedData: Data public let resumeData: Data? public let duration: Double public let waveform: Data? public let trimRange: Range? public init(compressedData: Data, resumeData: Data?, duration: Double, waveform: Data?, trimRange: Range?) { self.compressedData = compressedData self.resumeData = resumeData self.duration = duration self.waveform = waveform self.trimRange = trimRange } } public protocol ManagedAudioRecorder: AnyObject { var beginWithTone: Bool { get } var micLevel: Signal { get } var recordingState: Signal { get } func start() func pause() func resume() func stop() func takenRecordedData() -> Signal func updateTrimRange(start: Double, end: Double, updatedEnd: Bool, apply: Bool) }