Swiftgram/submodules/TelegramUI/Sources/SharedMediaPlayer.swift

519 lines
23 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import UniversalMediaPlayer
import TelegramAudio
import AccountContext
import TelegramUniversalVideoContent
import DeviceProximity
private enum SharedMediaPlaybackItem: Equatable {
case audio(MediaPlayer)
case instantVideo(OverlayInstantVideoNode)
var playbackStatus: Signal<MediaPlayerStatus, NoError> {
switch self {
case let .audio(player):
return player.status
case let .instantVideo(node):
return node.status |> map { status in
if let status = status {
return status
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
}
}
}
static func ==(lhs: SharedMediaPlaybackItem, rhs: SharedMediaPlaybackItem) -> Bool {
switch lhs {
case let .audio(lhsPlayer):
if case let .audio(rhsPlayer) = rhs, lhsPlayer === rhsPlayer {
return true
} else {
return false
}
case let .instantVideo(lhsNode):
if case let .instantVideo(rhsNode) = rhs, lhsNode === rhsNode {
return true
} else {
return false
}
}
}
func setActionAtEnd(_ f: @escaping () -> Void) {
switch self {
case let .audio(player):
player.actionAtEnd = .action(f)
case let .instantVideo(node):
node.playbackEnded = f
}
}
func play() {
switch self {
case let .audio(player):
player.play()
case let .instantVideo(node):
node.play()
}
}
func pause() {
switch self {
case let .audio(player):
player.pause()
case let .instantVideo(node):
node.pause()
}
}
func togglePlayPause() {
switch self {
case let .audio(player):
player.togglePlayPause(faded: true)
case let .instantVideo(node):
node.togglePlayPause()
}
}
func seek(_ timestamp: Double) {
switch self {
case let .audio(player):
player.seek(timestamp: timestamp)
case let .instantVideo(node):
node.seek(timestamp)
}
}
func setSoundEnabled(_ value: Bool) {
switch self {
case .audio:
break
case let .instantVideo(node):
node.setSoundEnabled(value)
}
}
func setForceAudioToSpeaker(_ value: Bool) {
switch self {
case let .audio(player):
player.setForceAudioToSpeaker(value)
case let .instantVideo(node):
node.setForceAudioToSpeaker(value)
}
}
}
final class SharedMediaPlayer {
private weak var mediaManager: MediaManager?
let account: Account
private let audioSession: ManagedAudioSession
private let overlayMediaManager: OverlayMediaManager
private let playerIndex: Int32
let playlist: SharedMediaPlaylist
private var playbackRate: AudioPlaybackRate
private var proximityManagerIndex: Int?
private let controlPlaybackWithProximity: Bool
private var forceAudioToSpeaker = false
private var stateDisposable: Disposable?
private var stateValue: SharedMediaPlaylistState? {
didSet {
if self.stateValue != oldValue {
self.state.set(.single(self.stateValue))
}
}
}
private let state = Promise<SharedMediaPlaylistState?>(nil)
private var playbackStateValueDisposable: Disposable?
private var _playbackStateValue: SharedMediaPlayerState?
private let playbackStateValue = Promise<SharedMediaPlayerState?>()
var playbackState: Signal<SharedMediaPlayerState?, NoError> {
return self.playbackStateValue.get()
}
private let audioLevelPipe = ValuePipe<Float>()
var audioLevel: Signal<Float, NoError> {
return self.audioLevelPipe.signal()
}
private let audioLevelDisposable = MetaDisposable()
private var playbackItem: SharedMediaPlaybackItem? {
didSet {
if playbackItem != oldValue {
switch playbackItem {
case let .audio(player):
let audioLevelPipe = self.audioLevelPipe
self.audioLevelDisposable.set((player.audioLevelEvents.start(next: { [weak audioLevelPipe] value in
audioLevelPipe?.putNext(value)
})))
default:
self.audioLevelDisposable.set(nil)
}
}
}
}
private var currentPlayedToEnd = false
private var scheduledPlaybackAction: SharedMediaPlayerPlaybackControlAction?
private var scheduledStartTime: Double?
private let markItemAsPlayedDisposable = MetaDisposable()
var playedToEnd: (() -> Void)?
var cancelled: (() -> Void)?
private var inForegroundDisposable: Disposable?
private var currentPrefetchItems: (SharedMediaPlaybackDataSource, SharedMediaPlaybackDataSource)?
private let prefetchDisposable = MetaDisposable()
let type: MediaManagerPlayerType
init(mediaManager: MediaManager, inForeground: Signal<Bool, NoError>, account: Account, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, initialPlaybackRate: AudioPlaybackRate, playerIndex: Int32, controlPlaybackWithProximity: Bool, type: MediaManagerPlayerType, continueInstantVideoLoopAfterFinish: Bool) {
self.mediaManager = mediaManager
self.account = account
self.audioSession = audioSession
self.overlayMediaManager = overlayMediaManager
playlist.setOrder(initialOrder)
playlist.setLooping(initialLooping)
self.playlist = playlist
self.playerIndex = playerIndex
self.playbackRate = initialPlaybackRate
self.controlPlaybackWithProximity = controlPlaybackWithProximity
self.type = type
if controlPlaybackWithProximity {
self.forceAudioToSpeaker = !DeviceProximityManager.shared().currentValue()
} else {
self.forceAudioToSpeaker = true
}
playlist.currentItemDisappeared = { [weak self] in
self?.cancelled?()
}
self.stateDisposable = (playlist.state
|> deliverOnMainQueue).start(next: { [weak self] state in
if let strongSelf = self {
let previousPlaybackItem = strongSelf.playbackItem
strongSelf.updatePrefetchItems(item: state.item, previousItem: state.previousItem, nextItem: state.nextItem, ordering: state.order)
if state.item?.playbackData != strongSelf.stateValue?.item?.playbackData {
if let playbackItem = strongSelf.playbackItem {
switch playbackItem {
case .audio:
playbackItem.pause()
case let .instantVideo(node):
node.setSoundEnabled(false)
strongSelf.overlayMediaManager.controller?.removeNode(node, customTransition: false)
}
}
strongSelf.playbackItem = nil
if let item = state.item, let playbackData = item.playbackData {
let rateValue: Double
if case .music = playbackData.type {
rateValue = 1.0
} else {
rateValue = strongSelf.playbackRate.doubleValue
}
switch playbackData.type {
case .voice, .music:
switch playbackData.source {
case let .telegramFile(fileReference, _):
strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice))
}
case .instantVideo:
if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem {
switch playbackData.source {
case let .telegramFile(fileReference, _):
let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in
mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause))
})
strongSelf.playbackItem = .instantVideo(videoNode)
videoNode.setSoundEnabled(true)
videoNode.setBaseRate(rateValue)
}
}
}
}
if let playbackItem = strongSelf.playbackItem {
playbackItem.setForceAudioToSpeaker(strongSelf.forceAudioToSpeaker)
playbackItem.setActionAtEnd({
Queue.mainQueue().async {
if let strongSelf = self {
switch strongSelf.playlist.looping {
case .item:
strongSelf.playbackItem?.seek(0.0)
strongSelf.playbackItem?.play()
default:
strongSelf.scheduledPlaybackAction = .play
strongSelf.control(.next)
}
}
}
})
switch playbackItem {
case .audio:
break
case let .instantVideo(node):
strongSelf.overlayMediaManager.controller?.addNode(node, customTransition: false)
}
if let scheduledPlaybackAction = strongSelf.scheduledPlaybackAction {
strongSelf.scheduledPlaybackAction = nil
let scheduledStartTime = strongSelf.scheduledStartTime
strongSelf.scheduledStartTime = nil
switch scheduledPlaybackAction {
case .play:
switch playbackItem {
case let .audio(player):
if let scheduledStartTime = scheduledStartTime {
player.seek(timestamp: scheduledStartTime)
player.play()
} else {
player.play()
}
case let .instantVideo(node):
if let scheduledStartTime = scheduledStartTime {
node.seek(scheduledStartTime)
node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity)
} else {
node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity)
}
}
case .pause:
playbackItem.pause()
case .togglePlayPause:
playbackItem.togglePlayPause()
}
}
}
}
if strongSelf.currentPlayedToEnd != state.playedToEnd {
strongSelf.currentPlayedToEnd = state.playedToEnd
if state.playedToEnd {
if let playbackItem = strongSelf.playbackItem {
switch playbackItem {
case let .audio(player):
player.pause()
case let .instantVideo(node):
if continueInstantVideoLoopAfterFinish {
node.setSoundEnabled(false)
} else {
node.pause()
}
}
}
strongSelf.playedToEnd?()
}
}
let updatePlaybackState = strongSelf.stateValue != state || strongSelf.playbackItem != previousPlaybackItem
strongSelf.stateValue = state
if updatePlaybackState {
let playlistId = strongSelf.playlist.id
let playlistLocation = strongSelf.playlist.location
let playerIndex = strongSelf.playerIndex
if let playbackItem = strongSelf.playbackItem, let item = state.item {
strongSelf.playbackStateValue.set(playbackItem.playbackStatus
|> map { itemStatus in
return .item(SharedMediaPlayerItemPlaybackState(playlistId: playlistId, playlistLocation: playlistLocation, item: item, previousItem: state.previousItem, nextItem: state.nextItem, status: itemStatus, order: state.order, looping: state.looping, playerIndex: playerIndex))
})
strongSelf.markItemAsPlayedDisposable.set((playbackItem.playbackStatus
|> filter { status in
if case .playing = status.status {
return true
} else {
return false
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { next in
if let strongSelf = self {
strongSelf.playlist.onItemPlaybackStarted(item)
}
}))
} else {
if state.item != nil || state.loading {
strongSelf.playbackStateValue.set(.single(.loading))
} else {
strongSelf.playbackStateValue.set(.single(nil))
if !state.loading {
if let proximityManagerIndex = strongSelf.proximityManagerIndex {
DeviceProximityManager.shared().remove(proximityManagerIndex)
}
}
}
}
}
}
})
self.playbackStateValueDisposable = (self.playbackState
|> deliverOnMainQueue).start(next: { [weak self] value in
self?._playbackStateValue = value
})
if controlPlaybackWithProximity {
self.proximityManagerIndex = DeviceProximityManager.shared().add { [weak self] value in
let forceAudioToSpeaker = !value
if let strongSelf = self, strongSelf.forceAudioToSpeaker != forceAudioToSpeaker {
strongSelf.forceAudioToSpeaker = forceAudioToSpeaker
strongSelf.playbackItem?.setForceAudioToSpeaker(forceAudioToSpeaker)
if !forceAudioToSpeaker {
strongSelf.control(.playback(.play))
} else {
strongSelf.control(.playback(.pause))
}
}
}
}
}
deinit {
self.stateDisposable?.dispose()
self.markItemAsPlayedDisposable.dispose()
self.inForegroundDisposable?.dispose()
self.playbackStateValueDisposable?.dispose()
self.prefetchDisposable.dispose()
if let proximityManagerIndex = self.proximityManagerIndex {
DeviceProximityManager.shared().remove(proximityManagerIndex)
}
if let playbackItem = self.playbackItem {
switch playbackItem {
case .audio:
playbackItem.pause()
case let .instantVideo(node):
node.setSoundEnabled(false)
self.overlayMediaManager.controller?.removeNode(node, customTransition: false)
}
}
}
func control(_ action: SharedMediaPlayerControlAction) {
switch action {
case .next:
self.scheduledPlaybackAction = .play
self.playlist.control(.next)
case .previous:
let threshold: Double = 5.0
if let playbackStateValue = self._playbackStateValue, case let .item(item) = playbackStateValue, item.status.duration > threshold, item.status.timestamp > threshold {
self.control(.seek(0.0))
} else {
self.scheduledPlaybackAction = .play
self.playlist.control(.previous)
}
case let .playback(action):
if let playbackItem = self.playbackItem {
switch action {
case .play:
playbackItem.play()
case .pause:
playbackItem.pause()
case .togglePlayPause:
playbackItem.togglePlayPause()
}
} else {
self.scheduledPlaybackAction = action
}
case let .seek(timestamp):
if let playbackItem = self.playbackItem {
playbackItem.seek(timestamp)
} else {
self.scheduledPlaybackAction = .play
self.scheduledStartTime = timestamp
}
case let .setOrder(order):
self.playlist.setOrder(order)
case let .setLooping(looping):
self.playlist.setLooping(looping)
case let .setBaseRate(baseRate):
self.playbackRate = baseRate
if let playbackItem = self.playbackItem {
let rateValue: Double = baseRate.doubleValue
switch playbackItem {
case let .audio(player):
player.setBaseRate(rateValue)
case let .instantVideo(node):
node.setBaseRate(rateValue)
}
}
}
}
func stop() {
if let playbackItem = self.playbackItem {
switch playbackItem {
case let .audio(player):
player.pause()
case let .instantVideo(node):
node.setSoundEnabled(false)
}
}
}
private func updatePrefetchItems(item: SharedMediaPlaylistItem?, previousItem: SharedMediaPlaylistItem?, nextItem: SharedMediaPlaylistItem?, ordering: MusicPlaybackSettingsOrder) {
var prefetchItems: (SharedMediaPlaybackDataSource, SharedMediaPlaybackDataSource)?
if let playbackData = item?.playbackData {
switch ordering {
case .regular:
if let previousItem = previousItem?.playbackData {
prefetchItems = (playbackData.source, previousItem.source)
}
case .reversed:
if let nextItem = nextItem?.playbackData {
prefetchItems = (playbackData.source, nextItem.source)
}
case .random:
break
}
}
if self.currentPrefetchItems?.0 != prefetchItems?.0 || self.currentPrefetchItems?.1 != prefetchItems?.1 {
self.currentPrefetchItems = prefetchItems
if let (current, next) = prefetchItems {
let fetchedCurrentSignal: Signal<Never, NoError>
let fetchedNextSignal: Signal<Never, NoError>
switch current {
case let .telegramFile(file, _):
fetchedCurrentSignal = self.account.postbox.mediaBox.resourceData(file.media.resource)
|> mapToSignal { data -> Signal<Void, NoError> in
if data.complete {
return .single(Void())
} else {
return .complete()
}
}
|> take(1)
|> ignoreValues
}
switch next {
case let .telegramFile(file, _):
fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, userLocation: .other, userContentType: .audio, reference: file.resourceReference(file.media.resource))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
self.prefetchDisposable.set((fetchedCurrentSignal |> then(fetchedNextSignal)).start())
} else {
self.prefetchDisposable.set(nil)
}
}
}
}