mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Added recent stickers clearing Added sending logs via email Added forward recipient change on forward acccessory panel tap Tweaked undo panel design Various UI fixes
1068 lines
41 KiB
Swift
1068 lines
41 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import CoreMedia
|
|
import TelegramCore
|
|
|
|
private let traceEvents = false
|
|
|
|
private struct MediaPlayerControlTimebase {
|
|
let timebase: CMTimebase
|
|
let isAudio: Bool
|
|
}
|
|
|
|
private enum MediaPlayerPlaybackAction {
|
|
case play
|
|
case pause
|
|
}
|
|
|
|
private final class MediaPlayerLoadedState {
|
|
let frameSource: MediaFrameSource
|
|
let mediaBuffers: MediaPlaybackBuffers
|
|
let controlTimebase: MediaPlayerControlTimebase
|
|
var extraVideoFrames: ([MediaTrackFrame], CMTime)?
|
|
var lostAudioSession: Bool = false
|
|
|
|
init(frameSource: MediaFrameSource, mediaBuffers: MediaPlaybackBuffers, controlTimebase: MediaPlayerControlTimebase) {
|
|
self.frameSource = frameSource
|
|
self.mediaBuffers = mediaBuffers
|
|
self.controlTimebase = controlTimebase
|
|
}
|
|
}
|
|
|
|
private struct MediaPlayerSeekState {
|
|
let duration: Double
|
|
}
|
|
|
|
private enum MediaPlayerState {
|
|
case empty
|
|
case seeking(frameSource: MediaFrameSource, timestamp: Double, seekState: MediaPlayerSeekState?, disposable: Disposable, action: MediaPlayerPlaybackAction, enableSound: Bool)
|
|
case paused(MediaPlayerLoadedState)
|
|
case playing(MediaPlayerLoadedState)
|
|
}
|
|
|
|
enum MediaPlayerActionAtEnd {
|
|
case loop((() -> Void)?)
|
|
case action(() -> Void)
|
|
case loopDisablingSound(() -> Void)
|
|
case stop
|
|
}
|
|
|
|
enum MediaPlayerPlayOnceWithSoundActionAtEnd {
|
|
case loop
|
|
case loopDisablingSound
|
|
case stop
|
|
case repeatIfNeeded
|
|
}
|
|
|
|
enum MediaPlayerSeek {
|
|
case none
|
|
case start
|
|
case automatic
|
|
case timecode(Double)
|
|
}
|
|
|
|
enum MediaPlayerStreaming {
|
|
case none
|
|
case conservative
|
|
case earlierStart
|
|
|
|
var enabled: Bool {
|
|
if case .none = self {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
var parameters: (Double, Double, Double) {
|
|
switch self {
|
|
case .none, .conservative:
|
|
return (1.0, 2.0, 3.0)
|
|
case .earlierStart:
|
|
return (1.0, 1.0, 2.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class MediaPlayerAudioRendererContext {
|
|
let renderer: MediaPlayerAudioRenderer
|
|
var requestedFrames = false
|
|
|
|
init(renderer: MediaPlayerAudioRenderer) {
|
|
self.renderer = renderer
|
|
}
|
|
}
|
|
|
|
private final class MediaPlayerContext {
|
|
private let queue: Queue
|
|
private let audioSessionManager: ManagedAudioSession
|
|
|
|
private let postbox: Postbox
|
|
private let resourceReference: MediaResourceReference
|
|
private let tempFilePath: String?
|
|
private let streamable: MediaPlayerStreaming
|
|
private let video: Bool
|
|
private let preferSoftwareDecoding: Bool
|
|
private var enableSound: Bool
|
|
private var baseRate: Double
|
|
private let fetchAutomatically: Bool
|
|
private var playAndRecord: Bool
|
|
private var keepAudioSessionWhilePaused: Bool
|
|
private var continuePlayingWithoutSoundOnLostAudioSession: Bool
|
|
|
|
private var seekId: Int = 0
|
|
|
|
private var state: MediaPlayerState = .empty
|
|
private var audioRenderer: MediaPlayerAudioRendererContext?
|
|
private var forceAudioToSpeaker = false
|
|
fileprivate let videoRenderer: VideoPlayerProxy
|
|
|
|
private var tickTimer: SwiftSignalKit.Timer?
|
|
|
|
private var lastStatusUpdateTimestamp: Double?
|
|
private let playerStatus: Promise<MediaPlayerStatus>
|
|
|
|
fileprivate var actionAtEnd: MediaPlayerActionAtEnd = .stop
|
|
|
|
private var stoppedAtEnd = false
|
|
|
|
init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise<MediaPlayerStatus>, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) {
|
|
assert(queue.isCurrent())
|
|
|
|
self.queue = queue
|
|
self.audioSessionManager = audioSessionManager
|
|
self.playerStatus = playerStatus
|
|
self.postbox = postbox
|
|
self.resourceReference = resourceReference
|
|
self.tempFilePath = tempFilePath
|
|
self.streamable = streamable
|
|
self.video = video
|
|
self.preferSoftwareDecoding = preferSoftwareDecoding
|
|
self.enableSound = enableSound
|
|
self.baseRate = baseRate
|
|
self.fetchAutomatically = fetchAutomatically
|
|
self.playAndRecord = playAndRecord
|
|
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
|
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
|
|
|
|
self.videoRenderer = VideoPlayerProxy(queue: queue)
|
|
|
|
self.videoRenderer.visibilityUpdated = { [weak self] value in
|
|
assert(queue.isCurrent())
|
|
|
|
if let strongSelf = self, !strongSelf.enableSound || strongSelf.continuePlayingWithoutSoundOnLostAudioSession {
|
|
switch strongSelf.state {
|
|
case .empty:
|
|
if value && playAutomatically {
|
|
strongSelf.play()
|
|
}
|
|
case .paused:
|
|
if value {
|
|
strongSelf.play()
|
|
}
|
|
case .playing:
|
|
if !value {
|
|
strongSelf.pause(lostAudioSession: false)
|
|
}
|
|
case let .seeking(_, _, _, _, action, _):
|
|
switch action {
|
|
case .pause:
|
|
if value {
|
|
strongSelf.play()
|
|
}
|
|
case .play:
|
|
if !value {
|
|
strongSelf.pause(lostAudioSession: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.videoRenderer.takeFrameAndQueue = (queue, { [weak self] in
|
|
assert(queue.isCurrent())
|
|
|
|
if let strongSelf = self {
|
|
var maybeLoadedState: MediaPlayerLoadedState?
|
|
|
|
switch strongSelf.state {
|
|
case .empty:
|
|
return .noFrames
|
|
case let .paused(state):
|
|
maybeLoadedState = state
|
|
case let .playing(state):
|
|
maybeLoadedState = state
|
|
case .seeking:
|
|
return .noFrames
|
|
}
|
|
|
|
if let loadedState = maybeLoadedState, let videoBuffer = loadedState.mediaBuffers.videoBuffer {
|
|
if let (extraVideoFrames, atTime) = loadedState.extraVideoFrames {
|
|
loadedState.extraVideoFrames = nil
|
|
return .restoreState(extraVideoFrames, atTime)
|
|
} else {
|
|
return videoBuffer.takeFrame()
|
|
}
|
|
} else {
|
|
return .noFrames
|
|
}
|
|
} else {
|
|
return .noFrames
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
assert(self.queue.isCurrent())
|
|
|
|
self.tickTimer?.invalidate()
|
|
|
|
if case let .seeking(_, _, _, disposable, _, _) = self.state {
|
|
disposable.dispose()
|
|
}
|
|
}
|
|
|
|
fileprivate func seek(timestamp: Double) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
let action: MediaPlayerPlaybackAction
|
|
switch self.state {
|
|
case .empty, .paused:
|
|
action = .pause
|
|
case .playing:
|
|
action = .play
|
|
case let .seeking(_, _, _, _, currentAction, _):
|
|
action = currentAction
|
|
}
|
|
self.seek(timestamp: timestamp, action: action)
|
|
}
|
|
|
|
fileprivate func seek(timestamp: Double, action: MediaPlayerPlaybackAction) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
var loadedState: MediaPlayerLoadedState?
|
|
var seekState: MediaPlayerSeekState?
|
|
switch self.state {
|
|
case .empty:
|
|
break
|
|
case let .playing(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .paused(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .seeking(previousFrameSource, previousTimestamp, seekStateValue, previousDisposable, _, previousEnableSound):
|
|
if previousTimestamp.isEqual(to: timestamp) && self.enableSound == previousEnableSound {
|
|
self.state = .seeking(frameSource: previousFrameSource, timestamp: previousTimestamp, seekState: seekStateValue, disposable: previousDisposable, action: action, enableSound: self.enableSound)
|
|
return
|
|
} else {
|
|
seekState = seekStateValue
|
|
previousDisposable.dispose()
|
|
}
|
|
}
|
|
|
|
self.tickTimer?.invalidate()
|
|
var loadedDuration: Double?
|
|
if let loadedState = loadedState {
|
|
self.seekId += 1
|
|
|
|
if loadedState.controlTimebase.isAudio {
|
|
self.audioRenderer?.renderer.setRate(0.0)
|
|
} else {
|
|
if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: 0.0) {
|
|
CMTimebaseSetRate(loadedState.controlTimebase.timebase, 0.0)
|
|
}
|
|
}
|
|
let currentTimestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
|
var duration: Double = 0.0
|
|
var videoStatus: MediaTrackFrameBufferStatus?
|
|
if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer {
|
|
videoStatus = videoTrackFrameBuffer.status(at: currentTimestamp)
|
|
duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration))
|
|
}
|
|
|
|
var audioStatus: MediaTrackFrameBufferStatus?
|
|
if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer {
|
|
audioStatus = audioTrackFrameBuffer.status(at: currentTimestamp)
|
|
duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration))
|
|
}
|
|
loadedDuration = duration
|
|
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play), soundEnabled: self.enableSound)
|
|
self.playerStatus.set(.single(status))
|
|
} else {
|
|
let duration = seekState?.duration ?? 0.0
|
|
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play), soundEnabled: self.enableSound)
|
|
self.playerStatus.set(.single(status))
|
|
}
|
|
|
|
let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, streamable: self.streamable.enabled, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2)
|
|
let disposable = MetaDisposable()
|
|
let updatedSeekState: MediaPlayerSeekState?
|
|
if let loadedDuration = loadedDuration {
|
|
updatedSeekState = MediaPlayerSeekState(duration: loadedDuration)
|
|
} else {
|
|
updatedSeekState = seekState
|
|
}
|
|
self.state = .seeking(frameSource: frameSource, timestamp: timestamp, seekState: updatedSeekState, disposable: disposable, action: action, enableSound: self.enableSound)
|
|
|
|
self.lastStatusUpdateTimestamp = nil
|
|
|
|
let seekResult = frameSource.seek(timestamp: timestamp)
|
|
|> deliverOn(self.queue)
|
|
|
|
disposable.set(seekResult.start(next: { [weak self] seekResult in
|
|
if let strongSelf = self {
|
|
var result: MediaFrameSourceSeekResult?
|
|
seekResult.with { object in
|
|
assert(strongSelf.queue.isCurrent())
|
|
result = object
|
|
}
|
|
if let result = result {
|
|
strongSelf.seekingCompleted(seekResult: result)
|
|
} else {
|
|
assertionFailure()
|
|
}
|
|
}
|
|
}, error: { _ in
|
|
}))
|
|
}
|
|
|
|
fileprivate func seekingCompleted(seekResult: MediaFrameSourceSeekResult) {
|
|
if traceEvents {
|
|
print("seekingCompleted at \(CMTimeGetSeconds(seekResult.timestamp))")
|
|
}
|
|
|
|
assert(self.queue.isCurrent())
|
|
|
|
guard case let .seeking(frameSource, _, _, _, action, _) = self.state else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
|
|
var buffers = seekResult.buffers
|
|
if !self.enableSound {
|
|
buffers = MediaPlaybackBuffers(audioBuffer: nil, videoBuffer: buffers.videoBuffer)
|
|
}
|
|
|
|
buffers.audioBuffer?.statusUpdated = { [weak self] in
|
|
self?.tick()
|
|
}
|
|
buffers.videoBuffer?.statusUpdated = { [weak self] in
|
|
self?.tick()
|
|
}
|
|
let controlTimebase: MediaPlayerControlTimebase
|
|
|
|
if let _ = buffers.audioBuffer {
|
|
let renderer: MediaPlayerAudioRenderer
|
|
if let currentRenderer = self.audioRenderer, !currentRenderer.requestedFrames {
|
|
renderer = currentRenderer.renderer
|
|
} else {
|
|
self.audioRenderer?.renderer.stop()
|
|
self.audioRenderer = nil
|
|
|
|
let queue = self.queue
|
|
renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, updatedRate: { [weak self] in
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.tick()
|
|
}
|
|
}
|
|
}, audioPaused: { [weak self] in
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
if strongSelf.enableSound {
|
|
if strongSelf.continuePlayingWithoutSoundOnLostAudioSession {
|
|
strongSelf.continuePlayingWithoutSound()
|
|
} else {
|
|
strongSelf.pause(lostAudioSession: true)
|
|
}
|
|
} else {
|
|
strongSelf.seek(timestamp: 0.0, action: .play)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer)
|
|
renderer.start()
|
|
}
|
|
|
|
controlTimebase = MediaPlayerControlTimebase(timebase: renderer.audioTimebase, isAudio: true)
|
|
} else {
|
|
self.audioRenderer?.renderer.stop()
|
|
self.audioRenderer = nil
|
|
|
|
var timebase: CMTimebase?
|
|
CMTimebaseCreateWithMasterClock(nil, CMClockGetHostTimeClock(), &timebase)
|
|
controlTimebase = MediaPlayerControlTimebase(timebase: timebase!, isAudio: false)
|
|
CMTimebaseSetTime(timebase!, seekResult.timestamp)
|
|
}
|
|
|
|
let loadedState = MediaPlayerLoadedState(frameSource: frameSource, mediaBuffers: buffers, controlTimebase: controlTimebase)
|
|
loadedState.extraVideoFrames = (seekResult.extraDecodedVideoFrames, seekResult.timestamp)
|
|
|
|
if let audioRenderer = self.audioRenderer?.renderer {
|
|
let queue = self.queue
|
|
audioRenderer.flushBuffers(at: seekResult.timestamp, completion: { [weak self] in
|
|
queue.async { [weak self] in
|
|
if let strongSelf = self {
|
|
switch action {
|
|
case .play:
|
|
strongSelf.state = .playing(loadedState)
|
|
strongSelf.audioRenderer?.renderer.start()
|
|
case .pause:
|
|
strongSelf.state = .paused(loadedState)
|
|
}
|
|
|
|
strongSelf.lastStatusUpdateTimestamp = nil
|
|
strongSelf.tick()
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
switch action {
|
|
case .play:
|
|
self.state = .playing(loadedState)
|
|
case .pause:
|
|
self.state = .paused(loadedState)
|
|
}
|
|
|
|
self.lastStatusUpdateTimestamp = nil
|
|
self.tick()
|
|
}
|
|
}
|
|
|
|
fileprivate func play() {
|
|
assert(self.queue.isCurrent())
|
|
|
|
switch self.state {
|
|
case .empty:
|
|
self.lastStatusUpdateTimestamp = nil
|
|
if self.enableSound {
|
|
let queue = self.queue
|
|
let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, updatedRate: { [weak self] in
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.tick()
|
|
}
|
|
}
|
|
}, audioPaused: { [weak self] in
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
if strongSelf.enableSound {
|
|
if strongSelf.continuePlayingWithoutSoundOnLostAudioSession {
|
|
strongSelf.continuePlayingWithoutSound()
|
|
} else {
|
|
strongSelf.pause(lostAudioSession: true)
|
|
}
|
|
} else {
|
|
strongSelf.seek(timestamp: 0.0, action: .play)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer)
|
|
renderer.start()
|
|
}
|
|
self.seek(timestamp: 0.0, action: .play)
|
|
case let .seeking(frameSource, timestamp, seekState, disposable, _, enableSound):
|
|
self.state = .seeking(frameSource: frameSource, timestamp: timestamp, seekState: seekState, disposable: disposable, action: .play, enableSound: enableSound)
|
|
self.lastStatusUpdateTimestamp = nil
|
|
case let .paused(loadedState):
|
|
if loadedState.lostAudioSession {
|
|
let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
|
self.seek(timestamp: timestamp, action: .play)
|
|
} else {
|
|
self.lastStatusUpdateTimestamp = nil
|
|
if self.stoppedAtEnd {
|
|
self.seek(timestamp: 0.0, action: .play)
|
|
} else {
|
|
self.state = .playing(loadedState)
|
|
self.tick()
|
|
}
|
|
}
|
|
case .playing:
|
|
break
|
|
}
|
|
}
|
|
|
|
fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
if !self.enableSound {
|
|
self.lastStatusUpdateTimestamp = nil
|
|
self.enableSound = true
|
|
self.playAndRecord = playAndRecord
|
|
|
|
var loadedState: MediaPlayerLoadedState?
|
|
switch self.state {
|
|
case .empty:
|
|
break
|
|
case let .playing(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .paused(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .seeking(_, timestamp, _, disposable, action, _):
|
|
self.state = .empty
|
|
disposable.dispose()
|
|
self.seek(timestamp: timestamp, action: action)
|
|
}
|
|
|
|
var timestamp: Double
|
|
if case let .timecode(time) = seek {
|
|
timestamp = time
|
|
}
|
|
else if let loadedState = loadedState, case .none = seek {
|
|
timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
|
if let duration = self.currentDuration() {
|
|
if timestamp > duration - 2.0 {
|
|
timestamp = 0.0
|
|
}
|
|
}
|
|
} else {
|
|
timestamp = 0.0
|
|
}
|
|
self.seek(timestamp: timestamp, action: .play)
|
|
} else {
|
|
if case let .timecode(time) = seek {
|
|
self.seek(timestamp: Double(time), action: .play)
|
|
}
|
|
else if case .playing = self.state {
|
|
} else {
|
|
self.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func continuePlayingWithoutSound() {
|
|
if self.enableSound {
|
|
self.lastStatusUpdateTimestamp = nil
|
|
|
|
var loadedState: MediaPlayerLoadedState?
|
|
switch self.state {
|
|
case .empty:
|
|
break
|
|
case let .playing(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .paused(currentLoadedState):
|
|
loadedState = currentLoadedState
|
|
case let .seeking(_, timestamp, _, disposable, action, _):
|
|
if self.enableSound {
|
|
self.state = .empty
|
|
disposable.dispose()
|
|
self.enableSound = false
|
|
self.seek(timestamp: timestamp, action: action)
|
|
}
|
|
}
|
|
|
|
if let loadedState = loadedState {
|
|
self.enableSound = false
|
|
self.playAndRecord = false
|
|
|
|
var timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
|
if let duration = self.currentDuration(), timestamp > duration - 2.0 {
|
|
timestamp = 0.0
|
|
}
|
|
self.seek(timestamp: timestamp, action: .play)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
|
if self.continuePlayingWithoutSoundOnLostAudioSession != value {
|
|
self.continuePlayingWithoutSoundOnLostAudioSession = value
|
|
}
|
|
}
|
|
|
|
fileprivate func setBaseRate(_ baseRate: Double) {
|
|
self.baseRate = baseRate
|
|
self.lastStatusUpdateTimestamp = nil
|
|
self.tick()
|
|
self.audioRenderer?.renderer.setBaseRate(baseRate)
|
|
}
|
|
|
|
fileprivate func setForceAudioToSpeaker(_ value: Bool) {
|
|
if self.forceAudioToSpeaker != value {
|
|
self.forceAudioToSpeaker = value
|
|
|
|
self.audioRenderer?.renderer.setForceAudioToSpeaker(value)
|
|
}
|
|
}
|
|
|
|
fileprivate func setKeepAudioSessionWhilePaused(_ value: Bool) {
|
|
if self.keepAudioSessionWhilePaused != value {
|
|
self.keepAudioSessionWhilePaused = value
|
|
|
|
var isPlaying = false
|
|
switch self.state {
|
|
case .playing:
|
|
isPlaying = true
|
|
case let .seeking(_, _, _, _, action, _):
|
|
switch action {
|
|
case .play:
|
|
isPlaying = true
|
|
default:
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
if value && !isPlaying {
|
|
self.audioRenderer?.renderer.stop()
|
|
} else {
|
|
self.audioRenderer?.renderer.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func pause(lostAudioSession: Bool) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
switch self.state {
|
|
case .empty:
|
|
break
|
|
case let .seeking(frameSource, timestamp, seekState, disposable, _, enableSound):
|
|
self.state = .seeking(frameSource: frameSource, timestamp: timestamp, seekState: seekState, disposable: disposable, action: .pause, enableSound: enableSound)
|
|
self.lastStatusUpdateTimestamp = nil
|
|
case let .paused(loadedState):
|
|
if lostAudioSession {
|
|
loadedState.lostAudioSession = true
|
|
}
|
|
case let .playing(loadedState):
|
|
if lostAudioSession {
|
|
loadedState.lostAudioSession = true
|
|
}
|
|
self.state = .paused(loadedState)
|
|
self.lastStatusUpdateTimestamp = nil
|
|
self.tick()
|
|
}
|
|
}
|
|
|
|
fileprivate func togglePlayPause() {
|
|
assert(self.queue.isCurrent())
|
|
|
|
switch self.state {
|
|
case .empty:
|
|
self.play()
|
|
case let .seeking(_, _, _, _, action, _):
|
|
switch action {
|
|
case .play:
|
|
self.pause(lostAudioSession: false)
|
|
case .pause:
|
|
self.play()
|
|
}
|
|
case .paused:
|
|
if !self.enableSound {
|
|
self.playOnceWithSound(playAndRecord: false, seek: .none)
|
|
} else {
|
|
self.play()
|
|
}
|
|
case .playing:
|
|
self.pause(lostAudioSession: false)
|
|
}
|
|
}
|
|
|
|
private func currentDuration() -> Double? {
|
|
var maybeLoadedState: MediaPlayerLoadedState?
|
|
switch self.state {
|
|
case let .paused(state):
|
|
maybeLoadedState = state
|
|
case let .playing(state):
|
|
maybeLoadedState = state
|
|
default:
|
|
break
|
|
}
|
|
|
|
guard let loadedState = maybeLoadedState else {
|
|
return nil
|
|
}
|
|
|
|
var duration: Double = 0.0
|
|
if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer {
|
|
duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration))
|
|
}
|
|
|
|
if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer {
|
|
duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration))
|
|
}
|
|
return duration
|
|
}
|
|
|
|
private func tick() {
|
|
self.tickTimer?.invalidate()
|
|
|
|
var maybeLoadedState: MediaPlayerLoadedState?
|
|
|
|
switch self.state {
|
|
case .empty:
|
|
return
|
|
case let .paused(state):
|
|
maybeLoadedState = state
|
|
case let .playing(state):
|
|
maybeLoadedState = state
|
|
case .seeking:
|
|
return
|
|
}
|
|
|
|
guard let loadedState = maybeLoadedState else {
|
|
return
|
|
}
|
|
|
|
let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
|
if traceEvents {
|
|
print("tick at \(timestamp)")
|
|
}
|
|
|
|
var duration: Double = 0.0
|
|
var videoStatus: MediaTrackFrameBufferStatus?
|
|
if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer {
|
|
videoStatus = videoTrackFrameBuffer.status(at: timestamp)
|
|
duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration))
|
|
}
|
|
|
|
var audioStatus: MediaTrackFrameBufferStatus?
|
|
if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer {
|
|
audioStatus = audioTrackFrameBuffer.status(at: timestamp)
|
|
duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration))
|
|
}
|
|
|
|
var performActionAtEndNow = false
|
|
|
|
var worstStatus: MediaTrackFrameBufferStatus?
|
|
for status in [videoStatus, audioStatus] {
|
|
if let status = status {
|
|
if let worst = worstStatus {
|
|
switch status {
|
|
case .buffering:
|
|
worstStatus = status
|
|
case let .full(currentFullUntil):
|
|
switch worst {
|
|
case .buffering:
|
|
worstStatus = worst
|
|
case let .full(worstFullUntil):
|
|
if currentFullUntil < worstFullUntil {
|
|
worstStatus = status
|
|
} else {
|
|
worstStatus = worst
|
|
}
|
|
case .finished:
|
|
worstStatus = status
|
|
}
|
|
case let .finished(currentFinishedAt):
|
|
switch worst {
|
|
case .buffering, .full:
|
|
worstStatus = worst
|
|
case let .finished(worstFinishedAt):
|
|
if currentFinishedAt < worstFinishedAt {
|
|
worstStatus = worst
|
|
} else {
|
|
worstStatus = status
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
worstStatus = status
|
|
}
|
|
}
|
|
}
|
|
|
|
var rate: Double
|
|
var buffering = false
|
|
|
|
if let worstStatus = worstStatus, case let .full(fullUntil) = worstStatus, fullUntil.isFinite {
|
|
if case .playing = self.state {
|
|
rate = self.baseRate
|
|
|
|
let nextTickDelay = max(0.0, fullUntil - timestamp) / self.baseRate
|
|
let tickTimer = SwiftSignalKit.Timer(timeout: nextTickDelay, repeat: false, completion: { [weak self] in
|
|
self?.tick()
|
|
}, queue: self.queue)
|
|
self.tickTimer = tickTimer
|
|
tickTimer.start()
|
|
} else {
|
|
rate = 0.0
|
|
}
|
|
} else if let worstStatus = worstStatus, case let .finished(finishedAt) = worstStatus, finishedAt.isFinite {
|
|
let nextTickDelay = max(0.0, finishedAt - timestamp) / self.baseRate
|
|
if nextTickDelay.isLessThanOrEqualTo(0.0) {
|
|
rate = 0.0
|
|
performActionAtEndNow = true
|
|
} else {
|
|
if case .playing = self.state {
|
|
rate = self.baseRate
|
|
|
|
let tickTimer = SwiftSignalKit.Timer(timeout: nextTickDelay, repeat: false, completion: { [weak self] in
|
|
self?.tick()
|
|
}, queue: self.queue)
|
|
self.tickTimer = tickTimer
|
|
tickTimer.start()
|
|
} else {
|
|
rate = 0.0
|
|
}
|
|
}
|
|
} else {
|
|
buffering = true
|
|
rate = 0.0
|
|
}
|
|
|
|
var reportRate = rate
|
|
|
|
if loadedState.controlTimebase.isAudio {
|
|
if !rate.isZero {
|
|
self.audioRenderer?.renderer.start()
|
|
}
|
|
self.audioRenderer?.renderer.setRate(rate)
|
|
if !rate.isZero, let audioRenderer = self.audioRenderer {
|
|
let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase)
|
|
if !timebaseRate.isEqual(to: rate) {
|
|
reportRate = timebaseRate
|
|
}
|
|
}
|
|
} else {
|
|
if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: rate) {
|
|
CMTimebaseSetRate(loadedState.controlTimebase.timebase, rate)
|
|
}
|
|
}
|
|
|
|
if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer, videoTrackFrameBuffer.hasFrames {
|
|
self.videoRenderer.state = (loadedState.controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect)
|
|
}
|
|
|
|
if let audioRenderer = self.audioRenderer, let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer, audioTrackFrameBuffer.hasFrames {
|
|
let queue = self.queue
|
|
audioRenderer.requestedFrames = true
|
|
audioRenderer.renderer.beginRequestingFrames(queue: queue.queue, takeFrame: { [weak audioTrackFrameBuffer] in
|
|
assert(queue.isCurrent())
|
|
if let audioTrackFrameBuffer = audioTrackFrameBuffer {
|
|
return audioTrackFrameBuffer.takeFrame()
|
|
} else {
|
|
return .noFrames
|
|
}
|
|
})
|
|
}
|
|
|
|
var statusTimestamp = CACurrentMediaTime()
|
|
let playbackStatus: MediaPlayerPlaybackStatus
|
|
if buffering {
|
|
var whilePlaying = false
|
|
if case .playing = self.state {
|
|
whilePlaying = true
|
|
}
|
|
playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying)
|
|
} else if !rate.isZero {
|
|
if reportRate.isZero {
|
|
//playbackStatus = .buffering(initial: false, whilePlaying: true)
|
|
playbackStatus = .playing
|
|
statusTimestamp = 0.0
|
|
} else {
|
|
playbackStatus = .playing
|
|
}
|
|
} else {
|
|
playbackStatus = .paused
|
|
}
|
|
if self.lastStatusUpdateTimestamp == nil || self.lastStatusUpdateTimestamp! < statusTimestamp + 500 {
|
|
lastStatusUpdateTimestamp = statusTimestamp
|
|
var reportTimestamp = timestamp
|
|
if case .seeking(_, timestamp, _, _, _, _) = self.state {
|
|
reportTimestamp = timestamp
|
|
}
|
|
let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, dimensions: CGSize(), timestamp: min(max(reportTimestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: playbackStatus, soundEnabled: self.enableSound)
|
|
self.playerStatus.set(.single(status))
|
|
}
|
|
|
|
if performActionAtEndNow {
|
|
switch self.actionAtEnd {
|
|
case let .loop(f):
|
|
self.stoppedAtEnd = false
|
|
self.seek(timestamp: 0.0, action: .play)
|
|
f?()
|
|
case .stop:
|
|
self.stoppedAtEnd = true
|
|
self.pause(lostAudioSession: false)
|
|
case let .action(f):
|
|
self.stoppedAtEnd = true
|
|
self.pause(lostAudioSession: false)
|
|
f()
|
|
case let .loopDisablingSound(f):
|
|
self.stoppedAtEnd = false
|
|
self.enableSound = false
|
|
self.seek(timestamp: 0.0, action: .play)
|
|
f()
|
|
}
|
|
} else {
|
|
self.stoppedAtEnd = false
|
|
}
|
|
}
|
|
}
|
|
|
|
enum MediaPlayerPlaybackStatus: Equatable {
|
|
case playing
|
|
case paused
|
|
case buffering(initial: Bool, whilePlaying: Bool)
|
|
|
|
static func ==(lhs: MediaPlayerPlaybackStatus, rhs: MediaPlayerPlaybackStatus) -> Bool {
|
|
switch lhs {
|
|
case .playing:
|
|
if case .playing = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .paused:
|
|
if case .paused = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .buffering(initial, whilePlaying):
|
|
if case .buffering(initial, whilePlaying) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MediaPlayerStatus: Equatable {
|
|
let generationTimestamp: Double
|
|
let duration: Double
|
|
let dimensions: CGSize
|
|
let timestamp: Double
|
|
let baseRate: Double
|
|
let seekId: Int
|
|
let status: MediaPlayerPlaybackStatus
|
|
let soundEnabled: Bool
|
|
}
|
|
|
|
final class MediaPlayer {
|
|
private let queue = Queue()
|
|
private var contextRef: Unmanaged<MediaPlayerContext>?
|
|
|
|
private let statusValue = Promise<MediaPlayerStatus>()
|
|
|
|
var status: Signal<MediaPlayerStatus, NoError> {
|
|
return self.statusValue.get()
|
|
}
|
|
|
|
var actionAtEnd: MediaPlayerActionAtEnd = .stop {
|
|
didSet {
|
|
let value = self.actionAtEnd
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.actionAtEnd = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = true, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) {
|
|
self.queue.async {
|
|
let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession)
|
|
self.contextRef = Unmanaged.passRetained(context)
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
let contextRef = self.contextRef
|
|
self.queue.async {
|
|
contextRef?.release()
|
|
}
|
|
}
|
|
|
|
func play() {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
|
}
|
|
}
|
|
}
|
|
|
|
func continuePlayingWithoutSound() {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.continuePlayingWithoutSound()
|
|
}
|
|
}
|
|
}
|
|
|
|
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.setContinuePlayingWithoutSoundOnLostAudioSession(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setForceAudioToSpeaker(_ value: Bool) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.setForceAudioToSpeaker(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setKeepAudioSessionWhilePaused(_ value: Bool) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.setKeepAudioSessionWhilePaused(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pause() {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.pause(lostAudioSession: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func togglePlayPause() {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.togglePlayPause()
|
|
}
|
|
}
|
|
}
|
|
|
|
func seek(timestamp: Double, play: Bool? = nil) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
if let play = play {
|
|
context.seek(timestamp: timestamp, action: play ? .play : .pause)
|
|
} else {
|
|
context.seek(timestamp: timestamp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func setBaseRate(_ baseRate: Double) {
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.setBaseRate(baseRate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func attachPlayerNode(_ node: MediaPlayerNode) {
|
|
let nodeRef: Unmanaged<MediaPlayerNode> = Unmanaged.passRetained(node)
|
|
self.queue.async {
|
|
if let context = self.contextRef?.takeUnretainedValue() {
|
|
context.videoRenderer.attachNodeAndRelease(nodeRef)
|
|
} else {
|
|
Queue.mainQueue().async {
|
|
nodeRef.release()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|