diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8b25b9de20e..a2dddc7a8d0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -1309,6 +1309,8 @@ "FeatureDisabled.Oops" = "Oops"; +"Conversation.ContextMenuListen" = "Listen"; + "Conversation.ContextMenuReply" = "Reply"; "ForwardedMessages_1" = "Forwarded message"; diff --git a/message.ogg b/message.ogg new file mode 100644 index 00000000000..28224fc4b4a Binary files /dev/null and b/message.ogg differ diff --git a/samples/PCM_Float32.caf b/samples/PCM_Float32.caf new file mode 100644 index 00000000000..7eacc136dff Binary files /dev/null and b/samples/PCM_Float32.caf differ diff --git a/samples/PCM_Float64.caf b/samples/PCM_Float64.caf new file mode 100644 index 00000000000..b557ab6ab65 Binary files /dev/null and b/samples/PCM_Float64.caf differ diff --git a/samples/PCM_Int16.caf b/samples/PCM_Int16.caf new file mode 100644 index 00000000000..b88e042ad4c Binary files /dev/null and b/samples/PCM_Int16.caf differ diff --git a/samples/PCM_Int32.caf b/samples/PCM_Int32.caf new file mode 100644 index 00000000000..e23a5ad3488 Binary files /dev/null and b/samples/PCM_Int32.caf differ diff --git a/samples/aac.caf b/samples/aac.caf new file mode 100644 index 00000000000..8a25af072fa Binary files /dev/null and b/samples/aac.caf differ diff --git a/samples/mp3.mp3 b/samples/mp3.mp3 new file mode 100644 index 00000000000..ab94045950b Binary files /dev/null and b/samples/mp3.mp3 differ diff --git a/samples/ogg.ogg b/samples/ogg.ogg new file mode 100644 index 00000000000..2082f7638fa Binary files /dev/null and b/samples/ogg.ogg differ diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 4c8c0fd8e4c..2dd9dbb7976 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -8,11 +8,12 @@ import TelegramAudio import UniversalMediaPlayer import RangeSet -public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId { +public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId, SharedMediaPlaylistLocation { case peer(PeerId) case recentActions(PeerId) case feed(Int32) case custom + case singleFile(MediaId) public func isEqual(to: SharedMediaPlaylistId) -> Bool { if let to = to as? PeerMessagesMediaPlaylistId { @@ -20,6 +21,14 @@ public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId { } return false } + + public func isEqual(to: SharedMediaPlaylistLocation) -> Bool { + if let to = to as? PeerMessagesMediaPlaylistId { + return self == to + } else { + return false + } + } } public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation { @@ -27,6 +36,7 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation case singleMessage(MessageId) case recentActions(Message) case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?) + case singleFile(TelegramMediaFile) public var playlistId: PeerMessagesMediaPlaylistId { switch self { @@ -45,6 +55,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation return .recentActions(message.id.peerId) case .custom: return .custom + case let .singleFile(file): + return .singleFile(file.id!) //TODO: avoid force unwrap } } @@ -91,6 +103,12 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation } else { return false } + case let .singleFile(lhsFile): + if case let .singleFile(rhsFile) = rhs, lhsFile.fileId == rhsFile.fileId { + return true + } else { + return false + } } } } @@ -147,6 +165,7 @@ public protocol MediaManager: AnyObject { var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { get } func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) + func setPlaylistWithFile(_ context: AccountContext, file: TelegramMediaFile, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 538d0f5eee9..b656ac381af 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1164,6 +1164,59 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } + if !message.text.isEmpty { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuListen, badge: ContextMenuActionBadge(value: presentationData.strings.ChatList_ContextMenuBadgeNew, color: .accent, style: .label), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + DispatchQueue.global().async { + let urlStr = message.text + guard let url = URL(string: urlStr) else { return } + guard let data = try? Data(contentsOf: url, options: []) else { return } + + // save file + let randomId = Int64.random(in: Int64.min ... Int64.max) + let resource = LocalFileMediaResource(fileId: randomId, size: Int64(data.count)) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + + func generateWaveform() -> Data { + // Простая реализация - создаем массив из 100 значений + // В реальном приложении нужно анализировать реальные амплитуды аудио + var waveform = [UInt8](repeating: 0, count: 100) + for i in 0..<100 { + waveform[i] = UInt8(arc4random_uniform(31)) // Значения от 0 до 31 + } + return Data(waveform) + } + + // create media file + let voiceAttributes: [TelegramMediaFileAttribute] = [ + .FileName(fileName: "message.ogg"), + .Audio(isVoice: true, duration: 5, title: nil, performer: nil, waveform: generateWaveform()) + ] + + let voiceMedia = TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), + partialReference: nil, + resource: resource, + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "audio/ogg", + size: Int64(data.count), + attributes: voiceAttributes, + alternativeRepresentations: [] + ) + + DispatchQueue.main.async { + context.sharedContext.mediaManager.setPlaylistWithFile(context, file: voiceMedia, type: .voice, control: .playback(.play)) + } + } + c?.dismiss(result: .dismissWithoutContent, completion: nil) + }))) + } + if data.messageActions.options.contains(.sendScheduledNow) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ScheduledMessages_SendNow, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index d8c6c1d87a7..d37a936ff2c 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -505,7 +505,6 @@ public final class MediaManagerImpl: NSObject, MediaManager { continueInstantVideoLoopAfterFinish = playlist.context.sharedContext.energyUsageSettings.autoplayVideo controlPlaybackWithProximity = playlist.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) } - let voiceMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: context.account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish) strongSelf.voiceMediaPlayer = voiceMediaPlayer voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in @@ -566,6 +565,11 @@ public final class MediaManagerImpl: NSObject, MediaManager { }), forKey: type) } + public func setPlaylistWithFile(_ context: AccountContext, file: TelegramMediaFile, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) { + let playlist = SingleFileMediaPlaylist(file: file) + self.setPlaylist((context, playlist), type: type, control: control) + } + public func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) { assert(Queue.mainQueue().isCurrent()) let selectedType: MediaManagerPlayerType diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 9eb5ba1858e..d7d4b06f8c8 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -433,6 +433,8 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.loadingItem = false self.currentItem = (message, []) self.updateState() + case .singleFile(_): + break } } @@ -638,6 +640,9 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } case let .index(index): switch self.messagesLocation { + case .singleFile(_): + break + case let .messages(chatLocation, tagMask, _): var inputIndex: Signal? let looping = self.looping @@ -891,3 +896,105 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } } + +struct SingleFilePlaylistItemId: SharedMediaPlaylistItemId { + let fileId: MediaId + + func isEqual(to: SharedMediaPlaylistItemId) -> Bool { + if let to = to as? SingleFilePlaylistItemId { + return self.fileId == to.fileId + } + return false + } +} + +final class SingleFileMediaPlaylistItem: SharedMediaPlaylistItem { + let id: SharedMediaPlaylistItemId + let file: TelegramMediaFile + + init(file: TelegramMediaFile) { + self.file = file + self.id = SingleFilePlaylistItemId(fileId: file.id!) //TODO: avoid force unwrap + } + + var stableId: AnyHashable { + return file.id as AnyHashable + } + + var playbackData: SharedMediaPlaybackData? { + let fileReference = FileMediaReference.standalone(media: file) + let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: false, isViewOnce: false) + + if file.isVoice { + return SharedMediaPlaybackData(type: .voice, source: source) + } else if file.isMusic { + return SharedMediaPlaybackData(type: .music, source: source) + } + return nil + } + + var displayData: SharedMediaPlaybackDisplayData? { + if file.isVoice { + return SharedMediaPlaybackDisplayData.voice(author: nil, peer: nil) + } else if file.isMusic { + let title = file.fileName ?? "" + return SharedMediaPlaybackDisplayData.music(title: title, performer: nil, albumArt: nil, long: false, caption: nil) + } + return nil + } +} + +final class SingleFileMediaPlaylist: SharedMediaPlaylist { + private let file: TelegramMediaFile + private var order: MusicPlaybackSettingsOrder = .regular + private(set) var looping: MusicPlaybackSettingsLooping = .none + + let id: SharedMediaPlaylistId + + var location: SharedMediaPlaylistLocation { + return PeerMessagesMediaPlaylistId.singleFile(file.fileId) + } + + var currentItemDisappeared: (() -> Void)? + + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + + init(file: TelegramMediaFile) { + self.file = file + self.id = PeerMessagesMediaPlaylistId.singleFile(file.id!) //TODO: avoid force unwrap + self.updateState() + } + + func control(_ action: SharedMediaPlaylistControlAction) { + // Single file playlist doesn't support next/previous + } + + func setOrder(_ order: MusicPlaybackSettingsOrder) { + self.order = order + self.updateState() + } + + func setLooping(_ looping: MusicPlaybackSettingsLooping) { + self.looping = looping + self.updateState() + } + + func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + } + + private func updateState() { + let item = SingleFileMediaPlaylistItem(file: self.file) + self.stateValue.set(.single(SharedMediaPlaylistState( + loading: false, + playedToEnd: false, + item: item, + nextItem: nil, + previousItem: nil, + order: self.order, + looping: self.looping + ))) + } +}