Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,147 @@
import Foundation
import SwiftSignalKit
import Postbox
import CoreMedia
import TelegramCore
public final class ChunkMediaPlayerPart {
public enum Id: Hashable {
case tempFile(path: String)
case directStream
}
public struct DirectStream: Hashable {
public let index: Int
public let startPts: CMTime
public let endPts: CMTime
public let duration: Double
public init(index: Int, startPts: CMTime, endPts: CMTime, duration: Double) {
self.index = index
self.startPts = startPts
self.endPts = endPts
self.duration = duration
}
}
public final class TempFile {
public let file: TempBoxFile
public init(file: TempBoxFile) {
self.file = file
}
deinit {
//TempBox.shared.dispose(self.file)
}
}
public let startTime: Double
public let endTime: Double
public let content: TempFile
public let clippedStartTime: Double?
public let codecName: String?
public let offsetTime: Double
public var id: Id {
return .tempFile(path: self.content.file.path)
}
public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, content: TempFile, codecName: String?, offsetTime: Double) {
self.startTime = startTime
self.clippedStartTime = clippedStartTime
self.endTime = endTime
self.content = content
self.codecName = codecName
self.offsetTime = offsetTime
}
}
public final class ChunkMediaPlayerPartsState {
public final class DirectReader {
public struct Stream {
public let mediaBox: MediaBox
public let resource: MediaResource
public let size: Int64
public let index: Int
public let seek: (streamIndex: Int, pts: Int64)
public let maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
public let codecName: String?
public init(mediaBox: MediaBox, resource: MediaResource, size: Int64, index: Int, seek: (streamIndex: Int, pts: Int64), maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?, codecName: String?) {
self.mediaBox = mediaBox
self.resource = resource
self.size = size
self.index = index
self.seek = seek
self.maxReadablePts = maxReadablePts
self.codecName = codecName
}
}
public final class Impl {
public let video: Stream?
public let audio: Stream?
public init(video: Stream?, audio: Stream?) {
self.video = video
self.audio = audio
}
}
public let id: Int
public let seekPosition: Double
public let availableUntilPosition: Double
public let bufferedUntilEnd: Bool
public let impl: Impl?
public init(id: Int, seekPosition: Double, availableUntilPosition: Double, bufferedUntilEnd: Bool, impl: Impl?) {
self.id = id
self.seekPosition = seekPosition
self.availableUntilPosition = availableUntilPosition
self.bufferedUntilEnd = bufferedUntilEnd
self.impl = impl
}
}
public enum Content {
case parts([ChunkMediaPlayerPart])
case directReader(DirectReader)
}
public let duration: Double?
public let content: Content
public init(duration: Double?, content: Content) {
self.duration = duration
self.content = content
}
}
#if os(iOS)
import UIKit
import TelegramAudio
public protocol ChunkMediaPlayer: AnyObject {
var status: Signal<MediaPlayerStatus, NoError> { get }
var audioLevelEvents: Signal<Float, NoError> { get }
var actionAtEnd: MediaPlayerActionAtEnd { get set }
func play()
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek)
func setSoundMuted(soundMuted: Bool)
func continueWithOverridingAmbientMode(isAmbient: Bool)
func continuePlayingWithoutSound(seek: MediaPlayerSeek)
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
func setForceAudioToSpeaker(_ value: Bool)
func setKeepAudioSessionWhilePaused(_ value: Bool)
func pause()
func togglePlayPause(faded: Bool)
func seek(timestamp: Double, play: Bool?)
func setBaseRate(_ baseRate: Double)
}
#endif
@@ -0,0 +1,657 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import FFMpegBinding
import RangeSet
import CoreMedia
private func FFMpegLookaheadReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<FFMpegLookaheadReader>.fromOpaque(userData!).takeUnretainedValue()
let readCount = min(256 * 1024, Int64(bufferSize))
let requestRange: Range<Int64> = context.readingOffset ..< (context.readingOffset + readCount)
var fetchedData: Data?
let fetchDisposable = MetaDisposable()
let semaphore = DispatchSemaphore(value: 0)
let disposable = context.params.getDataInRange(requestRange, { data in
if let data {
fetchedData = data
semaphore.signal()
}
})
var isCancelled = false
let cancelDisposable = context.params.cancel.start(next: { _ in
isCancelled = true
semaphore.signal()
})
semaphore.wait()
if isCancelled {
context.isCancelled = true
}
disposable.dispose()
cancelDisposable.dispose()
fetchDisposable.dispose()
if let fetchedData = fetchedData {
fetchedData.withUnsafeBytes { byteBuffer -> Void in
guard let bytes = byteBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(buffer, bytes, fetchedData.count)
}
let fetchedCount = Int32(fetchedData.count)
//print("Fetched from \(context.readingOffset) (\(fetchedCount) bytes)")
context.setReadingOffset(offset: context.readingOffset + Int64(fetchedCount))
if fetchedCount == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return fetchedCount
} else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
private func FFMpegLookaheadReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegLookaheadReader>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return context.params.size
} else {
context.setReadingOffset(offset: offset)
return offset
}
}
private func range(_ outer: Range<Int64>, fullyContains inner: Range<Int64>) -> Bool {
return inner.lowerBound >= outer.lowerBound && inner.upperBound <= outer.upperBound
}
private final class FFMpegLookaheadReader {
let params: FFMpegLookaheadThread.Params
var avIoContext: FFMpegAVIOContext?
var avFormatContext: FFMpegAVFormatContext?
var audioStream: FFMpegFileReader.StreamInfo?
var videoStream: FFMpegFileReader.StreamInfo?
var seekInfo: FFMpegLookahead.State.Seek?
var maxReadPts: FFMpegLookahead.State.Seek?
var audioStreamState: FFMpegLookahead.StreamState?
var videoStreamState: FFMpegLookahead.StreamState?
var reportedState: FFMpegLookahead.State?
var readingOffset: Int64 = 0
var isCancelled: Bool = false
var isEnded: Bool = false
private var currentFetchRange: Range<Int64>?
private var currentFetchDisposable: Disposable?
var currentTimestamp: Double?
init?(params: FFMpegLookaheadThread.Params) {
self.params = params
let ioBufferSize = 64 * 1024
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegLookaheadReader_readPacketCallback, writePacket: nil, seek: FFMpegLookaheadReader_seekCallback, isSeekable: true) else {
return nil
}
self.avIoContext = avIoContext
let avFormatContext = FFMpegAVFormatContext()
avFormatContext.setIO(avIoContext)
self.setReadingOffset(offset: 0)
if !avFormatContext.openInput(withDirectFilePath: nil) {
return nil
}
if !avFormatContext.findStreamInfo() {
return nil
}
self.avFormatContext = avFormatContext
var audioStream: FFMpegFileReader.StreamInfo?
var videoStream: FFMpegFileReader.StreamInfo?
for streamType in 0 ..< 2 {
let isVideo = streamType == 0
for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime))
//let metrics = avFormatContext.metricsForStream(at: streamIndex)
//let rotationAngle: Double = metrics.rotationAngle
//let aspect = Double(metrics.width) / Double(metrics.height)
let stream = FFMpegFileReader.StreamInfo(
index: streamIndexNumber.intValue,
codecId: codecId,
startTime: startTime,
duration: duration,
timeBase: timebase.value,
timeScale: timebase.timescale,
fps: fps
)
if isVideo {
videoStream = stream
} else {
audioStream = stream
}
}
}
self.audioStream = audioStream
self.videoStream = videoStream
if let preferredStream = self.videoStream ?? self.audioStream {
let pts = CMTimeMakeWithSeconds(params.seekToTimestamp, preferredTimescale: preferredStream.timeScale)
self.seekInfo = FFMpegLookahead.State.Seek(streamIndex: preferredStream.index, pts: pts.value)
avFormatContext.seekFrame(forStreamIndex: Int32(preferredStream.index), pts: pts.value, positionOnKeyframe: true)
}
self.updateCurrentTimestamp()
}
deinit {
self.currentFetchDisposable?.dispose()
}
func setReadingOffset(offset: Int64) {
self.readingOffset = offset
let readRange: Range<Int64> = offset ..< (offset + 512 * 1024)
if !self.params.isDataCachedInRange(readRange) {
if let currentFetchRange = self.currentFetchRange {
if currentFetchRange.overlaps(readRange) {
if !range(currentFetchRange, fullyContains: readRange) {
self.setFetchRange(range: currentFetchRange.lowerBound ..< max(currentFetchRange.upperBound, readRange.upperBound + 2 * 1024 * 1024))
}
} else {
self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024))
}
} else {
self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024))
}
}
}
private func setFetchRange(range: Range<Int64>) {
if self.currentFetchRange != range {
self.currentFetchRange = range
self.currentFetchDisposable?.dispose()
self.currentFetchDisposable = self.params.fetchInRange(range)
}
}
func updateCurrentTimestamp() {
self.currentTimestamp = self.params.currentTimestamp.with({ $0 })
self.updateReadIfNeeded()
}
private func updateReadIfNeeded() {
guard let avFormatContext = self.avFormatContext else {
return
}
guard let currentTimestamp = self.currentTimestamp else {
return
}
let maxPtsSeconds = max(self.params.seekToTimestamp, currentTimestamp) + self.params.lookaheadDuration
var currentAudioPtsSecondsAdvanced: Double = 0.0
var currentVideoPtsSecondsAdvanced: Double = 0.0
let packet = FFMpegPacket()
while !self.isCancelled && !self.isEnded {
var audioAlreadyRead: Bool = false
var videoAlreadyRead: Bool = false
if let audioStreamState = self.audioStreamState {
if audioStreamState.readableToTime.seconds >= maxPtsSeconds {
audioAlreadyRead = true
}
} else if self.audioStream == nil {
audioAlreadyRead = true
}
if let videoStreamState = self.videoStreamState {
if videoStreamState.readableToTime.seconds >= maxPtsSeconds {
videoAlreadyRead = true
}
} else if self.videoStream == nil {
videoAlreadyRead = true
}
if audioAlreadyRead && videoAlreadyRead {
break
}
if !avFormatContext.readFrame(into: packet) {
self.isEnded = true
break
}
self.maxReadPts = FFMpegLookahead.State.Seek(streamIndex: Int(packet.streamIndex), pts: packet.pts)
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timeScale)
if let audioStreamState = self.audioStreamState {
currentAudioPtsSecondsAdvanced += pts.seconds - audioStreamState.readableToTime.seconds
}
self.audioStreamState = FFMpegLookahead.StreamState(
info: audioStream,
readableToTime: pts
)
} else if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index {
let pts = CMTimeMake(value: packet.pts, timescale: videoStream.timeScale)
if let videoStreamState = self.videoStreamState {
currentVideoPtsSecondsAdvanced += pts.seconds - videoStreamState.readableToTime.seconds
}
self.videoStreamState = FFMpegLookahead.StreamState(
info: videoStream,
readableToTime: pts
)
}
if min(currentAudioPtsSecondsAdvanced, currentVideoPtsSecondsAdvanced) >= 0.1 {
self.reportStateIfNeeded()
}
}
self.reportStateIfNeeded()
}
private func reportStateIfNeeded() {
guard let seekInfo = self.seekInfo else {
return
}
var stateIsFullyInitialised = true
if self.audioStream != nil && self.audioStreamState == nil {
stateIsFullyInitialised = false
}
if self.videoStream != nil && self.videoStreamState == nil {
stateIsFullyInitialised = false
}
let state = FFMpegLookahead.State(
seek: seekInfo,
maxReadablePts: self.maxReadPts,
audio: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.audioStreamState : nil,
video: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.videoStreamState : nil,
isEnded: self.isEnded
)
if self.reportedState != state {
self.reportedState = state
self.params.updateState(state)
}
}
}
private final class FFMpegLookaheadThread: NSObject {
final class Params: NSObject {
let seekToTimestamp: Double
let lookaheadDuration: Double
let updateState: (FFMpegLookahead.State) -> Void
let fetchInRange: (Range<Int64>) -> Disposable
let getDataInRange: (Range<Int64>, @escaping (Data?) -> Void) -> Disposable
let isDataCachedInRange: (Range<Int64>) -> Bool
let size: Int64
let cancel: Signal<Void, NoError>
let currentTimestamp: Atomic<Double?>
init(
seekToTimestamp: Double,
lookaheadDuration: Double,
updateState: @escaping (FFMpegLookahead.State) -> Void,
fetchInRange: @escaping (Range<Int64>) -> Disposable,
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
size: Int64,
cancel: Signal<Void, NoError>,
currentTimestamp: Atomic<Double?>
) {
self.seekToTimestamp = seekToTimestamp
self.lookaheadDuration = lookaheadDuration
self.updateState = updateState
self.fetchInRange = fetchInRange
self.getDataInRange = getDataInRange
self.isDataCachedInRange = isDataCachedInRange
self.size = size
self.cancel = cancel
self.currentTimestamp = currentTimestamp
}
}
@objc static func entryPoint(_ params: Params) {
let runLoop = RunLoop.current
let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: FFMpegLookaheadThread.self, selector: #selector(FFMpegLookaheadThread.none), userInfo: nil, repeats: false)
runLoop.add(timer, forMode: .common)
Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] = FFMpegLookaheadReader(params: params)
while true {
runLoop.run(mode: .default, before: .distantFuture)
if Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] != nil {
break
}
}
Thread.current.threadDictionary.removeObject(forKey: "FFMpegLookaheadThread_params")
}
@objc static func none() {
}
@objc static func stop() {
Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] = "true"
}
@objc static func updateCurrentTimestamp() {
if let reader = Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] as? FFMpegLookaheadReader {
reader.updateCurrentTimestamp()
}
}
}
final class FFMpegLookahead {
struct StreamState: Equatable {
let info: FFMpegFileReader.StreamInfo
let readableToTime: CMTime
init(info: FFMpegFileReader.StreamInfo, readableToTime: CMTime) {
self.info = info
self.readableToTime = readableToTime
}
}
struct State: Equatable {
struct Seek: Equatable {
var streamIndex: Int
var pts: Int64
init(streamIndex: Int, pts: Int64) {
self.streamIndex = streamIndex
self.pts = pts
}
}
let seek: Seek
let maxReadablePts: Seek?
let audio: StreamState?
let video: StreamState?
let isEnded: Bool
init(seek: Seek, maxReadablePts: Seek?, audio: StreamState?, video: StreamState?, isEnded: Bool) {
self.seek = seek
self.maxReadablePts = maxReadablePts
self.audio = audio
self.video = video
self.isEnded = isEnded
}
}
private let cancel = Promise<Void>()
private let currentTimestamp = Atomic<Double?>(value: nil)
private let thread: Thread
init(
seekToTimestamp: Double,
lookaheadDuration: Double,
updateState: @escaping (FFMpegLookahead.State) -> Void,
fetchInRange: @escaping (Range<Int64>) -> Disposable,
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
size: Int64
) {
self.thread = Thread(
target: FFMpegLookaheadThread.self,
selector: #selector(FFMpegLookaheadThread.entryPoint(_:)),
object: FFMpegLookaheadThread.Params(
seekToTimestamp: seekToTimestamp,
lookaheadDuration: lookaheadDuration,
updateState: updateState,
fetchInRange: fetchInRange,
getDataInRange: getDataInRange,
isDataCachedInRange: isDataCachedInRange,
size: size,
cancel: self.cancel.get(),
currentTimestamp: self.currentTimestamp
)
)
self.thread.name = "FFMpegLookahead"
self.thread.start()
}
deinit {
self.cancel.set(.single(Void()))
FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.stop), on: self.thread, with: nil, waitUntilDone: false)
}
func updateCurrentTimestamp(timestamp: Double) {
let _ = self.currentTimestamp.swap(timestamp)
FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.updateCurrentTimestamp), on: self.thread, with: timestamp as NSNumber, waitUntilDone: false)
}
}
final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
private let resource: ChunkMediaPlayerV2.SourceDescription.ResourceDescription
private let partsStateValue = Promise<ChunkMediaPlayerPartsState>()
var partsState: Signal<ChunkMediaPlayerPartsState, NoError> {
return self.partsStateValue.get()
}
private var resourceSizeDisposable: Disposable?
private var completeFetchDisposable: Disposable?
private var seekTimestamp: Double?
private var currentLookaheadId: Int = 0
private var lookahead: FFMpegLookahead?
private var resolvedResourceSize: Int64?
private var pendingSeek: (id: Int, position: Double)?
init(resource: ChunkMediaPlayerV2.SourceDescription.ResourceDescription) {
self.resource = resource
if resource.fetchAutomatically {
self.completeFetchDisposable = fetchedMediaResource(
mediaBox: resource.postbox.mediaBox,
userLocation: resource.userLocation,
userContentType: resource.userContentType,
reference: resource.reference,
statsCategory: resource.statsCategory,
preferBackgroundReferenceRevalidation: true
).startStrict()
}
}
deinit {
self.resourceSizeDisposable?.dispose()
self.completeFetchDisposable?.dispose()
}
func seek(id: Int, position: Double) {
if self.resource.size == 0 && self.resolvedResourceSize == nil {
self.pendingSeek = (id, position)
if self.resourceSizeDisposable == nil {
self.resourceSizeDisposable = (self.resource.postbox.mediaBox.resourceData(self.resource.reference.resource, option: .complete(waitUntilFetchStatus: false))
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let self else {
return
}
if data.complete {
if self.resolvedResourceSize == nil {
self.resolvedResourceSize = data.size
if let pendingSeek = self.pendingSeek {
self.seek(id: pendingSeek.id, position: pendingSeek.position)
}
}
}
})
}
return
}
self.seekTimestamp = position
self.currentLookaheadId += 1
let lookaheadId = self.currentLookaheadId
let resource = self.resource
let resourceSize = self.resolvedResourceSize ?? Int64(resource.size)
let updateState: (FFMpegLookahead.State) -> Void = { [weak self] state in
Queue.mainQueue().async {
guard let self else {
return
}
if self.currentLookaheadId != lookaheadId {
return
}
guard let mainTrack = state.video ?? state.audio else {
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: nil,
content: .directReader(ChunkMediaPlayerPartsState.DirectReader(
id: id,
seekPosition: position,
availableUntilPosition: position,
bufferedUntilEnd: true,
impl: nil
))
)))
return
}
var minAvailableUntilPosition: Double?
if let audio = state.audio {
if let minAvailableUntilPositionValue = minAvailableUntilPosition {
minAvailableUntilPosition = min(minAvailableUntilPositionValue, audio.readableToTime.seconds)
} else {
minAvailableUntilPosition = audio.readableToTime.seconds
}
}
if let video = state.video {
if let minAvailableUntilPositionValue = minAvailableUntilPosition {
minAvailableUntilPosition = min(minAvailableUntilPositionValue, video.readableToTime.seconds)
} else {
minAvailableUntilPosition = video.readableToTime.seconds
}
}
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: mainTrack.info.duration.seconds,
content: .directReader(ChunkMediaPlayerPartsState.DirectReader(
id: id,
seekPosition: position,
availableUntilPosition: minAvailableUntilPosition ?? position,
bufferedUntilEnd: state.isEnded,
impl: ChunkMediaPlayerPartsState.DirectReader.Impl(
video: state.video.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in
guard let maxReadablePts = state.maxReadablePts else {
return nil
}
return ChunkMediaPlayerPartsState.DirectReader.Stream(
mediaBox: resource.postbox.mediaBox,
resource: resource.reference.resource,
size: resourceSize,
index: media.info.index,
seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts),
maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded),
codecName: resolveFFMpegCodecName(id: media.info.codecId)
)
},
audio: state.audio.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in
guard let maxReadablePts = state.maxReadablePts else {
return nil
}
return ChunkMediaPlayerPartsState.DirectReader.Stream(
mediaBox: resource.postbox.mediaBox,
resource: resource.reference.resource,
size: resource.size,
index: media.info.index,
seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts),
maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded),
codecName: resolveFFMpegCodecName(id: media.info.codecId)
)
}
)
))
)))
}
}
self.lookahead = FFMpegLookahead(
seekToTimestamp: position,
lookaheadDuration: 10.0,
updateState: updateState,
fetchInRange: { range in
return fetchedMediaResource(
mediaBox: resource.postbox.mediaBox,
userLocation: resource.userLocation,
userContentType: resource.userContentType,
reference: resource.reference,
range: (range, .elevated),
statsCategory: resource.statsCategory,
preferBackgroundReferenceRevalidation: true
).startStrict()
},
getDataInRange: { range, completion in
return resource.postbox.mediaBox.resourceData(resource.reference.resource, size: resourceSize, in: range, mode: .complete).start(next: { result, isComplete in
completion(isComplete ? result : nil)
})
},
isDataCachedInRange: { range in
return resource.postbox.mediaBox.internal_resourceDataIsCached(
id: resource.reference.resource.id,
size: resourceSize,
in: range
)
},
size: resourceSize
)
}
func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) {
if self.seekTimestamp == seekTimestamp {
self.lookahead?.updateCurrentTimestamp(timestamp: position)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,164 @@
import Foundation
import CoreMedia
import FFMpegBinding
final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
private let codecContext: FFMpegAVCodecContext
private let swrContext: FFMpegSWResample
private var timescale: CMTimeScale = 44000
private let audioFrame: FFMpegAVFrame
private var resetDecoderOnNextFrame = true
private let formatDescription: CMAudioFormatDescription
private var delayedFrames: [MediaTrackFrame] = []
init(codecContext: FFMpegAVCodecContext, sampleRate: Int = 44100, channelCount: Int = 2) {
self.codecContext = codecContext
self.audioFrame = FFMpegAVFrame()
self.swrContext = FFMpegSWResample(sourceChannelCount: Int(codecContext.channels()), sourceSampleRate: Int(codecContext.sampleRate()), sourceSampleFormat: codecContext.sampleFormat(), destinationChannelCount: channelCount, destinationSampleRate: sampleRate, destinationSampleFormat: FFMPEG_AV_SAMPLE_FMT_S16)
var outputDescription = AudioStreamBasicDescription(
mSampleRate: Float64(sampleRate),
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked,
mBytesPerPacket: UInt32(2 * channelCount),
mFramesPerPacket: 1,
mBytesPerFrame: UInt32(2 * channelCount),
mChannelsPerFrame: UInt32(channelCount),
mBitsPerChannel: 16,
mReserved: 0
)
var channelLayout = AudioChannelLayout()
memset(&channelLayout, 0, MemoryLayout<AudioChannelLayout>.size)
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono
var formatDescription: CMAudioFormatDescription?
CMAudioFormatDescriptionCreate(allocator: nil, asbd: &outputDescription, layoutSize: MemoryLayout<AudioChannelLayout>.size, layout: &channelLayout, magicCookieSize: 0, magicCookie: nil, extensions: nil, formatDescriptionOut: &formatDescription)
self.formatDescription = formatDescription!
}
func decodeRaw(frame: MediaTrackDecodableFrame) -> Data? {
let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 {
let result = self.codecContext.receive(into: self.audioFrame)
if case .success = result {
guard let data = self.swrContext.resample(self.audioFrame) else {
return nil
}
return data
} else {
return nil
}
} else {
return nil
}
}
func send(frame: MediaTrackDecodableFrame) -> Bool {
self.timescale = frame.pts.timescale
let status = frame.packet.send(toDecoder: self.codecContext)
return status == 0
}
func decode() -> MediaTrackFrame? {
while true {
let result = self.codecContext.receive(into: self.audioFrame)
if case .success = result {
if let convertedFrame = convertAudioFrame(self.audioFrame) {
self.delayedFrames.append(convertedFrame)
}
} else {
break
}
}
if self.delayedFrames.count >= 1 {
var minFrameIndex = 0
var minPosition = self.delayedFrames[0].position
for i in 1 ..< self.delayedFrames.count {
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
minFrameIndex = i
minPosition = self.delayedFrames[i].position
}
}
return self.delayedFrames.remove(at: minFrameIndex)
}
return nil
}
func takeQueuedFrame() -> MediaTrackFrame? {
if self.delayedFrames.count >= 1 {
var minFrameIndex = 0
var minPosition = self.delayedFrames[0].position
for i in 1 ..< self.delayedFrames.count {
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
minFrameIndex = i
minPosition = self.delayedFrames[i].position
}
}
return self.delayedFrames.remove(at: minFrameIndex)
} else {
return nil
}
}
func takeRemainingFrame() -> MediaTrackFrame? {
if !self.delayedFrames.isEmpty {
var minFrameIndex = 0
var minPosition = self.delayedFrames[0].position
for i in 1 ..< self.delayedFrames.count {
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
minFrameIndex = i
minPosition = self.delayedFrames[i].position
}
}
return self.delayedFrames.remove(at: minFrameIndex)
} else {
return nil
}
}
private func convertAudioFrame(_ frame: FFMpegAVFrame) -> MediaTrackFrame? {
guard let data = self.swrContext.resample(frame) else {
return nil
}
var blockBuffer: CMBlockBuffer?
let bytes = malloc(data.count)!
data.copyBytes(to: bytes.assumingMemoryBound(to: UInt8.self), count: data.count)
let status = CMBlockBufferCreateWithMemoryBlock(allocator: nil, memoryBlock: bytes, blockLength: data.count, blockAllocator: nil, customBlockSource: nil, offsetToData: 0, dataLength: data.count, flags: 0, blockBufferOut: &blockBuffer)
if status != noErr {
return nil
}
var sampleBuffer: CMSampleBuffer?
let pts = CMTime(value: frame.pts, timescale: self.timescale)
guard CMAudioSampleBufferCreateReadyWithPacketDescriptions(allocator: nil, dataBuffer: blockBuffer!, formatDescription: self.formatDescription, sampleCount: Int(data.count / 2), presentationTimeStamp: pts, packetDescriptions: nil, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
let resetDecoder = self.resetDecoderOnNextFrame
self.resetDecoderOnNextFrame = false
return MediaTrackFrame(type: .audio, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true)
}
func reset() {
self.codecContext.flushBuffers()
self.resetDecoderOnNextFrame = true
}
func sendEndToDecoder() -> Bool {
return true
}
}
@@ -0,0 +1,529 @@
import Foundation
#if !os(macOS)
import UIKit
#else
import AppKit
import TGUIKit
#endif
import CoreMedia
import SwiftSignalKit
import FFMpegBinding
import Postbox
import ManagedFile
private func FFMpegFileReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
guard let buffer else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
let context = Unmanaged<FFMpegFileReader>.fromOpaque(userData!).takeUnretainedValue()
switch context.source {
case let .file(file):
let result = file.read(buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
case let .resource(resource):
let readCount = min(256 * 1024, Int64(bufferSize))
var bufferOffset = 0
let doRead: (Range<Int64>) -> Void = { range in
//TODO:improve thread safe read if incomplete
if let (file, readSize) = resource.mediaBox.internal_resourceData(id: resource.resource.id, size: resource.resourceSize, in: range) {
let effectiveReadSize = max(0, min(Int(readCount) - bufferOffset, readSize))
let count = file.read(buffer.advanced(by: bufferOffset), effectiveReadSize)
bufferOffset += count
resource.readingPosition += Int64(count)
}
}
var mappedRangePosition: Int64 = 0
for mappedRange in resource.mappedRanges {
let bytesToRead = readCount - Int64(bufferOffset)
if bytesToRead <= 0 {
break
}
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
let mappedRangeReadingPosition = resource.readingPosition - mappedRangePosition
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
if mappedRangeBytesToRead > 0 {
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
doRead(mappedReadRange)
}
}
mappedRangePosition += mappedRangeSize
}
if bufferOffset != 0 {
return Int32(bufferOffset)
} else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
}
private func FFMpegFileReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegFileReader>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
switch context.source {
case let .file(file):
return file.getSize() ?? 0
case let .resource(resource):
return resource.size
}
} else {
switch context.source {
case let .file(file):
let _ = file.seek(position: offset)
case let .resource(resource):
resource.readingPosition = offset
}
return offset
}
}
public final class FFMpegFileReader {
public enum SourceDescription {
case file(String)
case resource(mediaBox: MediaBox, resource: MediaResource, resourceSize: Int64, mappedRanges: [Range<Int64>])
}
public final class StreamInfo: Equatable {
public let index: Int
public let codecId: Int32
public let startTime: CMTime
public let duration: CMTime
public let timeBase: CMTimeValue
public let timeScale: CMTimeScale
public let fps: CMTime
public init(index: Int, codecId: Int32, startTime: CMTime, duration: CMTime, timeBase: CMTimeValue, timeScale: CMTimeScale, fps: CMTime) {
self.index = index
self.codecId = codecId
self.startTime = startTime
self.duration = duration
self.timeBase = timeBase
self.timeScale = timeScale
self.fps = fps
}
public static func ==(lhs: StreamInfo, rhs: StreamInfo) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.codecId != rhs.codecId {
return false
}
if lhs.startTime != rhs.startTime {
return false
}
if lhs.duration != rhs.duration {
return false
}
if lhs.timeBase != rhs.timeBase {
return false
}
if lhs.timeScale != rhs.timeScale {
return false
}
if lhs.fps != rhs.fps {
return false
}
return true
}
}
fileprivate enum Source {
final class Resource {
let mediaBox: MediaBox
let resource: MediaResource
let resourceSize: Int64
let mappedRanges: [Range<Int64>]
let size: Int64
var readingPosition: Int64 = 0
init(mediaBox: MediaBox, resource: MediaResource, resourceSize: Int64, mappedRanges: [Range<Int64>]) {
self.mediaBox = mediaBox
self.resource = resource
self.resourceSize = resourceSize
self.mappedRanges = mappedRanges
var size: Int64 = 0
for range in mappedRanges {
size += range.upperBound - range.lowerBound
}
self.size = size
}
}
case file(ManagedFile)
case resource(Resource)
}
private enum Decoder {
case videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder)
case video(FFMpegMediaVideoFrameDecoder)
case audio(FFMpegAudioFrameDecoder)
func send(frame: MediaTrackDecodableFrame) -> Bool {
switch self {
case let .videoPassthrough(decoder):
decoder.send(frame: frame)
case let .video(decoder):
decoder.send(frame: frame)
case let .audio(decoder):
decoder.send(frame: frame)
}
}
func sendEnd() -> Bool {
switch self {
case let .videoPassthrough(decoder):
return decoder.sendEndToDecoder()
case let .video(decoder):
return decoder.sendEndToDecoder()
case let .audio(decoder):
return decoder.sendEndToDecoder()
}
}
}
private final class Stream {
let info: StreamInfo
let decoder: Decoder
init(info: StreamInfo, decoder: Decoder) {
self.info = info
self.decoder = decoder
}
}
public enum SelectedStream {
public enum MediaType {
case audio
case video
}
case mediaType(MediaType)
case index(Int)
}
public enum Seek {
case stream(streamIndex: Int, pts: Int64)
case direct(position: Double)
}
public enum ReadFrameResult {
case frame(MediaTrackFrame)
case waitingForMoreData
case endOfStream
case error
}
private(set) var readingError = false
private var stream: Stream?
private var avIoContext: FFMpegAVIOContext?
private var avFormatContext: FFMpegAVFormatContext?
fileprivate let source: Source
private var didSendEndToDecoder: Bool = false
private var hasReadToEnd: Bool = false
private var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
private var lastReadPts: (streamIndex: Int, pts: Int64)?
private var isWaitingForMoreData: Bool = false
public init?(source: SourceDescription, passthroughDecoder: Bool = false, useHardwareAcceleration: Bool, selectedStream: SelectedStream, seek: Seek?, maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
switch source {
case let .file(path):
guard let file = ManagedFile(queue: nil, path: path, mode: .read) else {
return nil
}
self.source = .file(file)
case let .resource(mediaBox, resource, resourceSize, mappedRanges):
self.source = .resource(Source.Resource(mediaBox: mediaBox, resource: resource, resourceSize: resourceSize, mappedRanges: mappedRanges))
}
self.maxReadablePts = maxReadablePts
let avFormatContext = FFMpegAVFormatContext()
/*if hintVP9 {
avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9)
}*/
let ioBufferSize = 64 * 1024
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegFileReader_readPacketCallback, writePacket: nil, seek: FFMpegFileReader_seekCallback, isSeekable: true)
self.avIoContext = avIoContext
avFormatContext.setIO(self.avIoContext!)
if !avFormatContext.openInput(withDirectFilePath: nil) {
self.readingError = true
return nil
}
if !avFormatContext.findStreamInfo() {
self.readingError = true
return nil
}
self.avFormatContext = avFormatContext
var stream: Stream?
outer: for mediaType in [.audio, .video] as [SelectedStream.MediaType] {
streamSearch: for streamIndexNumber in avFormatContext.streamIndices(for: mediaType == .video ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = Int(streamIndexNumber.int32Value)
if avFormatContext.isAttachedPic(atStreamIndex: Int32(streamIndex)) {
continue
}
switch selectedStream {
case let .mediaType(selectedMediaType):
if mediaType != selectedMediaType {
continue streamSearch
}
case let .index(index):
if streamIndex != index {
continue streamSearch
}
}
let codecId = avFormatContext.codecId(atStreamIndex: Int32(streamIndex))
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: Int32(streamIndex), defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: Int32(streamIndex))
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: Int32(streamIndex)), timescale: timebase.timescale)
let metrics = avFormatContext.metricsForStream(at: Int32(streamIndex))
let rotationAngle: Double = metrics.rotationAngle
//let aspect = Double(metrics.width) / Double(metrics.height)
let info = StreamInfo(
index: streamIndex,
codecId: codecId,
startTime: startTime,
duration: duration,
timeBase: timebase.value,
timeScale: timebase.timescale,
fps: fps
)
switch mediaType {
case .video:
if passthroughDecoder {
var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData?
if codecId == FFMpegCodecIdMPEG4 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdH264 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdHEVC {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdAV1 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
}
if let videoFormatData {
stream = Stream(
info: info,
decoder: .videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle))
)
break outer
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: useHardwareAcceleration) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) {
if useHardwareAcceleration {
codecContext.setupHardwareAccelerationIfPossible()
}
if codecContext.open() {
stream = Stream(
info: info,
decoder: .video(FFMpegMediaVideoFrameDecoder(codecContext: codecContext))
)
break outer
}
}
}
}
case .audio:
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) {
if codecContext.open() {
stream = Stream(
info: info,
decoder: .audio(FFMpegAudioFrameDecoder(codecContext: codecContext, sampleRate: 48000, channelCount: 1))
)
break outer
}
}
}
}
}
}
guard let stream else {
self.readingError = true
return nil
}
self.stream = stream
if let seek {
switch seek {
case let .stream(streamIndex, pts):
avFormatContext.seekFrame(forStreamIndex: Int32(streamIndex), pts: pts, positionOnKeyframe: true)
case let .direct(position):
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: CMTimeMakeWithSeconds(Float64(position), preferredTimescale: stream.info.timeScale).value, positionOnKeyframe: true)
}
} else {
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: 0, positionOnKeyframe: true)
}
}
deinit {
}
public func frameRate() -> Int {
if let stream = self.stream {
return Int(stream.info.fps.seconds)
} else {
return 0
}
}
public func duration() -> CMTime {
if let stream = self.stream {
return stream.info.duration
} else {
return .zero
}
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
if let maxReadablePts = self.maxReadablePts, !maxReadablePts.isEnded, let lastReadPts = self.lastReadPts, lastReadPts.streamIndex == maxReadablePts.streamIndex, lastReadPts.pts == maxReadablePts.pts {
self.isWaitingForMoreData = true
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
self.lastReadPts = (Int(packet.streamIndex), packet.pts)
return packet
} else {
self.hasReadToEnd = true
return nil
}
}
func readDecodableFrame() -> MediaTrackDecodableFrame? {
while !self.readingError && !self.hasReadToEnd && !self.isWaitingForMoreData {
if let packet = self.readPacketInternal() {
if let stream = self.stream, Int(packet.streamIndex) == stream.info.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: stream.info.timeScale)
let dts = CMTimeMake(value: packet.dts, timescale: stream.info.timeScale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * stream.info.timeBase, timescale: stream.info.timeScale)
} else {
duration = CMTimeConvertScale(CMTimeMakeWithSeconds(1.0 / stream.info.fps.seconds, preferredTimescale: stream.info.timeScale), timescale: stream.info.timeScale, method: .quickTime)
}
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
return frame
}
} else {
break
}
}
return nil
}
public func readFrame(argb: Bool = false) -> ReadFrameResult {
guard let stream = self.stream else {
return .error
}
while true {
var result: MediaTrackFrame?
switch stream.decoder {
case let .video(decoder):
result = decoder.decode(ptsOffset: nil, forceARGB: argb, unpremultiplyAlpha: false, displayImmediately: false)
case let .videoPassthrough(decoder):
result = decoder.decode()
case let .audio(decoder):
result = decoder.decode()
}
if let result {
if self.didSendEndToDecoder {
assert(true)
}
return .frame(result)
}
if !self.isWaitingForMoreData && !self.readingError && !self.hasReadToEnd {
if let decodableFrame = self.readDecodableFrame() {
let _ = stream.decoder.send(frame: decodableFrame)
}
} else if self.hasReadToEnd && !self.didSendEndToDecoder {
self.didSendEndToDecoder = true
let _ = stream.decoder.sendEnd()
} else {
break
}
}
if self.isWaitingForMoreData {
return .waitingForMoreData
} else {
return .endOfStream
}
}
public func updateMaxReadablePts(pts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
if self.maxReadablePts?.streamIndex != pts?.streamIndex || self.maxReadablePts?.pts != pts?.pts {
self.maxReadablePts = pts
if let pts {
if pts.isEnded {
self.isWaitingForMoreData = false
} else {
if self.lastReadPts?.streamIndex != pts.streamIndex || self.lastReadPts?.pts != pts.pts {
self.isWaitingForMoreData = false
}
}
} else {
self.isWaitingForMoreData = false
}
}
}
}
@@ -0,0 +1,325 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
private final class ThreadTaskQueue: NSObject {
private var mutex: pthread_mutex_t
private var condition: pthread_cond_t
private var tasks: [() -> Void] = []
private var shouldExit = false
override init() {
self.mutex = pthread_mutex_t()
self.condition = pthread_cond_t()
pthread_mutex_init(&self.mutex, nil)
pthread_cond_init(&self.condition, nil)
super.init()
}
deinit {
pthread_mutex_destroy(&self.mutex)
pthread_cond_destroy(&self.condition)
}
func loop() {
while !self.shouldExit {
pthread_mutex_lock(&self.mutex)
if tasks.isEmpty {
pthread_cond_wait(&self.condition, &self.mutex)
}
var task: (() -> Void)?
if !self.tasks.isEmpty {
task = self.tasks.removeFirst()
}
pthread_mutex_unlock(&self.mutex)
if let task = task {
autoreleasepool {
task()
}
}
}
}
func enqueue(_ task: @escaping () -> Void) {
pthread_mutex_lock(&self.mutex)
self.tasks.append(task)
pthread_cond_broadcast(&self.condition)
pthread_mutex_unlock(&self.mutex)
}
func terminate() {
pthread_mutex_lock(&self.mutex)
self.shouldExit = true
pthread_cond_broadcast(&self.condition)
pthread_mutex_unlock(&self.mutex)
}
}
private func contextForCurrentThread() -> FFMpegMediaFrameSourceContext? {
return Thread.current.threadDictionary["FFMpegMediaFrameSourceContext"] as? FFMpegMediaFrameSourceContext
}
public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource {
private let queue: Queue
private let postbox: Postbox
private let userLocation: MediaResourceUserLocation
private let userContentType: MediaResourceUserContentType
private let resourceReference: MediaResourceReference
private let tempFilePath: String?
private let limitedFileRange: Range<Int64>?
private let streamable: Bool
private let isSeekable: Bool
private let stallDuration: Double
private let lowWaterDuration: Double
private let highWaterDuration: Double
private let video: Bool
private let preferSoftwareDecoding: Bool
private let fetchAutomatically: Bool
private let maximumFetchSize: Int?
private let storeAfterDownload: (() -> Void)?
private let isAudioVideoMessage: Bool
private let taskQueue: ThreadTaskQueue
private let thread: Thread
private let eventSinkBag = Bag<(MediaTrackEvent) -> Void>()
private var generatingFrames = false
private var requestedFrameGenerationTimestamp: Double?
@objc private static func threadEntry(_ taskQueue: ThreadTaskQueue) {
autoreleasepool {
let context = FFMpegMediaFrameSourceContext(thread: Thread.current)
let localStorage = Thread.current.threadDictionary
localStorage["FFMpegMediaFrameSourceContext"] = context
taskQueue.loop()
Thread.current.threadDictionary.removeObject(forKey: "FFMpegMediaFrameSourceContext")
}
}
public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range<Int64>?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) {
self.queue = queue
self.postbox = postbox
self.userLocation = userLocation
self.userContentType = userContentType
self.resourceReference = resourceReference
self.tempFilePath = tempFilePath
self.limitedFileRange = limitedFileRange
self.streamable = streamable
self.isSeekable = isSeekable
self.video = video
self.preferSoftwareDecoding = preferSoftwareDecoding
self.fetchAutomatically = fetchAutomatically
self.maximumFetchSize = maximumFetchSize
self.stallDuration = stallDuration
self.lowWaterDuration = lowWaterDuration
self.highWaterDuration = highWaterDuration
self.storeAfterDownload = storeAfterDownload
self.isAudioVideoMessage = isAudioVideoMessage
self.taskQueue = ThreadTaskQueue()
self.thread = Thread(target: FFMpegMediaFrameSource.self, selector: #selector(FFMpegMediaFrameSource.threadEntry(_:)), object: taskQueue)
self.thread.name = "FFMpegMediaFrameSourceContext"
self.thread.start()
super.init()
}
deinit {
assert(self.queue.isCurrent())
self.taskQueue.terminate()
}
public func addEventSink(_ f: @escaping (MediaTrackEvent) -> Void) -> Int {
assert(self.queue.isCurrent())
return self.eventSinkBag.add(f)
}
public func removeEventSink(_ index: Int) {
assert(self.queue.isCurrent())
self.eventSinkBag.remove(index)
}
public func generateFrames(until timestamp: Double, types: [MediaTrackFrameType]) {
assert(self.queue.isCurrent())
if self.requestedFrameGenerationTimestamp == nil || !self.requestedFrameGenerationTimestamp!.isEqual(to: timestamp) {
self.requestedFrameGenerationTimestamp = timestamp
self.internalGenerateFrames(until: timestamp, types: types)
}
}
public func ensureHasFrames(until timestamp: Double) -> Signal<Never, NoError> {
assert(self.queue.isCurrent())
return Signal { subscriber in
let disposable = MetaDisposable()
let currentSemaphore = Atomic<Atomic<DispatchSemaphore?>?>(value: nil)
disposable.set(ActionDisposable {
currentSemaphore.with({ $0 })?.with({ $0 })?.signal()
})
self.performWithContext({ context in
let _ = currentSemaphore.swap(context.currentSemaphore)
let _ = context.takeFrames(until: timestamp, types: [.audio, .video])
subscriber.putCompletion()
})
return disposable
}
|> runOn(self.queue)
}
private func internalGenerateFrames(until timestamp: Double, types: [MediaTrackFrameType]) {
if self.generatingFrames {
return
}
self.generatingFrames = true
let postbox = self.postbox
let resourceReference = self.resourceReference
let tempFilePath = self.tempFilePath
let limitedFileRange = self.limitedFileRange
let queue = self.queue
let streamable = self.streamable
let isSeekable = self.isSeekable
let userLocation = self.userLocation
let video = self.video
let preferSoftwareDecoding = self.preferSoftwareDecoding
let fetchAutomatically = self.fetchAutomatically
let maximumFetchSize = self.maximumFetchSize
let storeAfterDownload = self.storeAfterDownload
let isAudioVideoMessage = self.isAudioVideoMessage
self.performWithContext { [weak self] context in
context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage)
let (frames, endOfStream) = context.takeFrames(until: timestamp, types: types)
queue.async { [weak self] in
if let strongSelf = self {
strongSelf.generatingFrames = false
for sink in strongSelf.eventSinkBag.copyItems() {
sink(.frames(frames))
if endOfStream {
sink(.endOfStream)
}
}
if strongSelf.requestedFrameGenerationTimestamp != nil && !strongSelf.requestedFrameGenerationTimestamp!.isEqual(to: timestamp) {
strongSelf.internalGenerateFrames(until: strongSelf.requestedFrameGenerationTimestamp!, types: types)
}
}
}
}
}
func performWithContext(_ f: @escaping (FFMpegMediaFrameSourceContext) -> Void) {
assert(self.queue.isCurrent())
taskQueue.enqueue {
if let context = contextForCurrentThread() {
f(context)
}
}
}
public func seek(timestamp: Double) -> Signal<QueueLocalObject<MediaFrameSourceSeekResult>, MediaFrameSourceSeekError> {
assert(self.queue.isCurrent())
return Signal { subscriber in
let disposable = MetaDisposable()
let queue = self.queue
let postbox = self.postbox
let userLocation = self.userLocation
let resourceReference = self.resourceReference
let tempFilePath = self.tempFilePath
let limitedFileRange = self.limitedFileRange
let streamable = self.streamable
let isSeekable = self.isSeekable
let video = self.video
let preferSoftwareDecoding = self.preferSoftwareDecoding
let fetchAutomatically = self.fetchAutomatically
let maximumFetchSize = self.maximumFetchSize
let storeAfterDownload = self.storeAfterDownload
let isAudioVideoMessage = self.isAudioVideoMessage
let currentSemaphore = Atomic<Atomic<DispatchSemaphore?>?>(value: nil)
disposable.set(ActionDisposable {
currentSemaphore.with({ $0 })?.with({ $0 })?.signal()
})
self.performWithContext { [weak self] context in
let _ = currentSemaphore.swap(context.currentSemaphore)
context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage)
context.seek(timestamp: timestamp, completed: { streamDescriptionsAndTimestamp in
queue.async {
if let strongSelf = self {
if let (streamDescriptions, timestamp) = streamDescriptionsAndTimestamp {
strongSelf.requestedFrameGenerationTimestamp = nil
subscriber.putNext(QueueLocalObject(queue: queue, generate: {
if let strongSelf = self {
var audioBuffer: MediaTrackFrameBuffer?
var videoBuffer: MediaTrackFrameBuffer?
if let audio = streamDescriptions.audio {
audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, startTime: audio.startTime, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration)
}
var extraDecodedVideoFrames: [MediaTrackFrame] = []
if let video = streamDescriptions.video {
videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, startTime: video.startTime, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration)
for videoFrame in streamDescriptions.extraVideoFrames {
if !video.decoder.send(frame: videoFrame) {
break
}
}
while true {
if let decodedFrame = video.decoder.decode() {
extraDecodedVideoFrames.append(decodedFrame)
} else {
break
}
}
}
return MediaFrameSourceSeekResult(buffers: MediaPlaybackBuffers(audioBuffer: audioBuffer, videoBuffer: videoBuffer), extraDecodedVideoFrames: extraDecodedVideoFrames, timestamp: timestamp)
} else {
return MediaFrameSourceSeekResult(buffers: MediaPlaybackBuffers(audioBuffer: nil, videoBuffer: nil), extraDecodedVideoFrames: [], timestamp: timestamp)
}
}))
let _ = currentSemaphore.swap(nil)
subscriber.putCompletion()
} else {
let _ = currentSemaphore.swap(nil)
subscriber.putError(.generic)
}
} else {
let _ = currentSemaphore.swap(nil)
subscriber.putError(.generic)
}
}
})
}
return disposable
}
}
}
@@ -0,0 +1,800 @@
import Foundation
import SwiftSignalKit
import Postbox
import CoreMedia
import TelegramCore
import FFMpegBinding
private struct StreamContext {
let index: Int
let codecContext: FFMpegAVCodecContext?
let fps: CMTime
let timebase: CMTime
let startTime: CMTime
let duration: CMTime
let decoder: MediaTrackFrameDecoder
let rotationAngle: Double
let aspect: Double
}
struct FFMpegMediaFrameSourceDescription {
let startTime: CMTime
let duration: CMTime
let decoder: MediaTrackFrameDecoder
let rotationAngle: Double
let aspect: Double
}
struct FFMpegMediaFrameSourceDescriptionSet {
let audio: FFMpegMediaFrameSourceDescription?
let video: FFMpegMediaFrameSourceDescription?
let extraVideoFrames: [MediaTrackDecodableFrame]
}
private final class InitializedState {
fileprivate let avIoContext: FFMpegAVIOContext?
fileprivate let avFormatContext: FFMpegAVFormatContext
fileprivate let audioStream: StreamContext?
fileprivate let videoStream: StreamContext?
init(avIoContext: FFMpegAVIOContext?, avFormatContext: FFMpegAVFormatContext, audioStream: StreamContext?, videoStream: StreamContext?) {
self.avIoContext = avIoContext
self.avFormatContext = avFormatContext
self.audioStream = audioStream
self.videoStream = videoStream
}
}
struct FFMpegMediaFrameSourceStreamContextInfo {
let duration: CMTime
let decoder: MediaTrackFrameDecoder
}
struct FFMpegMediaFrameSourceContextInfo {
let audioStream: FFMpegMediaFrameSourceStreamContextInfo?
let videoStream: FFMpegMediaFrameSourceStreamContextInfo?
}
private var maxOffset: Int = 0
private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<FFMpegMediaFrameSourceContext>.fromOpaque(userData!).takeUnretainedValue()
guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable else {
return 0
}
var fetchedCount: Int32 = 0
var fetchedData: Data?
var resourceSize: Int64 = resourceReference.resource.size ?? (Int64.max - 1)
if let limitedFileRange = context.limitedFileRange {
resourceSize = min(resourceSize, limitedFileRange.upperBound)
}
let readCount = max(0, min(resourceSize - context.readingOffset, Int64(bufferSize)))
let requestRange: Range<Int64> = context.readingOffset ..< (context.readingOffset + readCount)
assert(readCount < 16 * 1024 * 1024)
if let maximumFetchSize = context.maximumFetchSize {
context.touchedRanges.insert(integersIn: Int(requestRange.lowerBound) ..< Int(requestRange.upperBound))
var totalCount = 0
for range in context.touchedRanges.rangeView {
totalCount += range.count
}
if totalCount > maximumFetchSize {
context.readingError = true
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
if streamable {
let data: Signal<(Data, Bool), NoError>
data = postbox.mediaBox.resourceData(resourceReference.resource, size: resourceSize, in: requestRange, mode: .complete)
if readCount == 0 {
fetchedData = Data()
} else {
if let tempFilePath = context.tempFilePath, let fileData = (try? Data(contentsOf: URL(fileURLWithPath: tempFilePath), options: .mappedRead))?.subdata(in: Int(requestRange.lowerBound) ..< Int(requestRange.upperBound)) {
fetchedData = fileData
} else {
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
var completedRequest = false
let disposable = data.start(next: { result in
let (data, isComplete) = result
if data.count == readCount || isComplete {
precondition(data.count <= readCount)
fetchedData = data
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
context.readingError = true
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
}
} else {
if let tempFilePath = context.tempFilePath, let fileSize = fileSize(tempFilePath) {
let fd = open(tempFilePath, O_RDONLY, S_IRUSR)
if fd >= 0 {
let readingOffset = context.readingOffset
let readCount = max(0, min(fileSize - readingOffset, Int64(bufferSize)))
let range = readingOffset ..< (readingOffset + readCount)
assert(readCount < 16 * 1024 * 1024)
lseek(fd, off_t(range.lowerBound), SEEK_SET)
var data = Data(count: Int(readCount))
data.withUnsafeMutableBytes { bytes -> Void in
precondition(bytes.baseAddress != nil)
let readBytes = read(fd, bytes.baseAddress, Int(readCount))
precondition(readBytes <= readCount)
}
fetchedData = data
close(fd)
}
} else {
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
let readingOffset = context.readingOffset
var completedRequest = false
let disposable = data.start(next: { next in
if next.complete {
let readCount = max(0, min(next.size - readingOffset, Int64(bufferSize)))
let range = readingOffset ..< (readingOffset + readCount)
assert(readCount < 16 * 1024 * 1024)
let fd = open(next.path, O_RDONLY, S_IRUSR)
if fd >= 0 {
lseek(fd, off_t(range.lowerBound), SEEK_SET)
var data = Data(count: Int(readCount))
data.withUnsafeMutableBytes { bytes -> Void in
precondition(bytes.baseAddress != nil)
let readBytes = read(fd, bytes.baseAddress, Int(readCount))
assert(readBytes <= readCount)
precondition(readBytes <= readCount)
}
fetchedData = data
close(fd)
}
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
context.readingError = true
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
}
if let fetchedData = fetchedData {
assert(fetchedData.count <= readCount)
fetchedData.withUnsafeBytes { bytes -> Void in
precondition(bytes.baseAddress != nil)
memcpy(buffer, bytes.baseAddress, min(fetchedData.count, Int(readCount)))
}
fetchedCount = Int32(fetchedData.count)
context.readingOffset += Int64(fetchedCount)
if fetchedCount == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
if context.closed {
context.readingError = true
return FFMPEG_CONSTANT_AVERROR_EOF
}
return fetchedCount
}
private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegMediaFrameSourceContext>.fromOpaque(userData!).takeUnretainedValue()
guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable, let userLocation = context.userLocation, let userContentType = context.userContentType, let statsCategory = context.statsCategory else {
return 0
}
var result: Int64 = offset
var resourceSize: Int64
if let size = resourceReference.resource.size {
resourceSize = size
} else {
if !streamable {
if let tempFilePath = context.tempFilePath, let fileSize = fileSize(tempFilePath) {
resourceSize = fileSize
} else {
var resultSize: Int64 = Int64.max - 1
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
var completedRequest = false
let disposable = data.start(next: { next in
if next.complete {
resultSize = next.size
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
context.readingError = true
return 0
}
resourceSize = resultSize
}
} else {
resourceSize = Int64.max - 1
}
}
if let limitedFileRange = context.limitedFileRange {
resourceSize = min(resourceSize, limitedFileRange.upperBound)
}
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
result = Int64(resourceSize == Int(Int32.max - 1) ? 0 : resourceSize)
} else {
context.readingOffset = min(Int64(resourceSize), offset)
if context.readingOffset != context.requestedDataOffset {
context.requestedDataOffset = context.readingOffset
if context.readingOffset >= resourceSize {
context.fetchedDataDisposable.set(nil)
} else {
if streamable {
if context.tempFilePath == nil {
let fetchRange: Range<Int64>?
if let limitedFileRange = context.limitedFileRange {
if context.readingOffset < limitedFileRange.upperBound {
fetchRange = context.readingOffset ..< limitedFileRange.upperBound
} else {
fetchRange = nil
}
} else {
fetchRange = context.readingOffset ..< Int64.max
}
if let fetchRange {
context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
}
}
} else if !context.requestedCompleteFetch && context.fetchAutomatically && context.limitedFileRange == nil {
context.requestedCompleteFetch = true
if context.tempFilePath == nil {
context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start())
}
}
}
}
}
if context.closed {
context.readingError = true
return 0
}
return result
}
final class FFMpegMediaFrameSourceContext: NSObject {
private let thread: Thread
var closed = false
fileprivate var postbox: Postbox?
fileprivate var userLocation: MediaResourceUserLocation?
fileprivate var userContentType: MediaResourceUserContentType?
fileprivate var resourceReference: MediaResourceReference?
fileprivate var tempFilePath: String?
fileprivate var limitedFileRange: Range<Int64>?
fileprivate var streamable: Bool?
fileprivate var statsCategory: MediaResourceStatsCategory?
fileprivate var readingOffset: Int64 = 0
fileprivate var requestedDataOffset: Int64?
fileprivate let fetchedDataDisposable = MetaDisposable()
fileprivate let keepDataDisposable = MetaDisposable()
fileprivate let fetchedFullDataDisposable = MetaDisposable()
fileprivate let autosaveDisposable = MetaDisposable()
fileprivate var requestedCompleteFetch = false
fileprivate var readingError = false {
didSet {
self.fetchedDataDisposable.dispose()
self.fetchedFullDataDisposable.dispose()
self.keepDataDisposable.dispose()
}
}
private var initializedState: InitializedState?
private var packetQueue: [FFMpegPacket] = []
private var preferSoftwareDecoding: Bool = false
fileprivate var fetchAutomatically: Bool = true
fileprivate var maximumFetchSize: Int? = nil
fileprivate var touchedRanges = IndexSet()
let currentSemaphore = Atomic<DispatchSemaphore?>(value: nil)
init(thread: Thread) {
self.thread = thread
}
deinit {
assert(Thread.current === self.thread)
self.fetchedDataDisposable.dispose()
self.fetchedFullDataDisposable.dispose()
self.keepDataDisposable.dispose()
self.autosaveDisposable.dispose()
}
func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range<Int64>?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?, storeAfterDownload: (() -> Void)?, isAudioVideoMessage: Bool) {
if self.readingError || self.initializedState != nil {
return
}
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
var streamable = streamable
if limitedFileRange != nil {
streamable = true
}
self.postbox = postbox
self.resourceReference = resourceReference
self.tempFilePath = tempFilePath
self.limitedFileRange = limitedFileRange
self.streamable = streamable
self.statsCategory = video ? .video : .audio
self.userLocation = userLocation
self.userContentType = video ? .video : .audio
switch resourceReference {
case let .media(media, _):
switch media {
case .story:
self.userContentType = .story
default:
break
}
default:
break
}
self.preferSoftwareDecoding = preferSoftwareDecoding
self.fetchAutomatically = fetchAutomatically
self.maximumFetchSize = maximumFetchSize
if self.tempFilePath == nil {
self.keepDataDisposable.set(postbox.mediaBox.keepResource(id: resourceReference.resource.id).start())
}
if let storeAfterDownload = storeAfterDownload {
self.autosaveDisposable.set((postbox.mediaBox.resourceData(resourceReference.resource)
|> take(1)
|> mapToSignal { initialData -> Signal<Bool, NoError> in
if initialData.complete {
return .single(false)
} else {
return postbox.mediaBox.resourceData(resourceReference.resource)
|> filter { $0.complete }
|> take(1)
|> map { _ -> Bool in return true }
}
}
|> deliverOnMainQueue).start(next: { shouldSave in
if shouldSave {
storeAfterDownload()
}
}))
}
if streamable {
if self.tempFilePath == nil && limitedFileRange == nil {
self.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
}
} else if !self.requestedCompleteFetch && self.fetchAutomatically {
self.requestedCompleteFetch = true
if self.tempFilePath == nil {
self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
}
}
var directFilePath: String?
if !streamable && isAudioVideoMessage {
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
let semaphore = DispatchSemaphore(value: 0)
let _ = self.currentSemaphore.swap(semaphore)
var resultFilePath: String?
let disposable = data.start(next: { next in
if next.complete {
resultFilePath = next.path
semaphore.signal()
}
})
semaphore.wait()
let _ = self.currentSemaphore.swap(nil)
disposable.dispose()
if let resultFilePath {
directFilePath = resultFilePath
} else {
self.readingError = true
return
}
}
let avFormatContext = FFMpegAVFormatContext()
var avIoContext: FFMpegAVIOContext?
if directFilePath == nil {
guard let avIoContextValue = FFMpegAVIOContext(bufferSize: 64 * 1024, opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else {
self.readingError = true
return
}
avIoContext = avIoContextValue
avFormatContext.setIO(avIoContextValue)
}
if !avFormatContext.openInput(withDirectFilePath: directFilePath) {
self.readingError = true
return
}
if !avFormatContext.findStreamInfo() {
self.readingError = true;
return
}
var videoStream: StreamContext?
var audioStream: StreamContext?
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
if !isSeekable {
duration = CMTimeMake(value: Int64.min, timescale: duration.timescale)
}
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
let metrics = avFormatContext.metricsForStream(at: streamIndex)
let rotationAngle: Double = metrics.rotationAngle
let aspect = Double(metrics.width) / Double(metrics.height)
if self.preferSoftwareDecoding {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
videoStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect)
break
}
}
}
} else {
var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData?
if codecId == FFMpegCodecIdMPEG4 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdH264 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdHEVC {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdAV1 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
}
if let videoFormatData {
videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect)
}
}
}
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
var codec: FFMpegAVCodec?
if codec == nil {
codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false)
}
if let codec = codec {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
if !isSeekable {
duration = CMTimeMake(value: Int64.min, timescale: duration.timescale)
}
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0)
break
}
}
}
}
self.initializedState = InitializedState(avIoContext: avIoContext, avFormatContext: avFormatContext, audioStream: audioStream, videoStream: videoStream)
if streamable && limitedFileRange == nil {
if self.tempFilePath == nil {
self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start())
}
self.requestedCompleteFetch = true
}
}
private func readPacket() -> FFMpegPacket? {
if !self.packetQueue.isEmpty {
return self.packetQueue.remove(at: 0)
} else {
return self.readPacketInternal()
}
}
private func readPacketInternal() -> FFMpegPacket? {
guard let initializedState = self.initializedState else {
return nil
}
let packet = FFMpegPacket()
if initializedState.avFormatContext.readFrame(into: packet) {
return packet
} else {
return nil
}
}
func takeFrames(until: Double, types: [MediaTrackFrameType]) -> (frames: [MediaTrackDecodableFrame], endOfStream: Bool) {
if self.readingError {
return ([], true)
}
guard let initializedState = self.initializedState else {
return ([], true)
}
var videoTimestamp: Double?
if initializedState.videoStream == nil || !types.contains(.video) {
videoTimestamp = Double.infinity
}
var audioTimestamp: Double?
if initializedState.audioStream == nil || !types.contains(.audio) {
audioTimestamp = Double.infinity
}
var frames: [MediaTrackDecodableFrame] = []
var endOfStream = false
while !self.readingError && ((videoTimestamp == nil || videoTimestamp!.isLess(than: until)) || (audioTimestamp == nil || audioTimestamp!.isLess(than: until))) {
if let packet = self.readPacket() {
if let videoStream = initializedState.videoStream, Int(packet.streamIndex) == videoStream.index {
let frame = videoFrameFromPacket(packet, videoStream: videoStream)
frames.append(frame)
if videoTimestamp == nil || videoTimestamp! < CMTimeGetSeconds(frame.pts) {
videoTimestamp = CMTimeGetSeconds(frame.pts)
//print("read video at \(CMTimeGetSeconds(frame.pts))")
}
} else if let audioStream = initializedState.audioStream, Int(packet.streamIndex) == audioStream.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: audioStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: audioStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * audioStream.timebase.value, timescale: audioStream.timebase.timescale)
} else {
duration = audioStream.fps
}
let frame = MediaTrackDecodableFrame(type: .audio, packet: packet, pts: pts, dts: dts, duration: duration)
frames.append(frame)
if audioTimestamp == nil || audioTimestamp! < CMTimeGetSeconds(pts) {
audioTimestamp = CMTimeGetSeconds(pts)
//print("read audio at \(CMTimeGetSeconds(pts))")
}
}
} else {
endOfStream = true
break
}
}
return (frames, endOfStream)
}
func contextInfo() -> FFMpegMediaFrameSourceContextInfo? {
if let initializedState = self.initializedState {
var audioStreamContext: FFMpegMediaFrameSourceStreamContextInfo?
var videoStreamContext: FFMpegMediaFrameSourceStreamContextInfo?
if let audioStream = initializedState.audioStream {
audioStreamContext = FFMpegMediaFrameSourceStreamContextInfo(duration: audioStream.duration, decoder: audioStream.decoder)
}
if let videoStream = initializedState.videoStream {
videoStreamContext = FFMpegMediaFrameSourceStreamContextInfo(duration: videoStream.duration, decoder: videoStream.decoder)
}
return FFMpegMediaFrameSourceContextInfo(audioStream: audioStreamContext, videoStream: videoStreamContext)
}
return nil
}
func seek(timestamp: Double, completed: ((FFMpegMediaFrameSourceDescriptionSet, CMTime)?) -> Void) {
if let initializedState = self.initializedState {
self.packetQueue.removeAll()
for stream in [initializedState.videoStream, initializedState.audioStream] {
if let stream = stream {
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
#if DEBUG && false
let startTime = CFAbsoluteTimeGetCurrent()
#endif
initializedState.avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true)
#if DEBUG && false
print("Seek time: \(CFAbsoluteTimeGetCurrent() - startTime) s")
#endif
break
}
}
var audioDescription: FFMpegMediaFrameSourceDescription?
var videoDescription: FFMpegMediaFrameSourceDescription?
if let audioStream = initializedState.audioStream {
audioDescription = FFMpegMediaFrameSourceDescription(startTime: audioStream.startTime, duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0)
}
if let videoStream = initializedState.videoStream {
videoDescription = FFMpegMediaFrameSourceDescription(startTime: videoStream.startTime, duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect)
}
var actualPts: CMTime = CMTimeMake(value: 0, timescale: 1)
var extraVideoFrames: [MediaTrackDecodableFrame] = []
if timestamp.isZero || initializedState.videoStream == nil {
for _ in 0 ..< 24 {
if let packet = self.readPacketInternal() {
if let videoStream = initializedState.videoStream, Int(packet.streamIndex) == videoStream.index {
self.packetQueue.append(packet)
let pts = CMTimeMake(value: packet.pts, timescale: videoStream.timebase.timescale)
actualPts = pts
break
} else if let audioStream = initializedState.audioStream, Int(packet.streamIndex) == audioStream.index {
self.packetQueue.append(packet)
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timebase.timescale)
actualPts = pts
break
}
} else {
break
}
}
} else if let videoStream = initializedState.videoStream {
let targetPts = CMTimeMakeWithSeconds(Float64(timestamp), preferredTimescale: videoStream.timebase.timescale)
let limitPts = CMTimeMakeWithSeconds(Float64(timestamp + 0.5), preferredTimescale: videoStream.timebase.timescale)
var audioPackets: [FFMpegPacket] = []
while !self.readingError {
if let packet = self.readPacket() {
if let videoStream = initializedState.videoStream, Int(packet.streamIndex) == videoStream.index {
let frame = videoFrameFromPacket(packet, videoStream: videoStream)
extraVideoFrames.append(frame)
if CMTimeCompare(frame.dts, limitPts) >= 0 && CMTimeCompare(frame.pts, limitPts) >= 0 {
break
}
} else if let audioStream = initializedState.audioStream, Int(packet.streamIndex) == audioStream.index {
audioPackets.append(packet)
}
} else {
break
}
}
if !extraVideoFrames.isEmpty {
var closestFrame: MediaTrackDecodableFrame?
for frame in extraVideoFrames {
if CMTimeCompare(frame.pts, targetPts) >= 0 {
if let closestFrameValue = closestFrame {
if CMTimeCompare(frame.pts, closestFrameValue.pts) < 0 {
closestFrame = frame
}
} else {
closestFrame = frame
}
}
}
if let closestFrame = closestFrame {
actualPts = closestFrame.pts
} else {
if let videoStream = initializedState.videoStream {
actualPts = videoStream.duration
} else {
actualPts = extraVideoFrames.last!.pts
}
}
}
if let audioStream = initializedState.audioStream {
self.packetQueue.append(contentsOf: audioPackets.filter({ packet in
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timebase.timescale)
if CMTimeCompare(pts, actualPts) >= 0 {
return true
} else {
return false
}
}))
}
}
completed((FFMpegMediaFrameSourceDescriptionSet(audio: audioDescription, video: videoDescription, extraVideoFrames: extraVideoFrames), actualPts))
} else {
completed(nil)
}
}
func close() {
self.closed = true
}
}
private func videoFrameFromPacket(_ packet: FFMpegPacket, videoStream: StreamContext) -> MediaTrackDecodableFrame {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale)
} else {
duration = CMTimeMake(value: Int64(videoStream.fps.timescale), timescale: Int32(videoStream.fps.value))
}
return MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
}
@@ -0,0 +1,122 @@
import Foundation
import CoreMedia
import FFMpegBinding
public final class FFMpegMediaFrameSourceContextHelpers {
public static let registerFFMpegGlobals: Void = {
FFMpegGlobals.initializeGlobals()
return
}()
static func createFormatDescriptionFromAVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: Data) -> CMFormatDescription? {
let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
let atoms = NSMutableDictionary()
atoms.setObject(extradata as NSData, forKey: "avcC" as NSString)
let extensions = NSMutableDictionary()
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
extensions.setObject("avc1" as NSString, forKey: "FormatName" as NSString)
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
var formatDescription: CMFormatDescription?
CMVideoFormatDescriptionCreate(allocator: nil, codecType: CMVideoCodecType(formatId), width: width, height: height, extensions: extensions, formatDescriptionOut: &formatDescription)
return formatDescription
}
static func createFormatDescriptionFromMpeg4CodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: Data) -> CMFormatDescription? {
let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
let atoms = NSMutableDictionary()
atoms.setObject(extradata as NSData, forKey: "esds" as NSString)
let extensions = NSMutableDictionary()
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
extensions.setObject("mp4v" as NSString, forKey: "FormatName" as NSString)
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
//extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
var formatDescription: CMFormatDescription?
guard CMVideoFormatDescriptionCreate(allocator: nil, codecType: kCMVideoCodecType_MPEG4Video, width: width, height: height, extensions: extensions, formatDescriptionOut: &formatDescription) == noErr else {
return nil
}
return formatDescription
}
static func createFormatDescriptionFromHEVCCodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: Data) -> CMFormatDescription? {
let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
let atoms = NSMutableDictionary()
atoms.setObject(extradata as NSData, forKey: "hvcC" as NSString)
let extensions = NSMutableDictionary()
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
extensions.setObject("hevc" as NSString, forKey: "FormatName" as NSString)
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
var formatDescription: CMFormatDescription?
CMVideoFormatDescriptionCreate(allocator: nil, codecType: CMVideoCodecType(formatId), width: width, height: height, extensions: extensions, formatDescriptionOut: &formatDescription)
return formatDescription
}
static func createFormatDescriptionFromAV1CodecData(_ formatId: UInt32, _ width: Int32, _ height: Int32, _ extradata: Data, frameData: Data) -> CMFormatDescription? {
return nil
/*let par = NSMutableDictionary()
par.setObject(1 as NSNumber, forKey: "HorizontalSpacing" as NSString)
par.setObject(1 as NSNumber, forKey: "VerticalSpacing" as NSString)
let atoms = NSMutableDictionary()
atoms.setObject(extradata as NSData, forKey: "av1C" as NSString)
let extensions = NSMutableDictionary()
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationBottomField" as NSString)
extensions.setObject("left" as NSString, forKey: "CVImageBufferChromaLocationTopField" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(par, forKey: "CVPixelAspectRatio" as NSString)
extensions.setObject(atoms, forKey: "SampleDescriptionExtensionAtoms" as NSString)
extensions.setObject("hevc" as NSString, forKey: "FormatName" as NSString)
extensions.setObject(0 as NSNumber, forKey: "SpatialQuality" as NSString)
extensions.setObject(0 as NSNumber, forKey: "Version" as NSString)
extensions.setObject(0 as NSNumber, forKey: "FullRangeVideo" as NSString)
extensions.setObject(1 as NSNumber, forKey: "CVFieldCount" as NSString)
extensions.setObject(24 as NSNumber, forKey: "Depth" as NSString)
var formatDescription: CMFormatDescription?
CMVideoFormatDescriptionCreate(allocator: nil, codecType: CMVideoCodecType(formatId), width: width, height: height, extensions: extensions, formatDescriptionOut: &formatDescription)
return formatDescription*/
}
}
@@ -0,0 +1,100 @@
import CoreMedia
final class FFMpegMediaPassthroughVideoFrameDecoder: MediaTrackFrameDecoder {
final class VideoFormatData {
let codecType: CMVideoCodecType
let width: Int32
let height: Int32
let extraData: Data
init(codecType: CMVideoCodecType, width: Int32, height: Int32, extraData: Data) {
self.codecType = codecType
self.width = width
self.height = height
self.extraData = extraData
}
}
private let videoFormatData: VideoFormatData
private var videoFormat: CMVideoFormatDescription?
private let rotationAngle: Double
private var resetDecoderOnNextFrame = true
private var sentFrameQueue: [MediaTrackDecodableFrame] = []
init(videoFormatData: VideoFormatData, rotationAngle: Double) {
self.videoFormatData = videoFormatData
self.rotationAngle = rotationAngle
}
func send(frame: MediaTrackDecodableFrame) -> Bool {
self.sentFrameQueue.append(frame)
return true
}
func decode() -> MediaTrackFrame? {
guard let frame = self.sentFrameQueue.first else {
return nil
}
self.sentFrameQueue.removeFirst()
if self.videoFormat == nil {
if self.videoFormatData.codecType == kCMVideoCodecType_MPEG4Video {
self.videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), self.videoFormatData.width, self.videoFormatData.height, self.videoFormatData.extraData)
} else if self.videoFormatData.codecType == kCMVideoCodecType_H264 {
self.videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), self.videoFormatData.width, self.videoFormatData.height, self.videoFormatData.extraData)
} else if self.videoFormatData.codecType == kCMVideoCodecType_HEVC {
self.videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromHEVCCodecData(UInt32(kCMVideoCodecType_HEVC), self.videoFormatData.width, self.videoFormatData.height, self.videoFormatData.extraData)
} else if self.videoFormatData.codecType == kCMVideoCodecType_AV1 {
self.videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAV1CodecData(UInt32(kCMVideoCodecType_AV1), self.videoFormatData.width, self.videoFormatData.height, self.videoFormatData.extraData, frameData: frame.copyPacketData())
}
}
if self.videoFormat == nil {
return nil
}
var blockBuffer: CMBlockBuffer?
let bytes = malloc(Int(frame.packet.size))!
memcpy(bytes, frame.packet.data, Int(frame.packet.size))
guard CMBlockBufferCreateWithMemoryBlock(allocator: nil, memoryBlock: bytes, blockLength: Int(frame.packet.size), blockAllocator: nil, customBlockSource: nil, offsetToData: 0, dataLength: Int(frame.packet.size), flags: 0, blockBufferOut: &blockBuffer) == noErr else {
free(bytes)
return nil
}
var timingInfo = CMSampleTimingInfo(duration: frame.duration, presentationTimeStamp: frame.pts, decodeTimeStamp: frame.dts)
var sampleBuffer: CMSampleBuffer?
var sampleSize = Int(frame.packet.size)
guard CMSampleBufferCreate(allocator: nil, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: self.videoFormat, sampleCount: 1, sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleSizeEntryCount: 1, sampleSizeArray: &sampleSize, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
let resetDecoder = self.resetDecoderOnNextFrame
if self.resetDecoderOnNextFrame {
self.resetDecoderOnNextFrame = false
let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString as String)
}
return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: false, rotationAngle: self.rotationAngle)
}
func takeQueuedFrame() -> MediaTrackFrame? {
return nil
}
func takeRemainingFrame() -> MediaTrackFrame? {
return nil
}
func reset() {
self.resetDecoderOnNextFrame = true
}
func sendEndToDecoder() -> Bool {
return true
}
}
@@ -0,0 +1,515 @@
#if !os(macOS)
import UIKit
#else
import AppKit
import TGUIKit
#endif
import CoreMedia
import Accelerate
import FFMpegBinding
import YuvConversion
private let bufferCount = 32
#if os(macOS)
private let deviceColorSpace: CGColorSpace = {
if #available(OSX 10.11.2, *) {
if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) {
return colorSpace
} else {
return CGColorSpaceCreateDeviceRGB()
}
} else {
return CGColorSpaceCreateDeviceRGB()
}
}()
#else
private let deviceColorSpace: CGColorSpace = {
if #available(iOSApplicationExtension 9.3, iOS 9.3, *) {
if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) {
return colorSpace
} else {
return CGColorSpaceCreateDeviceRGB()
}
} else {
return CGColorSpaceCreateDeviceRGB()
}
}()
#endif
public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
public enum ReceiveResult {
case error
case moreDataNeeded
case result(MediaTrackFrame)
}
private let codecContext: FFMpegAVCodecContext
private let videoFrame: FFMpegAVFrame
private var resetDecoderOnNextFrame = true
private var isError = false
private var defaultDuration: CMTime?
private var defaultTimescale: CMTimeScale?
private var pixelBufferPool: CVPixelBufferPool?
private var delayedFrames: [MediaTrackFrame] = []
private var uvPlane: (UnsafeMutablePointer<UInt8>, Int)?
public init(codecContext: FFMpegAVCodecContext) {
self.codecContext = codecContext
self.videoFrame = FFMpegAVFrame()
}
deinit {
if let (dstPlane, _) = self.uvPlane {
free(dstPlane)
}
}
func decodeInternal(frame: MediaTrackDecodableFrame) {
}
public func decode() -> MediaTrackFrame? {
return self.decode(ptsOffset: nil)
}
public func sendToDecoder(frame: MediaTrackDecodableFrame) -> Bool {
self.defaultDuration = frame.duration
self.defaultTimescale = frame.pts.timescale
let status = frame.packet.send(toDecoder: self.codecContext)
return status == 0
}
public func sendEndToDecoder() -> Bool {
return self.codecContext.sendEnd()
}
public func receiveFromDecoder(ptsOffset: CMTime?, displayImmediately: Bool = true) -> ReceiveResult {
if self.isError {
return .error
}
guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else {
return .error
}
let receiveResult = self.codecContext.receive(into: self.videoFrame)
switch receiveResult {
case .success:
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
self.isError = true
return .error
}
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
if let ptsOffset = ptsOffset {
pts = CMTimeAdd(pts, ptsOffset)
}
if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration, displayImmediately: displayImmediately) {
return .result(convertedFrame)
} else {
return .error
}
case .notEnoughData:
return .moreDataNeeded
case .error:
return .error
@unknown default:
return .error
}
}
public func send(frame: MediaTrackDecodableFrame) -> Bool {
let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 {
self.defaultDuration = frame.duration
self.defaultTimescale = frame.pts.timescale
return true
} else {
return false
}
}
public func decode(ptsOffset: CMTime?, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? {
if self.isError {
return nil
}
guard let defaultDuration = self.defaultDuration, let defaultTimescale = self.defaultTimescale else {
return nil
}
if self.codecContext.receive(into: self.videoFrame) == .success {
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
self.isError = true
return nil
}
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
if let ptsOffset = ptsOffset {
pts = CMTimeAdd(pts, ptsOffset)
}
return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: defaultDuration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha, displayImmediately: displayImmediately)
}
return nil
}
public func receiveRemainingFrames(ptsOffset: CMTime?) -> [MediaTrackFrame] {
guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else {
return []
}
if self.isError {
return []
}
var result: [MediaTrackFrame] = []
result.append(contentsOf: self.delayedFrames)
self.delayedFrames.removeAll()
while true {
if case .success = self.codecContext.receive(into: self.videoFrame) {
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
self.isError = true
return []
}
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
if let ptsOffset = ptsOffset {
pts = CMTimeAdd(pts, ptsOffset)
}
if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) {
result.append(convertedFrame)
}
} else {
break
}
}
return result
}
public func render(frame: MediaTrackDecodableFrame) -> UIImage? {
let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 {
if case .success = self.codecContext.receive(into: self.videoFrame) {
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
self.isError = true
return nil
}
return convertVideoFrameToImage(self.videoFrame)
}
}
return nil
}
public func takeQueuedFrame() -> MediaTrackFrame? {
return nil
}
public func takeRemainingFrame() -> MediaTrackFrame? {
if !self.delayedFrames.isEmpty {
var minFrameIndex = 0
var minPosition = self.delayedFrames[0].position
for i in 1 ..< self.delayedFrames.count {
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
minFrameIndex = i
minPosition = self.delayedFrames[i].position
}
}
return self.delayedFrames.remove(at: minFrameIndex)
} else {
return nil
}
}
private func convertVideoFrameToImage(_ frame: FFMpegAVFrame) -> UIImage? {
var info = vImage_YpCbCrToARGB()
var pixelRange: vImage_YpCbCrPixelRange
switch frame.colorRange {
case .full:
pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 0, CbCr_bias: 128, YpRangeMax: 255, CbCrRangeMax: 255, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0)
default:
pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0)
}
var result = kvImageNoError
result = vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0)
if result != kvImageNoError {
return nil
}
var srcYp = vImage_Buffer(data: frame.data[0], height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width), rowBytes: Int(frame.lineSize[0]))
var srcCb = vImage_Buffer(data: frame.data[1], height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width / 2), rowBytes: Int(frame.lineSize[1]))
var srcCr = vImage_Buffer(data: frame.data[2], height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width / 2), rowBytes: Int(frame.lineSize[2]))
let argbBytesPerRow = (4 * Int(frame.width) + 31) & (~31)
let argbLength = argbBytesPerRow * Int(frame.height)
let argb = malloc(argbLength)!
guard let provider = CGDataProvider(dataInfo: argb, data: argb, size: argbLength, releaseData: { bytes, _, _ in
free(bytes)
}) else {
return nil
}
var dst = vImage_Buffer(data: argb, height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width), rowBytes: argbBytesPerRow)
var permuteMap: [UInt8] = [3, 2, 1, 0]
result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&srcYp, &srcCb, &srcCr, &dst, &info, &permuteMap, 0x00, 0)
if result != kvImageNoError {
return nil
}
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue)
guard let image = CGImage(width: Int(frame.width), height: Int(frame.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: argbBytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) else {
return nil
}
return UIImage(cgImage: image, scale: 1.0, orientation: .up)
}
private func convertVideoFrame(_ frame: FFMpegAVFrame, pts: CMTime, dts: CMTime, duration: CMTime, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? {
if frame.nativePixelFormat() == FFMpegAVFrameNativePixelFormat.videoToolbox {
guard let pixelBufferRef = frame.data[3] else {
return nil
}
let unmanagedPixelBuffer = Unmanaged<CVPixelBuffer>.fromOpaque(UnsafeRawPointer(pixelBufferRef))
let pixelBuffer = unmanagedPixelBuffer.takeUnretainedValue()
var formatRef: CMVideoFormatDescription?
let formatStatus = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatRef)
guard let format = formatRef, formatStatus == 0 else {
return nil
}
var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts)
var sampleBuffer: CMSampleBuffer?
guard CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
let resetDecoder = self.resetDecoderOnNextFrame
if self.resetDecoderOnNextFrame {
self.resetDecoderOnNextFrame = false
//dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString as String)
}
if displayImmediately {
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)
}
return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true)
}
if frame.data[0] == nil {
return nil
}
if frame.lineSize[1] != frame.lineSize[2] {
return nil
}
var pixelBufferRef: CVPixelBuffer?
let pixelFormat: OSType
var hasAlpha = false
if forceARGB {
pixelFormat = kCVPixelFormatType_32ARGB
switch frame.pixelFormat {
case .YUV:
hasAlpha = false
case .YUVA:
hasAlpha = true
default:
hasAlpha = false
}
} else {
switch frame.pixelFormat {
case .YUV:
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
case .YUVA:
pixelFormat = kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar
default:
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
}
}
if let pixelBufferPool = self.pixelBufferPool {
let auxAttributes: [String: Any] = [kCVPixelBufferPoolAllocationThresholdKey as String: bufferCount as NSNumber];
let err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pixelBufferPool, auxAttributes as CFDictionary, &pixelBufferRef)
if err == kCVReturnWouldExceedAllocationThreshold {
print("kCVReturnWouldExceedAllocationThreshold, dropping frame")
return nil
}
} else {
let ioSurfaceProperties = NSMutableDictionary()
ioSurfaceProperties["IOSurfaceIsGlobal"] = true as NSNumber
var options: [String: Any] = [kCVPixelBufferBytesPerRowAlignmentKey as String: frame.lineSize[0] as NSNumber]
options[kCVPixelBufferIOSurfacePropertiesKey as String] = ioSurfaceProperties
CVPixelBufferCreate(kCFAllocatorDefault,
Int(frame.width),
Int(frame.height),
pixelFormat,
options as CFDictionary,
&pixelBufferRef)
}
guard let pixelBuffer = pixelBufferRef else {
return nil
}
let status = CVPixelBufferLockBaseAddress(pixelBuffer, [])
if status != kCVReturnSuccess {
return nil
}
var base: UnsafeMutableRawPointer
if pixelFormat == kCVPixelFormatType_32ARGB {
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
decodeYUVAPlanesToRGBA(frame.data[0], Int32(frame.lineSize[0]), frame.data[1], Int32(frame.lineSize[1]), frame.data[2], Int32(frame.lineSize[2]), hasAlpha, frame.data[3], CVPixelBufferGetBaseAddress(pixelBuffer)?.assumingMemoryBound(to: UInt8.self), Int32(frame.width), Int32(frame.height), Int32(bytesPerRow), unpremultiplyAlpha)
} else {
let srcPlaneSize = Int(frame.lineSize[1]) * Int(frame.height / 2)
let uvPlaneSize = srcPlaneSize * 2
let uvPlane: UnsafeMutablePointer<UInt8>
if let (existingUvPlane, existingUvPlaneSize) = self.uvPlane, existingUvPlaneSize == uvPlaneSize {
uvPlane = existingUvPlane
} else {
if let (existingDstPlane, _) = self.uvPlane {
free(existingDstPlane)
}
uvPlane = malloc(uvPlaneSize)!.assumingMemoryBound(to: UInt8.self)
self.uvPlane = (uvPlane, uvPlaneSize)
}
fillDstPlane(uvPlane, frame.data[1]!, frame.data[2]!, srcPlaneSize)
let bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
let bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
let bytesPerRowA = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)
var requiresAlphaMultiplication = false
if pixelFormat == kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar {
requiresAlphaMultiplication = true
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!
if bytesPerRowA == frame.lineSize[3] {
memcpy(base, frame.data[3]!, bytesPerRowA * Int(frame.height))
} else {
var dest = base
var src = frame.data[3]!
let lineSize = Int(frame.lineSize[3])
for _ in 0 ..< Int(frame.height) {
memcpy(dest, src, lineSize)
dest = dest.advanced(by: bytesPerRowA)
src = src.advanced(by: lineSize)
}
}
}
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!
if bytesPerRowY == frame.lineSize[0] {
memcpy(base, frame.data[0]!, bytesPerRowY * Int(frame.height))
} else {
var dest = base
var src = frame.data[0]!
let lineSize = Int(frame.lineSize[0])
for _ in 0 ..< Int(frame.height) {
memcpy(dest, src, lineSize)
dest = dest.advanced(by: bytesPerRowY)
src = src.advanced(by: lineSize)
}
}
if requiresAlphaMultiplication {
var y = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowY)
var a = vImage_Buffer(data: CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2)!, height: vImagePixelCount(frame.height), width: vImagePixelCount(bytesPerRowY), rowBytes: bytesPerRowA)
let _ = vImagePremultiplyData_Planar8(&y, &a, &y, vImage_Flags(kvImageDoNotTile))
}
base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)!
if bytesPerRowUV == frame.lineSize[1] * 2 {
memcpy(base, uvPlane, Int(frame.height / 2) * bytesPerRowUV)
} else {
var dest = base
var src = uvPlane
let lineSize = Int(frame.lineSize[1]) * 2
for _ in 0 ..< Int(frame.height / 2) {
memcpy(dest, src, lineSize)
dest = dest.advanced(by: bytesPerRowUV)
src = src.advanced(by: lineSize)
}
}
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
var formatRef: CMVideoFormatDescription?
let formatStatus = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatRef)
guard let format = formatRef, formatStatus == 0 else {
return nil
}
var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts)
var sampleBuffer: CMSampleBuffer?
guard CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
let resetDecoder = self.resetDecoderOnNextFrame
if self.resetDecoderOnNextFrame {
self.resetDecoderOnNextFrame = false
//dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString as String)
}
if displayImmediately {
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)
}
let decodedFrame = MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true)
self.delayedFrames.append(decodedFrame)
if self.delayedFrames.count >= 1 {
var minFrameIndex = 0
var minPosition = self.delayedFrames[0].position
for i in 1 ..< self.delayedFrames.count {
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
minFrameIndex = i
minPosition = self.delayedFrames[i].position
}
}
if minFrameIndex != 0 {
assert(true)
}
return self.delayedFrames.remove(at: minFrameIndex)
} else {
return nil
}
}
public func reset() {
self.codecContext.flushBuffers()
self.resetDecoderOnNextFrame = true
}
}
@@ -0,0 +1,316 @@
import Foundation
import AVFoundation
import CoreMedia
import FFMpegBinding
import VideoToolbox
import Postbox
#if os(macOS)
public let internal_isHardwareAv1Supported: Bool = {
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
return value
}()
#endif
public enum MediaDataReaderReadSampleBufferResult {
case frame(CMSampleBuffer)
case waitingForMoreData
case endOfStream
case error
}
public protocol MediaDataReader: AnyObject {
var hasVideo: Bool { get }
var hasAudio: Bool { get }
func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult
}
public final class FFMpegMediaDataReaderV2: MediaDataReader {
public enum Content {
case tempFile(ChunkMediaPlayerPart.TempFile)
case directStream(ChunkMediaPlayerPartsState.DirectReader.Stream)
}
private let content: Content
private let isVideo: Bool
private let videoSource: FFMpegFileReader?
private let audioSource: FFMpegFileReader?
public var hasVideo: Bool {
return self.videoSource != nil
}
public var hasAudio: Bool {
return self.audioSource != nil
}
public init(content: Content, isVideo: Bool, codecName: String?) {
self.content = content
self.isVideo = isVideo
let source: FFMpegFileReader.SourceDescription
var seek: FFMpegFileReader.Seek?
var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
switch content {
case let .tempFile(tempFile):
source = .file(tempFile.file.path)
case let .directStream(directStream):
let mappedRanges: [Range<Int64>]
#if DEBUG && false
var mappedRangesValue: [Range<Int64>] = []
var testOffset: Int64 = 0
while testOffset < directStream.size {
let testBlock: Int64 = min(3 * 1024 + 1, directStream.size - testOffset)
mappedRangesValue.append(testOffset ..< (testOffset + testBlock))
testOffset += testBlock
}
mappedRanges = mappedRangesValue
#else
mappedRanges = [0 ..< directStream.size]
#endif
source = .resource(mediaBox: directStream.mediaBox, resource: directStream.resource, resourceSize: directStream.size, mappedRanges: mappedRanges)
seek = .stream(streamIndex: directStream.seek.streamIndex, pts: directStream.seek.pts)
maxReadablePts = directStream.maxReadablePts
}
if self.isVideo {
var passthroughDecoder = true
var useHardwareAcceleration = false
if (codecName == "h264" || codecName == "hevc") {
passthroughDecoder = false
#if targetEnvironment(simulator)
useHardwareAcceleration = false
#else
useHardwareAcceleration = true
#endif
}
if (codecName == "av1" || codecName == "av01") {
passthroughDecoder = false
useHardwareAcceleration = internal_isHardwareAv1Supported
}
if codecName == "vp9" || codecName == "vp8" {
passthroughDecoder = false
}
/*#if DEBUG
if codecName == "h264" {
passthroughDecoder = false
useHardwareAcceleration = true
}
#endif*/
if let videoSource = FFMpegFileReader(source: source, passthroughDecoder: passthroughDecoder, useHardwareAcceleration: useHardwareAcceleration, selectedStream: .mediaType(.video), seek: seek, maxReadablePts: maxReadablePts) {
self.videoSource = videoSource
} else {
self.videoSource = nil
}
self.audioSource = nil
} else {
if let audioSource = FFMpegFileReader(source: source, passthroughDecoder: false, useHardwareAcceleration: false, selectedStream: .mediaType(.audio), seek: seek, maxReadablePts: maxReadablePts) {
self.audioSource = audioSource
} else {
self.audioSource = nil
}
self.videoSource = nil
}
}
public func update(content: Content) {
guard case let .directStream(directStream) = content else {
return
}
if let audioSource = self.audioSource {
audioSource.updateMaxReadablePts(pts: directStream.maxReadablePts)
} else if let videoSource = self.videoSource {
videoSource.updateMaxReadablePts(pts: directStream.maxReadablePts)
}
}
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
if let videoSource {
switch videoSource.readFrame() {
case let .frame(frame):
return .frame(frame.sampleBuffer)
case .waitingForMoreData:
return .waitingForMoreData
case .endOfStream:
return .endOfStream
case .error:
return .error
}
} else if let audioSource {
switch audioSource.readFrame() {
case let .frame(frame):
return .frame(frame.sampleBuffer)
case .waitingForMoreData:
return .waitingForMoreData
case .endOfStream:
return .endOfStream
case .error:
return .error
}
} else {
return .endOfStream
}
}
}
public final class FFMpegMediaDataReaderV1: MediaDataReader {
private let isVideo: Bool
private let videoSource: SoftwareVideoReader?
private let audioSource: SoftwareAudioSource?
public var hasVideo: Bool {
return self.videoSource != nil
}
public var hasAudio: Bool {
return self.audioSource != nil
}
public init(filePath: String, isVideo: Bool, codecName: String?) {
self.isVideo = isVideo
if self.isVideo {
var passthroughDecoder = true
if (codecName == "av1" || codecName == "av01") && !internal_isHardwareAv1Supported {
passthroughDecoder = false
}
let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder)
if videoSource.hasStream {
self.videoSource = videoSource
} else {
self.videoSource = nil
}
self.audioSource = nil
} else {
let audioSource = SoftwareAudioSource(path: filePath)
if audioSource.hasStream {
self.audioSource = audioSource
} else {
self.audioSource = nil
}
self.videoSource = nil
}
}
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
if let videoSource {
let frame = videoSource.readFrame()
if let frame {
return .frame(frame.sampleBuffer)
} else {
return .endOfStream
}
} else if let audioSource {
if let sampleBuffer = audioSource.readSampleBuffer() {
return .frame(sampleBuffer)
} else {
return .endOfStream
}
} else {
return .endOfStream
}
}
}
public final class AVAssetVideoDataReader: MediaDataReader {
private let isVideo: Bool
private var mediaInfo: FFMpegMediaInfo.Info?
private var assetReader: AVAssetReader?
private var assetOutput: AVAssetReaderOutput?
public var hasVideo: Bool {
return self.assetOutput != nil
}
public var hasAudio: Bool {
return false
}
public init(filePath: String, isVideo: Bool) {
self.isVideo = isVideo
if self.isVideo {
guard let video = extractFFMpegMediaInfo(path: filePath)?.video else {
return
}
self.mediaInfo = video
let asset = AVURLAsset(url: URL(fileURLWithPath: filePath))
guard let assetReader = try? AVAssetReader(asset: asset) else {
return
}
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
return
}
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: [String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange])
assetReader.add(videoOutput)
if assetReader.startReading() {
self.assetReader = assetReader
self.assetOutput = videoOutput
}
}
}
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
guard let mediaInfo = self.mediaInfo, let assetReader = self.assetReader, let assetOutput = self.assetOutput else {
return .endOfStream
}
var retryCount = 0
while true {
if let sampleBuffer = assetOutput.copyNextSampleBuffer() {
if let convertedSampleBuffer = createSampleBuffer(fromSampleBuffer: sampleBuffer, withTimeOffset: mediaInfo.startTime, duration: nil) {
return .frame(convertedSampleBuffer)
} else {
return .endOfStream
}
} else if assetReader.status == .reading && retryCount < 100 {
Thread.sleep(forTimeInterval: 1.0 / 60.0)
retryCount += 1
} else {
break
}
}
return .endOfStream
}
}
private func createSampleBuffer(fromSampleBuffer sampleBuffer: CMSampleBuffer, withTimeOffset timeOffset: CMTime, duration: CMTime?) -> CMSampleBuffer? {
var itemCount: CMItemCount = 0
var status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &itemCount)
if status != 0 {
return nil
}
var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(duration: CMTimeMake(value: 0, timescale: 0), presentationTimeStamp: CMTimeMake(value: 0, timescale: 0), decodeTimeStamp: CMTimeMake(value: 0, timescale: 0)), count: itemCount)
status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: itemCount, arrayToFill: &timingInfo, entriesNeededOut: &itemCount)
if status != 0 {
return nil
}
if let dur = duration {
for i in 0 ..< itemCount {
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
timingInfo[i].duration = dur
}
} else {
for i in 0 ..< itemCount {
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
}
}
var sampleBufferOffset: CMSampleBuffer?
CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: itemCount, sampleTimingArray: &timingInfo, sampleBufferOut: &sampleBufferOffset)
if let output = sampleBufferOffset {
return output
} else {
return nil
}
}
@@ -0,0 +1,31 @@
import Foundation
import SwiftSignalKit
import CoreMedia
public enum MediaTrackEvent {
case frames([MediaTrackDecodableFrame])
case endOfStream
}
public final class MediaFrameSourceSeekResult {
public let buffers: MediaPlaybackBuffers
public let extraDecodedVideoFrames: [MediaTrackFrame]
public let timestamp: CMTime
public init(buffers: MediaPlaybackBuffers, extraDecodedVideoFrames: [MediaTrackFrame], timestamp: CMTime) {
self.buffers = buffers
self.extraDecodedVideoFrames = extraDecodedVideoFrames
self.timestamp = timestamp
}
}
public enum MediaFrameSourceSeekError {
case generic
}
public protocol MediaFrameSource {
func addEventSink(_ f: @escaping (MediaTrackEvent) -> Void) -> Int
func removeEventSink(_ index: Int)
func generateFrames(until timestamp: Double, types: [MediaTrackFrameType])
func seek(timestamp: Double) -> Signal<QueueLocalObject<MediaFrameSourceSeekResult>, MediaFrameSourceSeekError>
}
@@ -0,0 +1,12 @@
import Foundation
import SwiftSignalKit
public final class MediaPlaybackBuffers {
public let audioBuffer: MediaTrackFrameBuffer?
public let videoBuffer: MediaTrackFrameBuffer?
public init(audioBuffer: MediaTrackFrameBuffer?, videoBuffer: MediaTrackFrameBuffer?) {
self.audioBuffer = audioBuffer
self.videoBuffer = videoBuffer
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,992 @@
import Foundation
import SwiftSignalKit
import CoreMedia
import AVFoundation
import TelegramCore
import TelegramAudio
private enum AudioPlayerRendererState {
case paused
case playing(rate: Double, didSetRate: Bool)
}
private final class AudioPlayerRendererBufferContext {
var state: AudioPlayerRendererState = .paused
let timebase: CMTimebase
let buffer: RingByteBuffer
var audioLevelPeak: Int16 = 0
var audioLevelPeakCount: Int = 0
var audioLevelPeakUpdate: Double = 0.0
var bufferMaxChannelSampleIndex: Int64 = 0
var lowWaterSize: Int
var notifyLowWater: () -> Void
var updatedRate: () -> Void
var updatedLevel: (Float) -> Void
var notifiedLowWater = false
var overflowData = Data()
var overflowDataMaxChannelSampleIndex: Int64 = 0
var renderTimestampTick: Int64 = 0
init(timebase: CMTimebase, buffer: RingByteBuffer, lowWaterSize: Int, notifyLowWater: @escaping () -> Void, updatedRate: @escaping () -> Void, updatedLevel: @escaping (Float) -> Void) {
self.timebase = timebase
self.buffer = buffer
self.lowWaterSize = lowWaterSize
self.notifyLowWater = notifyLowWater
self.updatedRate = updatedRate
self.updatedLevel = updatedLevel
}
}
private let audioPlayerRendererBufferContextMap = Atomic<[Int32: Atomic<AudioPlayerRendererBufferContext>]>(value: [:])
private let audioPlayerRendererQueue = Queue()
private var _nextPlayerRendererBufferContextId: Int32 = 1
private func registerPlayerRendererBufferContext(_ context: Atomic<AudioPlayerRendererBufferContext>) -> Int32 {
var id: Int32 = 0
let _ = audioPlayerRendererBufferContextMap.modify { contextMap in
id = _nextPlayerRendererBufferContextId
_nextPlayerRendererBufferContextId += 1
var contextMap = contextMap
contextMap[id] = context
return contextMap
}
return id
}
private func unregisterPlayerRendererBufferContext(_ id: Int32) {
let _ = audioPlayerRendererBufferContextMap.modify { contextMap in
var contextMap = contextMap
let _ = contextMap.removeValue(forKey: id)
return contextMap
}
}
private func withPlayerRendererBuffer(_ id: Int32, _ f: (Atomic<AudioPlayerRendererBufferContext>) -> Void) {
audioPlayerRendererBufferContextMap.with { contextMap in
if let context = contextMap[id] {
f(context)
}
}
}
private let kOutputBus: UInt32 = 0
private let kInputBus: UInt32 = 1
private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus {
guard let ioData = ioData else {
return noErr
}
let bufferList = UnsafeMutableAudioBufferListPointer(ioData)
var rendererFillOffset = (0, 0)
var notifyLowWater: (() -> Void)?
var updatedRate: (() -> Void)?
withPlayerRendererBuffer(Int32(intptr_t(bitPattern: refCon)), { context in
context.with { context in
switch context.state {
case let .playing(rate, didSetRate):
if context.buffer.availableBytes != 0 {
let sampleIndex = context.bufferMaxChannelSampleIndex - Int64(context.buffer.availableBytes / (2 *
2))
if !didSetRate {
context.state = .playing(rate: rate, didSetRate: true)
let masterClock = CMTimebaseCopySource(context.timebase)
let anchorTime = CMTimeMake(value: sampleIndex, timescale: 44100)
let immediateSourceTime = CMSyncGetTime(masterClock)
if anchorTime.seconds < CMTimebaseGetTime(context.timebase).seconds - 0.5 {
assert(true)
}
CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: anchorTime, immediateSourceTime: immediateSourceTime)
updatedRate = context.updatedRate
} else {
context.renderTimestampTick += 1
if context.renderTimestampTick % 1000 == 0 {
let delta = (Double(sampleIndex) / 44100.0) - CMTimeGetSeconds(CMTimebaseGetTime(context.timebase))
if delta > 0.01 {
CMTimebaseSetTime(context.timebase, time: CMTimeMake(value: sampleIndex, timescale: 44100))
updatedRate = context.updatedRate
}
}
}
let rendererBuffer = context.buffer
while rendererFillOffset.0 < bufferList.count {
if let bufferData = bufferList[rendererFillOffset.0].mData {
let bufferDataSize = Int(bufferList[rendererFillOffset.0].mDataByteSize)
let dataOffset = rendererFillOffset.1
if dataOffset == bufferDataSize {
rendererFillOffset = (rendererFillOffset.0 + 1, 0)
continue
}
let consumeCount = bufferDataSize - dataOffset
let actualConsumedCount = rendererBuffer.dequeue(bufferData.advanced(by: dataOffset), count: consumeCount)
var samplePtr = bufferData.advanced(by: dataOffset).assumingMemoryBound(to: Int16.self)
for _ in 0 ..< actualConsumedCount / 4 {
var sample: Int16 = samplePtr.pointee
if sample < 0 {
if sample <= -32768 {
sample = Int16.max
} else {
sample = -sample
}
}
samplePtr = samplePtr.advanced(by: 2)
if context.audioLevelPeak < sample {
context.audioLevelPeak = sample
}
context.audioLevelPeakCount += 1
if context.audioLevelPeakCount >= 1200 {
let level = Float(context.audioLevelPeak) / (4000.0)
/*let timestamp = CFAbsoluteTimeGetCurrent()
if !context.audioLevelPeakUpdate.isZero {
let delta = timestamp - context.audioLevelPeakUpdate
print("level = \(level), delta = \(delta)")
}
context.audioLevelPeakUpdate = timestamp*/
context.updatedLevel(level)
context.audioLevelPeak = 0
context.audioLevelPeakCount = 0
}
}
rendererFillOffset.1 += actualConsumedCount
if actualConsumedCount == 0 {
break
}
} else {
break
}
}
} else {
#if DEBUG
print("No audio data")
#endif
}
if !context.notifiedLowWater {
let availableBytes = context.buffer.availableBytes
if availableBytes <= context.lowWaterSize {
context.notifiedLowWater = true
notifyLowWater = context.notifyLowWater
}
}
case .paused:
break
}
}
})
for i in rendererFillOffset.0 ..< bufferList.count {
var dataOffset = 0
if i == rendererFillOffset.0 {
dataOffset = rendererFillOffset.1
}
if let data = bufferList[i].mData {
memset(data.advanced(by: dataOffset), 0, Int(bufferList[i].mDataByteSize) - dataOffset)
}
}
if let notifyLowWater = notifyLowWater {
notifyLowWater()
}
if let updatedRate = updatedRate {
updatedRate()
}
return noErr
}
private struct RequestingFramesContext {
let queue: DispatchQueue
let takeFrame: () -> MediaTrackFrameResult
}
private final class AudioPlayerRendererContext {
let audioStreamDescription: AudioStreamBasicDescription
let bufferSizeInSeconds: Int = 5
let lowWaterSizeInSeconds: Int = 2
let audioSession: MediaPlayerAudioSessionControl
let forAudioVideoMessage: Bool
let useVoiceProcessingMode: Bool
let controlTimebase: CMTimebase
let updatedRate: () -> Void
let audioPaused: () -> Void
var paused = true
var baseRate: Double
let audioLevelPipe: ValuePipe<Float>
var audioGraph: AUGraph?
var timePitchAudioUnit: AudioComponentInstance?
var mixerAudioUnit: AudioComponentInstance?
var equalizerAudioUnit: AudioComponentInstance?
var outputAudioUnit: AudioComponentInstance?
var bufferContextId: Int32!
let bufferContext: Atomic<AudioPlayerRendererBufferContext>
var requestingFramesContext: RequestingFramesContext?
let audioSessionDisposable = MetaDisposable()
var audioSessionControl: ManagedAudioSessionControl?
let playAndRecord: Bool
var soundMuted: Bool
var ambient: Bool
var volume: Double = 1.0
let mixWithOthers: Bool
var forceAudioToSpeaker: Bool {
didSet {
if self.forceAudioToSpeaker != oldValue {
if let audioSessionControl = self.audioSessionControl {
audioSessionControl.setOutputMode(self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system)
}
if let equalizerAudioUnit = self.equalizerAudioUnit, self.forAudioVideoMessage && !self.ambient {
AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, self.forceAudioToSpeaker ? 0.0 : 5.0, 0)
}
}
}
}
init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool, playAndRecord: Bool, useVoiceProcessingMode: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
assert(audioPlayerRendererQueue.isCurrent())
self.audioSession = audioSession
self.forAudioVideoMessage = forAudioVideoMessage
self.forceAudioToSpeaker = forceAudioToSpeaker
self.baseRate = baseRate
self.audioLevelPipe = audioLevelPipe
self.controlTimebase = controlTimebase
self.updatedRate = updatedRate
self.audioPaused = audioPaused
self.playAndRecord = playAndRecord
self.useVoiceProcessingMode = useVoiceProcessingMode
self.soundMuted = soundMuted
self.ambient = ambient
self.mixWithOthers = mixWithOthers
self.audioStreamDescription = audioRendererNativeStreamDescription()
let bufferSize = Int(self.audioStreamDescription.mSampleRate) * self.bufferSizeInSeconds * Int(self.audioStreamDescription.mBytesPerFrame)
let lowWaterSize = Int(self.audioStreamDescription.mSampleRate) * self.lowWaterSizeInSeconds * Int(self.audioStreamDescription.mBytesPerFrame)
var notifyLowWater: () -> Void = { }
self.bufferContext = Atomic(value: AudioPlayerRendererBufferContext(timebase: controlTimebase, buffer: RingByteBuffer(size: bufferSize), lowWaterSize: lowWaterSize, notifyLowWater: {
notifyLowWater()
}, updatedRate: {
updatedRate()
}, updatedLevel: { level in
audioLevelPipe.putNext(level)
}))
self.bufferContextId = registerPlayerRendererBufferContext(self.bufferContext)
notifyLowWater = { [weak self] in
audioPlayerRendererQueue.async {
if let strongSelf = self {
strongSelf.checkBuffer()
}
}
}
}
deinit {
assert(audioPlayerRendererQueue.isCurrent())
self.audioSessionDisposable.dispose()
unregisterPlayerRendererBufferContext(self.bufferContextId)
self.closeAudioUnit()
}
fileprivate func setBaseRate(_ baseRate: Double) {
if let timePitchAudioUnit = self.timePitchAudioUnit, !self.baseRate.isEqual(to: baseRate) {
self.baseRate = baseRate
AudioUnitSetParameter(timePitchAudioUnit, kTimePitchParam_Rate, kAudioUnitScope_Global, 0, Float32(baseRate), 0)
self.bufferContext.with { context in
if case .playing = context.state {
context.state = .playing(rate: baseRate, didSetRate: false)
}
}
}
}
fileprivate func setVolume(_ volume: Double) {
self.volume = volume
if let mixerAudioUnit = self.mixerAudioUnit {
AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(volume) * (self.soundMuted ? 0.0 : 1.0), 0)
}
}
fileprivate func setRate(_ rate: Double) {
assert(audioPlayerRendererQueue.isCurrent())
if !rate.isZero && self.paused {
self.start()
}
let baseRate = self.baseRate
self.bufferContext.with { context in
if !rate.isZero {
if case .playing = context.state {
} else {
context.state = .playing(rate: baseRate, didSetRate: false)
}
} else {
context.state = .paused
CMTimebaseSetRate(context.timebase, rate: 0.0)
}
}
}
fileprivate func setSoundMuted(soundMuted: Bool) {
self.soundMuted = soundMuted
if let mixerAudioUnit = self.mixerAudioUnit {
AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(self.volume) * (self.soundMuted ? 0.0 : 1.0), 0)
}
}
fileprivate func reconfigureAudio(ambient: Bool) {
self.ambient = ambient
if let audioGraph = self.audioGraph {
var isRunning: DarwinBoolean = false
AUGraphIsRunning(audioGraph, &isRunning)
if isRunning.boolValue {
AUGraphStop(audioGraph)
}
}
self.audioSessionControl?.setType(self.ambient ? .ambient : (self.playAndRecord ? .playWithPossiblePortOverride : .play(mixWithOthers: self.mixWithOthers)), completion: { [weak self] in
audioPlayerRendererQueue.async {
guard let self else {
return
}
if let audioGraph = self.audioGraph {
AUGraphStart(audioGraph)
}
}
})
}
fileprivate func flushBuffers(at timestamp: CMTime, completion: () -> Void) {
assert(audioPlayerRendererQueue.isCurrent())
self.bufferContext.with { context in
context.buffer.clear()
context.bufferMaxChannelSampleIndex = 0
context.notifiedLowWater = false
context.overflowData = Data()
context.overflowDataMaxChannelSampleIndex = 0
CMTimebaseSetTime(context.timebase, time: timestamp)
switch context.state {
case let .playing(rate, _):
context.state = .playing(rate: rate, didSetRate: false)
case .paused:
break
}
}
completion()
}
fileprivate func start() {
assert(audioPlayerRendererQueue.isCurrent())
if self.paused {
self.paused = false
self.acquireAudioSession()
}
}
fileprivate func stop() {
assert(audioPlayerRendererQueue.isCurrent())
if !self.paused {
self.paused = true
self.setRate(0.0)
self.closeAudioUnit()
}
}
private func acquireAudioSession() {
switch self.audioSession {
case let .manager(manager):
self.audioSessionDisposable.set(manager.push(audioSessionType: self.ambient ? .ambient : (self.playAndRecord ? .playWithPossiblePortOverride : .play(mixWithOthers: self.mixWithOthers)), outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: self.ambient, manualActivate: { [weak self] control in
audioPlayerRendererQueue.async {
if let strongSelf = self {
strongSelf.audioSessionControl = control
if !strongSelf.paused {
control.setup()
control.setOutputMode(strongSelf.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system)
control.activate({ _ in
audioPlayerRendererQueue.async {
if let strongSelf = self, !strongSelf.paused {
strongSelf.audioSessionAcquired()
}
}
})
}
}
}
}, deactivate: { [weak self] temporary in
return Signal { subscriber in
audioPlayerRendererQueue.async {
if let strongSelf = self {
strongSelf.audioSessionControl = nil
if !temporary {
strongSelf.audioPaused()
strongSelf.stop()
}
subscriber.putCompletion()
}
}
return EmptyDisposable
}
}, headsetConnectionStatusChanged: { [weak self] value in
audioPlayerRendererQueue.async {
if let strongSelf = self, !value {
strongSelf.audioPaused()
}
}
}))
case let .custom(request):
self.audioSessionDisposable.set(request(MediaPlayerAudioSessionCustomControl(activate: { [weak self] in
audioPlayerRendererQueue.async {
if let strongSelf = self {
if !strongSelf.paused {
strongSelf.audioSessionAcquired()
}
}
}
}, deactivate: { [weak self] in
audioPlayerRendererQueue.async {
if let strongSelf = self {
strongSelf.audioSessionControl = nil
strongSelf.audioPaused()
strongSelf.stop()
}
}
})))
}
}
private func startAudioUnit() {
assert(audioPlayerRendererQueue.isCurrent())
if self.audioGraph == nil {
let startTime = CFAbsoluteTimeGetCurrent()
var maybeAudioGraph: AUGraph?
guard NewAUGraph(&maybeAudioGraph) == noErr, let audioGraph = maybeAudioGraph else {
return
}
var converterNode: AUNode = 0
var converterDescription = AudioComponentDescription()
converterDescription.componentType = kAudioUnitType_FormatConverter
converterDescription.componentSubType = kAudioUnitSubType_AUConverter
converterDescription.componentManufacturer = kAudioUnitManufacturer_Apple
guard AUGraphAddNode(audioGraph, &converterDescription, &converterNode) == noErr else {
return
}
var timePitchNode: AUNode = 0
var timePitchDescription = AudioComponentDescription()
timePitchDescription.componentType = kAudioUnitType_FormatConverter
timePitchDescription.componentSubType = kAudioUnitSubType_AUiPodTimeOther
timePitchDescription.componentManufacturer = kAudioUnitManufacturer_Apple
guard AUGraphAddNode(audioGraph, &timePitchDescription, &timePitchNode) == noErr else {
return
}
var mixerNode: AUNode = 0
var mixerDescription = AudioComponentDescription()
mixerDescription.componentType = kAudioUnitType_Mixer
mixerDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer
mixerDescription.componentManufacturer = kAudioUnitManufacturer_Apple
guard AUGraphAddNode(audioGraph, &mixerDescription, &mixerNode) == noErr else {
return
}
var equalizerNode: AUNode = 0
var equalizerDescription = AudioComponentDescription()
equalizerDescription.componentType = kAudioUnitType_Effect
equalizerDescription.componentSubType = kAudioUnitSubType_NBandEQ
equalizerDescription.componentManufacturer = kAudioUnitManufacturer_Apple
guard AUGraphAddNode(audioGraph, &equalizerDescription, &equalizerNode) == noErr else {
return
}
var outputNode: AUNode = 0
var outputDesc = AudioComponentDescription()
outputDesc.componentType = kAudioUnitType_Output
if self.useVoiceProcessingMode {
outputDesc.componentSubType = kAudioUnitSubType_VoiceProcessingIO
} else {
outputDesc.componentSubType = kAudioUnitSubType_RemoteIO
}
outputDesc.componentFlags = 0
outputDesc.componentFlagsMask = 0
outputDesc.componentManufacturer = kAudioUnitManufacturer_Apple
guard AUGraphAddNode(audioGraph, &outputDesc, &outputNode) == noErr else {
return
}
guard AUGraphOpen(audioGraph) == noErr else {
return
}
guard AUGraphConnectNodeInput(audioGraph, converterNode, 0, timePitchNode, 0) == noErr else {
return
}
guard AUGraphConnectNodeInput(audioGraph, timePitchNode, 0, mixerNode, 0) == noErr else {
return
}
guard AUGraphConnectNodeInput(audioGraph, mixerNode, 0, equalizerNode, 0) == noErr else {
return
}
guard AUGraphConnectNodeInput(audioGraph, equalizerNode, 0, outputNode, 0) == noErr else {
return
}
var maybeConverterAudioUnit: AudioComponentInstance?
guard AUGraphNodeInfo(audioGraph, converterNode, &converterDescription, &maybeConverterAudioUnit) == noErr, let converterAudioUnit = maybeConverterAudioUnit else {
return
}
var maybeTimePitchAudioUnit: AudioComponentInstance?
guard AUGraphNodeInfo(audioGraph, timePitchNode, &timePitchDescription, &maybeTimePitchAudioUnit) == noErr, let timePitchAudioUnit = maybeTimePitchAudioUnit else {
return
}
AudioUnitSetParameter(timePitchAudioUnit, kTimePitchParam_Rate, kAudioUnitScope_Global, 0, Float32(self.baseRate), 0)
var maybeMixerAudioUnit: AudioComponentInstance?
guard AUGraphNodeInfo(audioGraph, mixerNode, &mixerDescription, &maybeMixerAudioUnit) == noErr, let mixerAudioUnit = maybeMixerAudioUnit else {
return
}
var maybeEqualizerAudioUnit: AudioComponentInstance?
guard AUGraphNodeInfo(audioGraph, equalizerNode, &equalizerDescription, &maybeEqualizerAudioUnit) == noErr, let equalizerAudioUnit = maybeEqualizerAudioUnit else {
return
}
if self.forAudioVideoMessage && !self.ambient {
AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, self.forceAudioToSpeaker ? 0.0 : 12.0, 0)
} else if self.soundMuted {
AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, 0.0, 0)
}
var maybeOutputAudioUnit: AudioComponentInstance?
guard AUGraphNodeInfo(audioGraph, outputNode, &outputDesc, &maybeOutputAudioUnit) == noErr, let outputAudioUnit = maybeOutputAudioUnit else {
return
}
var outputAudioFormat = audioRendererNativeStreamDescription()
AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputAudioFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
var streamFormat = AudioStreamBasicDescription()
AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &streamFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
AudioUnitSetProperty(timePitchAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &streamFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
AudioUnitSetProperty(mixerAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &streamFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
AudioUnitSetProperty(equalizerAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &streamFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
var callbackStruct = AURenderCallbackStruct()
callbackStruct.inputProc = rendererInputProc
callbackStruct.inputProcRefCon = UnsafeMutableRawPointer(bitPattern: intptr_t(self.bufferContextId))
guard AUGraphSetNodeInputCallback(audioGraph, converterNode, 0, &callbackStruct) == noErr else {
return
}
var one: UInt32 = 1
guard AudioUnitSetProperty(outputAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &one, 4) == noErr else {
return
}
var maximumFramesPerSlice: UInt32 = 4096
AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(timePitchAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(mixerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(equalizerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(self.volume) * (self.soundMuted ? 0.0 : 1.0), 0)
guard AUGraphInitialize(audioGraph) == noErr else {
return
}
print("\(CFAbsoluteTimeGetCurrent()) MediaPlayerAudioRenderer initialize audio unit: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
self.audioGraph = audioGraph
self.timePitchAudioUnit = timePitchAudioUnit
self.mixerAudioUnit = mixerAudioUnit
self.equalizerAudioUnit = equalizerAudioUnit
self.outputAudioUnit = outputAudioUnit
}
}
private func audioSessionAcquired() {
assert(audioPlayerRendererQueue.isCurrent())
self.startAudioUnit()
if let audioGraph = self.audioGraph {
let startTime = CFAbsoluteTimeGetCurrent()
guard AUGraphStart(audioGraph) == noErr else {
self.closeAudioUnit()
return
}
print("\(CFAbsoluteTimeGetCurrent()) MediaPlayerAudioRenderer start audio unit: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
}
}
private func closeAudioUnit() {
assert(audioPlayerRendererQueue.isCurrent())
if let audioGraph = self.audioGraph {
var status = noErr
self.bufferContext.with { context in
context.buffer.clear()
}
status = AUGraphStop(audioGraph)
if status != noErr {
Logger.shared.log("AudioPlayerRenderer", "AUGraphStop error \(status)")
}
status = AUGraphUninitialize(audioGraph)
if status != noErr {
Logger.shared.log("AudioPlayerRenderer", "AUGraphUninitialize error \(status)")
}
status = AUGraphClose(audioGraph)
if status != noErr {
Logger.shared.log("AudioPlayerRenderer", "AUGraphClose error \(status)")
}
status = DisposeAUGraph(audioGraph)
if status != noErr {
Logger.shared.log("AudioPlayerRenderer", "DisposeAUGraph error \(status)")
}
self.audioGraph = nil
self.timePitchAudioUnit = nil
self.mixerAudioUnit = nil
self.equalizerAudioUnit = nil
self.outputAudioUnit = nil
}
}
func checkBuffer() {
assert(audioPlayerRendererQueue.isCurrent())
while true {
let bytesToRequest = self.bufferContext.with { context -> Int in
let availableBytes = context.buffer.availableBytes
if availableBytes <= context.lowWaterSize {
return context.buffer.size - availableBytes
} else {
return 0
}
}
if bytesToRequest == 0 {
self.bufferContext.with { context in
context.notifiedLowWater = false
}
break
}
let overflowTakenLength = self.bufferContext.with { context -> Int in
let takeLength = min(context.overflowData.count, bytesToRequest)
if takeLength != 0 {
if takeLength == context.overflowData.count {
let data = context.overflowData
context.overflowData = Data()
self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(data.count / (2 * 2)))
} else {
let data = context.overflowData.subdata(in: 0 ..< takeLength)
self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(context.overflowData.count / (2 * 2)))
context.overflowData.replaceSubrange(0 ..< takeLength, with: Data())
}
}
return takeLength
}
if overflowTakenLength != 0 {
continue
}
if let requestingFramesContext = self.requestingFramesContext {
requestingFramesContext.queue.async { [weak self] in
let takenFrame = requestingFramesContext.takeFrame()
audioPlayerRendererQueue.async {
guard let strongSelf = self else {
return
}
switch takenFrame {
case let .frame(frame):
if let dataBuffer = CMSampleBufferGetDataBuffer(frame.sampleBuffer) {
let dataLength = CMBlockBufferGetDataLength(dataBuffer)
let takeLength = min(dataLength, bytesToRequest)
let pts = CMSampleBufferGetPresentationTimeStamp(frame.sampleBuffer)
let bufferSampleIndex = CMTimeConvertScale(pts, timescale: 44100, method: .roundAwayFromZero).value
let bytes = malloc(takeLength)!
CMBlockBufferCopyDataBytes(dataBuffer, atOffset: 0, dataLength: takeLength, destination: bytes)
strongSelf.enqueueSamples(Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: takeLength, deallocator: .free), sampleIndex: bufferSampleIndex)
if takeLength < dataLength {
strongSelf.bufferContext.with { context in
let copyOffset = context.overflowData.count
context.overflowData.count += dataLength - takeLength
context.overflowData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
CMBlockBufferCopyDataBytes(dataBuffer, atOffset: takeLength, dataLength: dataLength - takeLength, destination: bytes.advanced(by: copyOffset))
}
}
}
strongSelf.checkBuffer()
} else {
assertionFailure()
}
case .restoreState:
assertionFailure()
strongSelf.checkBuffer()
break
case .skipFrame:
strongSelf.checkBuffer()
break
case .noFrames, .finished:
strongSelf.requestingFramesContext = nil
}
}
}
} else {
self.bufferContext.with { context in
context.notifiedLowWater = false
}
}
break
}
}
private func enqueueSamples(_ data: Data, sampleIndex: Int64) {
assert(audioPlayerRendererQueue.isCurrent())
self.bufferContext.with { context in
let bytesToCopy = min(context.buffer.size - context.buffer.availableBytes, data.count)
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let _ = context.buffer.enqueue(UnsafeRawPointer(bytes), count: bytesToCopy)
context.bufferMaxChannelSampleIndex = sampleIndex + Int64(data.count / (2 * 2))
}
}
}
fileprivate func beginRequestingFrames(queue: DispatchQueue, takeFrame: @escaping () -> MediaTrackFrameResult) {
assert(audioPlayerRendererQueue.isCurrent())
if let _ = self.requestingFramesContext {
return
}
self.requestingFramesContext = RequestingFramesContext(queue: queue, takeFrame: takeFrame)
self.checkBuffer()
}
func endRequestingFrames() {
assert(audioPlayerRendererQueue.isCurrent())
self.requestingFramesContext = nil
}
}
private func audioRendererNativeStreamDescription() -> AudioStreamBasicDescription {
var canonicalBasicStreamDescription = AudioStreamBasicDescription()
canonicalBasicStreamDescription.mSampleRate = 44100.00
canonicalBasicStreamDescription.mFormatID = kAudioFormatLinearPCM
canonicalBasicStreamDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked
canonicalBasicStreamDescription.mFramesPerPacket = 1
canonicalBasicStreamDescription.mChannelsPerFrame = 2
canonicalBasicStreamDescription.mBytesPerFrame = 2 * 2
canonicalBasicStreamDescription.mBitsPerChannel = 8 * 2
canonicalBasicStreamDescription.mBytesPerPacket = 2 * 2
return canonicalBasicStreamDescription
}
public final class MediaPlayerAudioSessionCustomControl {
public let activate: () -> Void
public let deactivate: () -> Void
public init(activate: @escaping () -> Void, deactivate: @escaping () -> Void) {
self.activate = activate
self.deactivate = deactivate
}
}
public enum MediaPlayerAudioSessionControl {
case manager(ManagedAudioSession)
case custom((MediaPlayerAudioSessionCustomControl) -> Disposable)
}
public final class MediaPlayerAudioRenderer {
private var contextRef: Unmanaged<AudioPlayerRendererContext>?
private let audioClock: CMClock
public let audioTimebase: CMTimebase
public init(audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool = false, playAndRecord: Bool, useVoiceProcessingMode: Bool = false, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
var audioClock: CMClock?
CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
if audioClock == nil {
audioClock = CMClockGetHostTimeClock()
}
self.audioClock = audioClock!
var audioTimebase: CMTimebase?
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: audioClock!, timebaseOut: &audioTimebase)
self.audioTimebase = audioTimebase!
audioPlayerRendererQueue.async {
let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, forAudioVideoMessage: forAudioVideoMessage, playAndRecord: playAndRecord, useVoiceProcessingMode: useVoiceProcessingMode, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused)
self.contextRef = Unmanaged.passRetained(context)
}
}
deinit {
let contextRef = self.contextRef
audioPlayerRendererQueue.async {
contextRef?.release()
}
}
public func start() {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.start()
}
}
}
public func stop() {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.stop()
}
}
}
public func setSoundMuted(soundMuted: Bool) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.setSoundMuted(soundMuted: soundMuted)
}
}
}
public func reconfigureAudio(ambient: Bool) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.reconfigureAudio(ambient: ambient)
}
}
}
public func setRate(_ rate: Double) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.setRate(rate)
}
}
}
public func setBaseRate(_ baseRate: Double) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.setBaseRate(baseRate)
}
}
}
public func setVolume(_ volume: Double) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.setVolume(volume)
}
}
}
public func beginRequestingFrames(queue: DispatchQueue, takeFrame: @escaping () -> MediaTrackFrameResult) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.beginRequestingFrames(queue: queue, takeFrame: takeFrame)
}
}
}
public func flushBuffers(at timestamp: CMTime, completion: @escaping () -> Void) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.flushBuffers(at: timestamp, completion: completion)
}
}
}
public func setForceAudioToSpeaker(_ value: Bool) {
audioPlayerRendererQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
context.forceAudioToSpeaker = value
}
}
}
}
@@ -0,0 +1,899 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import FFMpegBinding
import VideoToolbox
public enum FramePreviewResult {
case image(UIImage)
case waitingForData
}
public protocol FramePreview {
var generatedFrames: Signal<FramePreviewResult, NoError> { get }
func generateFrame(at timestamp: Double)
func cancelPendingFrames()
}
private final class FramePreviewContext {
let source: UniversalSoftwareVideoSource
init(source: UniversalSoftwareVideoSource) {
self.source = source
}
}
private func initializedPreviewContext(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) -> Signal<QueueLocalObject<FramePreviewContext>, NoError> {
return Signal { subscriber in
let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, source: .file(userLocation: userLocation, userContentType: userContentType, fileReference: fileReference))
let readyDisposable = (source.ready
|> filter { $0 }).start(next: { _ in
subscriber.putNext(QueueLocalObject(queue: queue, generate: {
return FramePreviewContext(source: source)
}))
})
return ActionDisposable {
readyDisposable.dispose()
}
}
}
private final class MediaPlayerFramePreviewImpl {
private let queue: Queue
private let context: Promise<QueueLocalObject<FramePreviewContext>>
private let currentFrameDisposable = MetaDisposable()
private var currentFrameTimestamp: Double?
private var nextFrameTimestamp: Double?
fileprivate let framePipe = ValuePipe<FramePreviewResult>()
init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) {
self.queue = queue
self.context = Promise()
self.context.set(initializedPreviewContext(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference))
}
deinit {
assert(self.queue.isCurrent())
self.currentFrameDisposable.dispose()
}
func generateFrame(at timestamp: Double) {
if self.currentFrameTimestamp != nil {
self.nextFrameTimestamp = timestamp
return
}
self.currentFrameTimestamp = timestamp
let queue = self.queue
let takeDisposable = MetaDisposable()
let disposable = (self.context.get()
|> take(1)).start(next: { [weak self] context in
queue.justDispatch {
guard context.queue === queue else {
return
}
context.with { context in
let disposable = context.source.takeFrame(at: timestamp).start(next: { result in
queue.async {
guard let strongSelf = self else {
return
}
switch result {
case .waitingForData:
strongSelf.framePipe.putNext(.waitingForData)
case let .image(image):
if let image = image {
strongSelf.framePipe.putNext(.image(image))
}
strongSelf.currentFrameTimestamp = nil
if let nextFrameTimestamp = strongSelf.nextFrameTimestamp {
strongSelf.nextFrameTimestamp = nil
strongSelf.generateFrame(at: nextFrameTimestamp)
}
}
}
})
takeDisposable.set(disposable)
}
}
})
self.currentFrameDisposable.set(ActionDisposable {
queue.async {
takeDisposable.dispose()
disposable.dispose()
}
})
}
func cancelPendingFrames() {
self.nextFrameTimestamp = nil
self.currentFrameTimestamp = nil
self.currentFrameDisposable.set(nil)
}
}
public final class MediaPlayerFramePreview: FramePreview {
private let queue: Queue
private let impl: QueueLocalObject<MediaPlayerFramePreviewImpl>
public var generatedFrames: Signal<FramePreviewResult, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.framePipe.signal().start(next: { result in
subscriber.putNext(result)
}))
}
return disposable
}
}
public init(postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) {
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return MediaPlayerFramePreviewImpl(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference)
})
}
public func generateFrame(at timestamp: Double) {
self.impl.with { impl in
impl.generateFrame(at: timestamp)
}
}
public func cancelPendingFrames() {
self.impl.with { impl in
impl.cancelPendingFrames()
}
}
}
public final class MediaPlayerFramePreviewHLS: FramePreview {
private final class Impl {
private struct Part {
var timestamp: Int
var duration: Int
var range: Range<Int>
init(timestamp: Int, duration: Int, range: Range<Int>) {
self.timestamp = timestamp
self.duration = duration
self.range = range
}
}
private final class Playlist {
let dataFile: FileMediaReference
let initializationPart: Part
let parts: [Part]
init(dataFile: FileMediaReference, initializationPart: Part, parts: [Part]) {
self.dataFile = dataFile
self.initializationPart = initializationPart
self.parts = parts
}
}
let queue: Queue
let postbox: Postbox
let userLocation: MediaResourceUserLocation
let userContentType: MediaResourceUserContentType
let playlistFile: FileMediaReference
let mainDataFile: FileMediaReference
let alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)]
private var playlist: Playlist?
private var alternativePlaylists: [Playlist] = []
private var fetchPlaylistDisposable: Disposable?
private var playlistDisposable: Disposable?
private var pendingFrame: (Int, FFMpegLookahead)?
private let nextRequestedFrame: Atomic<Double?>
let framePipe = ValuePipe<FramePreviewResult>()
init(
queue: Queue,
postbox: Postbox,
userLocation: MediaResourceUserLocation,
userContentType: MediaResourceUserContentType,
playlistFile: FileMediaReference,
mainDataFile: FileMediaReference,
alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)],
nextRequestedFrame: Atomic<Double?>
) {
self.queue = queue
self.postbox = postbox
self.userLocation = userLocation
self.userContentType = userContentType
self.playlistFile = playlistFile
self.mainDataFile = mainDataFile
self.alternativeQualities = alternativeQualities
self.nextRequestedFrame = nextRequestedFrame
self.loadPlaylist()
}
deinit {
self.fetchPlaylistDisposable?.dispose()
self.playlistDisposable?.dispose()
}
func generateFrame() {
if self.pendingFrame != nil {
return
}
self.updateFrameRequest()
}
func cancelPendingFrames() {
self.pendingFrame = nil
}
private func loadPlaylist() {
if self.fetchPlaylistDisposable != nil {
return
}
let loadPlaylist: (FileMediaReference, FileMediaReference) -> Signal<Playlist?, NoError> = { playlistFile, dataFile in
return self.postbox.mediaBox.resourceData(playlistFile.media.resource)
|> mapToSignal { data -> Signal<Playlist?, NoError> in
if !data.complete {
return .never()
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
return .single(nil)
}
guard let playlistString = String(data: data, encoding: .utf8) else {
return .single(nil)
}
var durations: [Int] = []
var byteRanges: [Range<Int>] = []
let extinfRegex = try! NSRegularExpression(pattern: "EXTINF:(\\d+)", options: [])
let byteRangeRegex = try! NSRegularExpression(pattern: "EXT-X-BYTERANGE:(\\d+)@(\\d+)", options: [])
let extinfResults = extinfRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString))
for result in extinfResults {
if let durationRange = Range(result.range(at: 1), in: playlistString) {
if let duration = Int(String(playlistString[durationRange])) {
durations.append(duration)
}
}
}
let byteRangeResults = byteRangeRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString))
for result in byteRangeResults {
if let lengthRange = Range(result.range(at: 1), in: playlistString), let upperBoundRange = Range(result.range(at: 2), in: playlistString) {
if let length = Int(String(playlistString[lengthRange])), let lowerBound = Int(String(playlistString[upperBoundRange])) {
byteRanges.append(lowerBound ..< (lowerBound + length))
}
}
}
if durations.count != byteRanges.count {
return .single(nil)
}
var durationOffset = 0
var initializationPart: Part?
var parts: [Part] = []
for i in 0 ..< durations.count {
let part = Part(timestamp: durationOffset, duration: durations[i], range: byteRanges[i])
if i == 0 {
initializationPart = Part(timestamp: 0, duration: 0, range: 0 ..< byteRanges[i].lowerBound)
}
parts.append(part)
durationOffset += durations[i]
}
if let initializationPart {
return .single(Playlist(dataFile: dataFile, initializationPart: initializationPart, parts: parts))
} else {
return .single(nil)
}
}
}
let fetchPlaylist: (FileMediaReference) -> Signal<Never, NoError> = { playlistFile in
return fetchedMediaResource(
mediaBox: self.postbox.mediaBox,
userLocation: self.userLocation,
userContentType: self.userContentType,
reference: playlistFile.resourceReference(playlistFile.media.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
var fetchSignals: [Signal<Never, NoError>] = []
fetchSignals.append(fetchPlaylist(self.playlistFile))
for quality in self.alternativeQualities {
fetchSignals.append(fetchPlaylist(quality.playlist))
}
self.fetchPlaylistDisposable = combineLatest(fetchSignals).startStrict()
self.playlistDisposable = (combineLatest(queue: self.queue,
loadPlaylist(self.playlistFile, self.mainDataFile),
combineLatest(self.alternativeQualities.map {
return loadPlaylist($0.playlist, $0.dataFile)
})
)
|> deliverOn(self.queue)).startStrict(next: { [weak self] mainPlaylist, alternativePlaylists in
guard let self else {
return
}
self.playlist = mainPlaylist
self.alternativePlaylists = alternativePlaylists.compactMap{ $0 }
})
}
private func updateFrameRequest() {
guard let playlist = self.playlist else {
return
}
if self.pendingFrame != nil {
return
}
guard let nextRequestedFrame = self.nextRequestedFrame.swap(nil) else {
return
}
var allPlaylists: [Playlist] = [playlist]
allPlaylists.append(contentsOf: self.alternativePlaylists)
outer: for playlist in allPlaylists {
if let dataFileSize = playlist.dataFile.media.size, let part = playlist.parts.first(where: { $0.timestamp <= Int(nextRequestedFrame) && ($0.timestamp + $0.duration) > Int(nextRequestedFrame) }) {
let mappedRanges: [Range<Int64>] = [
Int64(playlist.initializationPart.range.lowerBound) ..< Int64(playlist.initializationPart.range.upperBound),
Int64(part.range.lowerBound) ..< Int64(part.range.upperBound)
]
for mappedRange in mappedRanges {
if !self.postbox.mediaBox.internal_resourceDataIsCached(id: playlist.dataFile.media.resource.id, size: dataFileSize, in: mappedRange) {
continue outer
}
}
if let directReader = FFMpegFileReader(
source: .resource(mediaBox: self.postbox.mediaBox, resource: playlist.dataFile.media.resource, resourceSize: dataFileSize, mappedRanges: mappedRanges),
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: .direct(position: nextRequestedFrame),
maxReadablePts: nil
) {
var lastFrame: CMSampleBuffer?
findFrame: while true {
switch directReader.readFrame() {
case let .frame(frame):
if lastFrame == nil {
lastFrame = frame.sampleBuffer
} else if CMSampleBufferGetPresentationTimeStamp(frame.sampleBuffer).seconds > nextRequestedFrame {
break findFrame
} else {
lastFrame = frame.sampleBuffer
}
default:
break findFrame
}
}
if let lastFrame {
if let imageBuffer = CMSampleBufferGetImageBuffer(lastFrame) {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &cgImage)
if let cgImage {
self.framePipe.putNext(.image(UIImage(cgImage: cgImage)))
}
}
}
}
self.updateFrameRequest()
return
}
}
let initializationPart = playlist.initializationPart
guard let part = playlist.parts.first(where: { $0.timestamp <= Int(nextRequestedFrame) && ($0.timestamp + $0.duration) > Int(nextRequestedFrame) }) else {
return
}
guard let dataFileSize = self.mainDataFile.media.size else {
return
}
let resource = self.mainDataFile.media.resource
let postbox = self.postbox
let userLocation = self.userLocation
let userContentType = self.userContentType
let dataFile = self.mainDataFile
let partRange: Range<Int64> = Int64(part.range.lowerBound) ..< Int64(part.range.upperBound)
let mappedRanges: [Range<Int64>] = [
Int64(initializationPart.range.lowerBound) ..< Int64(initializationPart.range.upperBound),
partRange
]
var mappedSize: Int64 = 0
for range in mappedRanges {
mappedSize += range.upperBound - range.lowerBound
}
let queue = self.queue
let updateState: (FFMpegLookahead.State) -> Void = { [weak self] state in
queue.async {
guard let self else {
return
}
if self.pendingFrame?.0 != part.timestamp {
return
}
guard let video = state.video else {
return
}
if let directReader = FFMpegFileReader(
source: .resource(mediaBox: postbox.mediaBox, resource: resource, resourceSize: dataFileSize, mappedRanges: mappedRanges),
useHardwareAcceleration: false,
selectedStream: .index(video.info.index),
seek: .stream(streamIndex: state.seek.streamIndex, pts: state.seek.pts),
maxReadablePts: (video.info.index, video.readableToTime.value, state.isEnded)
) {
switch directReader.readFrame() {
case let .frame(frame):
if let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer) {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &cgImage)
if let cgImage {
self.framePipe.putNext(.image(UIImage(cgImage: cgImage)))
}
}
default:
break
}
}
self.pendingFrame = nil
self.updateFrameRequest()
}
}
let lookahead = FFMpegLookahead(
seekToTimestamp: 0.0,
lookaheadDuration: 0.0,
updateState: updateState,
fetchInRange: { fetchRange in
let disposable = DisposableSet()
let readCount = fetchRange.upperBound - fetchRange.lowerBound
var readingPosition = fetchRange.lowerBound
var bufferOffset = 0
let doRead: (Range<Int64>) -> Void = { range in
disposable.add(fetchedMediaResource(
mediaBox: postbox.mediaBox,
userLocation: userLocation,
userContentType: userContentType,
reference: dataFile.resourceReference(dataFile.media.resource),
range: (range, .elevated),
statsCategory: .video,
preferBackgroundReferenceRevalidation: false
).startStrict())
let count = Int(range.upperBound - range.lowerBound)
bufferOffset += count
readingPosition += Int64(count)
}
var mappedRangePosition: Int64 = 0
for mappedRange in mappedRanges {
let bytesToRead = readCount - Int64(bufferOffset)
if bytesToRead <= 0 {
break
}
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
if mappedRangeBytesToRead > 0 {
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
doRead(mappedReadRange)
}
}
mappedRangePosition += mappedRangeSize
}
return disposable
},
getDataInRange: { getRange, completion in
var signals: [Signal<(Data, Bool), NoError>] = []
let readCount = getRange.upperBound - getRange.lowerBound
var readingPosition = getRange.lowerBound
var bufferOffset = 0
let doRead: (Range<Int64>) -> Void = { range in
signals.append(postbox.mediaBox.resourceData(resource, size: dataFileSize, in: range, mode: .complete))
let readSize = Int(range.upperBound - range.lowerBound)
let effectiveReadSize = max(0, min(Int(readCount) - bufferOffset, readSize))
let count = effectiveReadSize
bufferOffset += count
readingPosition += Int64(count)
}
var mappedRangePosition: Int64 = 0
for mappedRange in mappedRanges {
let bytesToRead = readCount - Int64(bufferOffset)
if bytesToRead <= 0 {
break
}
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
if mappedRangeBytesToRead > 0 {
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
doRead(mappedReadRange)
}
}
mappedRangePosition += mappedRangeSize
}
let singal = combineLatest(signals)
|> map { results -> Data? in
var result = Data()
for (partData, partIsComplete) in results {
if !partIsComplete {
return nil
}
result.append(partData)
}
return result
}
return singal.start(next: { result in
completion(result)
})
},
isDataCachedInRange: { cachedRange in
let readCount = cachedRange.upperBound - cachedRange.lowerBound
var readingPosition = cachedRange.lowerBound
var allDataIsCached = true
var bufferOffset = 0
let doRead: (Range<Int64>) -> Void = { range in
let isCached = postbox.mediaBox.internal_resourceDataIsCached(
id: resource.id,
size: dataFileSize,
in: range
)
if !isCached {
allDataIsCached = false
}
let effectiveReadSize = Int(range.upperBound - range.lowerBound)
let count = effectiveReadSize
bufferOffset += count
readingPosition += Int64(count)
}
var mappedRangePosition: Int64 = 0
for mappedRange in mappedRanges {
let bytesToRead = readCount - Int64(bufferOffset)
if bytesToRead <= 0 {
break
}
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
if mappedRangeBytesToRead > 0 {
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
doRead(mappedReadRange)
}
}
mappedRangePosition += mappedRangeSize
}
return allDataIsCached
},
size: mappedSize
)
self.pendingFrame = (part.timestamp, lookahead)
lookahead.updateCurrentTimestamp(timestamp: 0.0)
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var generatedFrames: Signal<FramePreviewResult, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.framePipe.signal().start(next: { result in
subscriber.putNext(result)
}))
}
return disposable
}
}
private let nextRequestedFrame = Atomic<Double?>(value: nil)
public init(
postbox: Postbox,
userLocation: MediaResourceUserLocation,
userContentType: MediaResourceUserContentType,
playlistFile: FileMediaReference,
mainDataFile: FileMediaReference,
alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)]
) {
let queue = Queue()
self.queue = queue
let nextRequestedFrame = self.nextRequestedFrame
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(
queue: queue,
postbox: postbox,
userLocation: userLocation,
userContentType: userContentType,
playlistFile: playlistFile,
mainDataFile: mainDataFile,
alternativeQualities: alternativeQualities,
nextRequestedFrame: nextRequestedFrame
)
})
}
public func generateFrame(at timestamp: Double) {
let _ = self.nextRequestedFrame.swap(timestamp)
self.impl.with { impl in
impl.generateFrame()
}
}
public func cancelPendingFrames() {
self.impl.with { impl in
impl.cancelPendingFrames()
}
}
}
public final class MediaPlayerFramePreviewHLSThumbnails: FramePreview {
private final class Impl {
let queue: Queue
let postbox: Postbox
let userLocation: MediaResourceUserLocation
let userContentType: MediaResourceUserContentType
let file: FileMediaReference
let fileMap: FileMediaReference
private var fileDisposable: Disposable?
let framePipe = ValuePipe<FramePreviewResult>()
private let nextRequestedFrame: Atomic<Double?>
private var mapData: (image: UIImage, frames: [(Double, CGRect)])?
private var currentFrame: Double?
init(
queue: Queue,
postbox: Postbox,
userLocation: MediaResourceUserLocation,
userContentType: MediaResourceUserContentType,
file: FileMediaReference,
fileMap: FileMediaReference,
nextRequestedFrame: Atomic<Double?>
) {
self.queue = queue
self.postbox = postbox
self.userLocation = userLocation
self.userContentType = userContentType
self.file = file
self.fileMap = fileMap
self.nextRequestedFrame = nextRequestedFrame
self.loadFiles()
}
deinit {
self.fileDisposable?.dispose()
}
func generateFrame() {
self.updateFrameRequest()
}
func cancelPendingFrames() {
}
private func loadFiles() {
if self.fileDisposable != nil {
return
}
let fetchDisposables = DisposableSet()
self.fileDisposable = fetchDisposables
fetchDisposables.add(fetchedMediaResource(
mediaBox: self.postbox.mediaBox,
userLocation: self.userLocation,
userContentType: self.userContentType,
reference: self.fileMap.resourceReference(self.fileMap.media.resource)
).startStrict())
fetchDisposables.add(fetchedMediaResource(
mediaBox: self.postbox.mediaBox,
userLocation: self.userLocation,
userContentType: self.userContentType,
reference: self.file.resourceReference(self.file.media.resource)
).startStrict())
fetchDisposables.add((combineLatest(queue: .mainQueue(),
self.postbox.mediaBox.resourceData(self.fileMap.media.resource) |> filter { $0.complete } |> take(1),
self.postbox.mediaBox.resourceData(self.file.media.resource) |> filter { $0.complete } |> take(1)
)
|> deliverOn(self.queue)).startStrict(next: { [weak self] fileMap, file in
guard let self else {
return
}
guard let fileMapData = try? Data(contentsOf: URL(fileURLWithPath: fileMap.path)) else {
return
}
guard let fileMapString = String(data: fileMapData, encoding: .utf8) else {
return
}
let mapLines = fileMapString.components(separatedBy: "\n")
/*
file=mtproto:5330572490471112705
frame_width=80
frame_height=144
0,0,0
5,80,0
10,160,0
15,240,0
20,320,0
*/
var frameWidth: Int?
var frameHeight: Int?
var frames: [(Double, CGRect)] = []
for line in mapLines {
if line.hasPrefix("file=") {
} else if line.hasPrefix("frame_width=") {
frameWidth = Int(line[line.index(line.startIndex, offsetBy: "frame_width=".count)...])
} else if line.hasPrefix("frame_height=") {
frameHeight = Int(line[line.index(line.startIndex, offsetBy: "frame_height=".count)...])
} else {
let components = line.components(separatedBy: ",")
if components.count == 3 {
let offset = Double(components[0])
let x = Int(components[1])
let y = Int(components[2])
if let offset, let x, let y {
if let frameWidth, let frameHeight {
let frameWidth = min(frameWidth, 1024)
let frameHeight = min(frameHeight, 1024)
frames.append((offset, CGRect(origin: CGPoint(x: CGFloat(x), y: CGFloat(y)), size: CGSize(width: CGFloat(frameWidth), height: CGFloat(frameHeight)))))
}
}
}
}
}
if let image = UIImage(contentsOfFile: file.path) {
self.mapData = (image, frames)
}
self.updateFrameRequest()
}))
}
private func updateFrameRequest() {
guard let mapData = self.mapData else {
return
}
if let timestamp = self.nextRequestedFrame.swap(nil) {
if self.currentFrame == timestamp {
return
}
self.currentFrame = timestamp
for (offset, rect) in mapData.frames {
if offset >= timestamp {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: rect.size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
context.cgContext.setFillColor(UIColor.black.cgColor)
context.cgContext.fill(CGRect(origin: CGPoint(), size: rect.size))
mapData.image.draw(in: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: mapData.image.size))
UIGraphicsPopContext()
}
self.framePipe.putNext(FramePreviewResult.image(image))
break
}
}
}
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var generatedFrames: Signal<FramePreviewResult, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.framePipe.signal().start(next: { result in
subscriber.putNext(result)
}))
}
return disposable
}
}
private let nextRequestedFrame = Atomic<Double?>(value: nil)
public init(
postbox: Postbox,
userLocation: MediaResourceUserLocation,
userContentType: MediaResourceUserContentType,
file: FileMediaReference,
fileMap: FileMediaReference
) {
let queue = Queue()
self.queue = queue
let nextRequestedFrame = self.nextRequestedFrame
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(
queue: queue,
postbox: postbox,
userLocation: userLocation,
userContentType: userContentType,
file: file,
fileMap: fileMap,
nextRequestedFrame: nextRequestedFrame
)
})
}
public func generateFrame(at timestamp: Double) {
let _ = self.nextRequestedFrame.swap(timestamp)
self.impl.with { impl in
impl.generateFrame()
}
}
public func cancelPendingFrames() {
self.impl.with { impl in
impl.cancelPendingFrames()
}
}
}
@@ -0,0 +1,447 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import AVFoundation
private final class MediaPlayerNodeLayerNullAction: NSObject, CAAction {
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
private final class MediaPlayerNodeLayer: AVSampleBufferDisplayLayer {
override init() {
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
assert(Queue.mainQueue().isCurrent())
}
override func action(forKey event: String) -> CAAction? {
return MediaPlayerNodeLayerNullAction()
}
}
private final class MediaPlayerNodeDisplayNode: ASDisplayNode {
var updateInHierarchy: ((Bool) -> Void)?
override init() {
super.init()
self.isLayerBacked = true
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
self.updateInHierarchy?(true)
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.updateInHierarchy?(false)
}
}
private enum PollStatus: CustomStringConvertible {
case delay(Double)
case finished
var description: String {
switch self {
case let .delay(value):
return "delay(\(value))"
case .finished:
return "finished"
}
}
}
public final class MediaPlayerNode: ASDisplayNode {
public var videoInHierarchy: Bool = false
var canPlaybackWithoutHierarchy: Bool = false
public var updateVideoInHierarchy: ((Bool) -> Void)?
private var videoNode: MediaPlayerNodeDisplayNode
public private(set) var videoLayer: AVSampleBufferDisplayLayer?
private var videoLayerReadyForDisplayObserver: NSObjectProtocol?
private var didNotifyVideoLayerReadyForDisplay: Bool = false
private let videoQueue: Queue
public var snapshotNode: ASDisplayNode? {
didSet {
if let snapshotNode = oldValue {
snapshotNode.removeFromSupernode()
}
if let snapshotNode = self.snapshotNode {
snapshotNode.frame = self.bounds
self.insertSubnode(snapshotNode, at: 0)
}
}
}
public var hasSentFramesToDisplay: (() -> Void)?
var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)?
var timer: SwiftSignalKit.Timer?
var polling = false
var currentRotationAngle = 0.0
var currentAspect = 1.0
public var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? {
didSet {
self.updateState()
}
}
private func updateState() {
if let (timebase, requestFrames, rotationAngle, aspect) = self.state {
if let videoLayer = self.videoLayer {
videoQueue.async {
if videoLayer.controlTimebase !== timebase || videoLayer.status == .failed {
videoLayer.flush()
videoLayer.controlTimebase = timebase
}
}
if !self.currentRotationAngle.isEqual(to: rotationAngle) || !self.currentAspect.isEqual(to: aspect) {
self.currentRotationAngle = rotationAngle
self.currentAspect = aspect
var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
if abs(rotationAngle).remainder(dividingBy: Double.pi) > 0.1 {
transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect))
}
if videoLayer.affineTransform() != transform {
videoLayer.setAffineTransform(transform)
}
}
if self.videoInHierarchy || self.canPlaybackWithoutHierarchy {
if requestFrames {
self.startPolling()
}
}
}
}
}
private func startPolling() {
if !self.polling {
self.polling = true
MediaPlayerNode.poll(node: self, completion: { [weak self] status in
self?.polling = false
if let strongSelf = self, let (_, requestFrames, _, _) = strongSelf.state, requestFrames {
strongSelf.timer?.invalidate()
switch status {
case let .delay(delay):
strongSelf.timer = SwiftSignalKit.Timer( timeout: delay, repeat: true, completion: {
if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _, _) = strongSelf.state, requestFrames, (strongSelf.videoInHierarchy || strongSelf.canPlaybackWithoutHierarchy) {
if videoLayer.isReadyForMoreMediaData {
strongSelf.timer?.invalidate()
strongSelf.timer = nil
strongSelf.startPolling()
}
}
}, queue: Queue.mainQueue())
strongSelf.timer?.start()
case .finished:
break
}
}
})
}
}
private struct PollState {
var numFrames: Int
var maxTakenTime: Double
}
private static func pollInner(node: MediaPlayerNode, layerTime: Double, state: PollState, completion: @escaping (PollStatus) -> Void) {
assert(Queue.mainQueue().isCurrent())
guard let (takeFrameQueue, takeFrame) = node.takeFrameAndQueue else {
return
}
guard let videoLayer = node.videoLayer else {
return
}
if !videoLayer.isReadyForMoreMediaData {
completion(.delay(max(1.0 / 30.0, state.maxTakenTime - layerTime)))
return
}
var state = state
takeFrameQueue.async { [weak node] in
let takeFrameResult = takeFrame()
switch takeFrameResult {
case let .restoreState(frames, atTime, soft):
if !soft {
Queue.mainQueue().async {
guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else {
return
}
videoLayer.flush()
}
}
for i in 0 ..< frames.count {
let frame = frames[i]
let frameTime = CMTimeGetSeconds(frame.position)
state.maxTakenTime = frameTime
let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
if i == 0 && !soft {
CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate)
CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate)
}
if !soft {
if CMTimeCompare(frame.position, atTime) < 0 {
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String)
} else if CMTimeCompare(frame.position, atTime) == 0 {
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String)
}
}
Queue.mainQueue().async {
guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else {
return
}
videoLayer.enqueue(frame.sampleBuffer)
if #available(iOS 17.4, *) {
} else {
if !strongSelf.didNotifyVideoLayerReadyForDisplay {
strongSelf.didNotifyVideoLayerReadyForDisplay = true
strongSelf.hasSentFramesToDisplay?()
}
}
}
}
Queue.mainQueue().async {
guard let node else {
return
}
MediaPlayerNode.pollInner(node: node, layerTime: layerTime, state: state, completion: completion)
}
case let .frame(frame):
state.numFrames += 1
let frameTime = CMTimeGetSeconds(frame.position)
if frame.resetDecoder {
Queue.mainQueue().async {
guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else {
return
}
videoLayer.flush()
}
}
if frame.decoded && frameTime < layerTime {
Queue.mainQueue().async {
guard let node else {
return
}
MediaPlayerNode.pollInner(node: node, layerTime: layerTime, state: state, completion: completion)
}
} else {
state.maxTakenTime = frameTime
Queue.mainQueue().async {
guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else {
return
}
videoLayer.enqueue(frame.sampleBuffer)
if !strongSelf.didNotifyVideoLayerReadyForDisplay {
strongSelf.didNotifyVideoLayerReadyForDisplay = true
strongSelf.hasSentFramesToDisplay?()
}
}
Queue.mainQueue().async {
guard let node else {
return
}
MediaPlayerNode.pollInner(node: node, layerTime: layerTime, state: state, completion: completion)
}
}
case .skipFrame:
Queue.mainQueue().async {
guard let node else {
return
}
MediaPlayerNode.pollInner(node: node, layerTime: layerTime, state: state, completion: completion)
}
case .noFrames:
DispatchQueue.main.async {
completion(.finished)
}
case .finished:
DispatchQueue.main.async {
completion(.finished)
}
}
}
}
private static func poll(node: MediaPlayerNode, completion: @escaping (PollStatus) -> Void) {
if let _ = node.videoLayer, let (timebase, _, _, _) = node.state {
let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase))
let loopImpl: (PollState) -> Void = { [weak node] state in
guard let node else {
return
}
MediaPlayerNode.pollInner(node: node, layerTime: layerTime, state: state, completion: completion)
}
loopImpl(PollState(numFrames: 0, maxTakenTime: layerTime + 0.1))
}
}
public var transformArguments: TransformImageArguments? {
didSet {
var cornerRadius: CGFloat = 0.0
if let transformArguments = self.transformArguments {
cornerRadius = transformArguments.corners.bottomLeft.radius
}
if !self.cornerRadius.isEqual(to: cornerRadius) {
self.cornerRadius = cornerRadius
self.clipsToBounds = !cornerRadius.isZero
} else {
if let transformArguments = self.transformArguments {
self.clipsToBounds = !cornerRadius.isZero || (transformArguments.imageSize.width > transformArguments.boundingSize.width || transformArguments.imageSize.height > transformArguments.boundingSize.height)
}
}
self.updateLayout()
}
}
public init(backgroundThread: Bool = false, captureProtected: Bool = false) {
self.videoNode = MediaPlayerNodeDisplayNode()
if false && backgroundThread {
self.videoQueue = Queue()
} else {
self.videoQueue = Queue.mainQueue()
}
super.init()
self.videoNode.updateInHierarchy = { [weak self] value in
if let strongSelf = self {
if strongSelf.videoInHierarchy != value {
strongSelf.videoInHierarchy = value
if value {
strongSelf.updateState()
}
}
strongSelf.updateVideoInHierarchy?(strongSelf.videoInHierarchy || strongSelf.canPlaybackWithoutHierarchy)
}
}
self.addSubnode(self.videoNode)
self.videoQueue.async { [weak self] in
let videoLayer = MediaPlayerNodeLayer()
videoLayer.videoGravity = .resize
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.videoLayer = videoLayer
if #available(iOS 13.0, *) {
videoLayer.preventsCapture = captureProtected
}
strongSelf.updateLayout()
strongSelf.layer.addSublayer(videoLayer)
if #available(iOS 17.4, *) {
strongSelf.videoLayerReadyForDisplayObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVSampleBufferDisplayLayerReadyForDisplayDidChange, object: videoLayer, queue: .main, using: { [weak strongSelf] _ in
guard let strongSelf else {
return
}
if !strongSelf.didNotifyVideoLayerReadyForDisplay {
strongSelf.didNotifyVideoLayerReadyForDisplay = true
strongSelf.hasSentFramesToDisplay?()
}
})
}
strongSelf.updateState()
}
}
}
}
deinit {
assert(Queue.mainQueue().isCurrent())
self.videoLayer?.removeFromSuperlayer()
if let _ = self.takeFrameAndQueue {
if let videoLayer = self.videoLayer {
videoLayer.flushAndRemoveImage()
Queue.mainQueue().after(1.0, {
videoLayer.flushAndRemoveImage()
})
}
}
}
override public var frame: CGRect {
didSet {
if !oldValue.size.equalTo(self.frame.size) {
self.updateLayout()
}
}
}
public func updateLayout() {
let bounds = self.bounds
if bounds.isEmpty {
return
}
let fittedRect: CGRect
if let arguments = self.transformArguments {
let drawingRect = bounds
var fittedSize = arguments.imageSize
if abs(fittedSize.width - bounds.size.width).isLessThanOrEqualTo(CGFloat(1.0)) {
fittedSize.width = bounds.size.width
}
if abs(fittedSize.height - bounds.size.height).isLessThanOrEqualTo(CGFloat(1.0)) {
fittedSize.height = bounds.size.height
}
fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)
} else {
fittedRect = bounds
}
if let videoLayer = self.videoLayer {
videoLayer.position = CGPoint(x: fittedRect.midX, y: fittedRect.midY)
videoLayer.bounds = CGRect(origin: CGPoint(), size: fittedRect.size)
}
self.snapshotNode?.frame = fittedRect
}
public func reset() {
self.videoLayer?.flush()
}
public func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
if self.canPlaybackWithoutHierarchy != canPlaybackWithoutHierarchy {
self.canPlaybackWithoutHierarchy = canPlaybackWithoutHierarchy
if canPlaybackWithoutHierarchy {
self.updateState()
}
}
self.updateVideoInHierarchy?(self.videoInHierarchy || self.canPlaybackWithoutHierarchy)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
import Foundation
import AsyncDisplayKit
import SwiftSignalKit
import UIKit
import Display
public enum MediaPlayerTimeTextNodeMode {
case normal
case reversed
}
private struct MediaPlayerTimeTextNodeState: Equatable {
let hours: Int32?
let minutes: Int32?
let seconds: Int32?
init() {
self.hours = nil
self.minutes = nil
self.seconds = nil
}
init(hours: Int32, minutes: Int32, seconds: Int32) {
self.hours = hours
self.minutes = minutes
self.seconds = seconds
}
static func ==(lhs: MediaPlayerTimeTextNodeState, rhs: MediaPlayerTimeTextNodeState) -> Bool {
if lhs.hours != rhs.hours || lhs.minutes != rhs.minutes || lhs.seconds != rhs.seconds {
return false
}
return true
}
}
private extension MediaPlayerTimeTextNodeState {
var string: String {
if let hours = self.hours, let minutes = self.minutes, let seconds = self.seconds {
if hours != 0 {
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
return String(format: "%d:%02d", minutes, seconds)
}
} else {
return "-:--"
}
}
}
private final class MediaPlayerTimeTextNodeParameters: NSObject {
let state: MediaPlayerTimeTextNodeState
let alignment: NSTextAlignment
let mode: MediaPlayerTimeTextNodeMode
let textColor: UIColor
let textFont: UIFont
init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode, textColor: UIColor, textFont: UIFont) {
self.state = state
self.alignment = alignment
self.mode = mode
self.textColor = textColor
self.textFont = textFont
super.init()
}
}
public final class MediaPlayerTimeTextNode: ASDisplayNode {
public var alignment: NSTextAlignment = .left
public var mode: MediaPlayerTimeTextNodeMode = .normal
public var keepPreviousValueOnEmptyState = false
public var textColor: UIColor {
didSet {
self.updateTimestamp()
}
}
public var textFont: UIFont {
didSet {
self.updateTimestamp()
}
}
public var defaultDuration: Double? {
didSet {
self.updateTimestamp()
}
}
public var trimRange: Range<Double>? {
didSet {
self.updateTimestamp()
}
}
public var showDurationIfNotStarted = false
public var isScrubbing = false
private var updateTimer: SwiftSignalKit.Timer?
private var statusValue: MediaPlayerStatus? {
didSet {
if self.statusValue != oldValue {
if let statusValue = statusValue, case .playing = statusValue.status {
self.ensureHasTimer()
} else {
self.stopTimer()
}
self.updateTimestamp()
}
}
}
private var state = MediaPlayerTimeTextNodeState() {
didSet {
if self.state != oldValue {
self.setNeedsDisplay()
}
}
}
private var statusDisposable: Disposable?
private var statusValuePromise = Promise<MediaPlayerStatus>()
public var status: Signal<MediaPlayerStatus, NoError>? {
didSet {
if let status = self.status {
self.statusValuePromise.set(status)
} else {
self.statusValuePromise.set(.never())
}
}
}
public init(textColor: UIColor, textFont: UIFont = Font.regular(13.0)) {
self.textColor = textColor
self.textFont = textFont
super.init()
self.isOpaque = false
self.statusDisposable = (self.statusValuePromise.get()
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.statusValue = status
}
})
}
deinit {
self.statusDisposable?.dispose()
self.updateTimer?.invalidate()
}
private func ensureHasTimer() {
if self.updateTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
self?.updateTimestamp()
}, queue: Queue.mainQueue())
self.updateTimer = timer
timer.start()
}
}
private func stopTimer() {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
func updateTimestamp() {
if ((self.statusValue?.duration ?? 0.0) < 0.1) && self.state.seconds != nil && self.keepPreviousValueOnEmptyState {
return
}
if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) {
let timestamp = max(0.0, statusValue.timestamp - (self.trimRange?.lowerBound ?? 0.0))
var duration = statusValue.duration
if let trimRange = self.trimRange {
duration = trimRange.upperBound - trimRange.lowerBound
}
var isPlaying = false
if case .playing = statusValue.status {
isPlaying = true
}
if self.showDurationIfNotStarted && (timestamp < .ulpOfOne || self.isScrubbing) && !isPlaying {
let timestamp = Int32(duration)
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
} else {
let timestampSeconds: Double
if !statusValue.generationTimestamp.isZero && isPlaying {
timestampSeconds = timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp)
} else {
timestampSeconds = timestamp
}
switch self.mode {
case .normal:
let timestamp = Int32(truncatingIfNeeded: Int64(floor(timestampSeconds)))
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
case .reversed:
let timestamp = abs(Int32(Int32(truncatingIfNeeded: Int64(floor(timestampSeconds - duration)))))
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
}
}
} else if let defaultDuration = self.defaultDuration {
var timestamp = Int32(defaultDuration)
if let trimRange = self.trimRange {
timestamp = Int32(trimRange.upperBound - trimRange.lowerBound)
}
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
} else {
self.state = MediaPlayerTimeTextNodeState()
}
}
private let digitsSet = CharacterSet(charactersIn: "0123456789")
private func widthForString(_ string: String) -> CGFloat {
let convertedString = string.components(separatedBy: digitsSet).joined(separator: "8")
let text = NSAttributedString(string: convertedString, font: textFont, textColor: .black)
let size = text.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
return size.width
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor, textFont: self.textFont)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? MediaPlayerTimeTextNodeParameters {
let string = NSAttributedString(string: parameters.state.string, font: parameters.textFont, textColor: parameters.textColor)
let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
if parameters.alignment == .left {
string.draw(at: CGPoint())
} else {
string.draw(at: CGPoint(x: bounds.size.width - size.width, y: 0.0))
}
}
}
}
@@ -0,0 +1,30 @@
import Foundation
import CoreMedia
import FFMpegBinding
public enum MediaTrackFrameType {
case video
case audio
}
public final class MediaTrackDecodableFrame {
public let type: MediaTrackFrameType
public let packet: FFMpegPacket
public let pts: CMTime
public let dts: CMTime
public let duration: CMTime
public init(type: MediaTrackFrameType, packet: FFMpegPacket, pts: CMTime, dts: CMTime, duration: CMTime) {
self.type = type
self.pts = pts
self.dts = dts
self.duration = duration
self.packet = packet
}
public func copyPacketData() -> Data {
return Data(bytes: self.packet.data, count: Int(self.packet.size))
}
}
@@ -0,0 +1,26 @@
import Foundation
import CoreMedia
public final class MediaTrackFrame {
public let type: MediaTrackFrameType
public let sampleBuffer: CMSampleBuffer
public let resetDecoder: Bool
public let decoded: Bool
public let rotationAngle: Double
public init(type: MediaTrackFrameType, sampleBuffer: CMSampleBuffer, resetDecoder: Bool, decoded: Bool, rotationAngle: Double = 0.0) {
self.type = type
self.sampleBuffer = sampleBuffer
self.resetDecoder = resetDecoder
self.decoded = decoded
self.rotationAngle = rotationAngle
}
public var position: CMTime {
return CMSampleBufferGetPresentationTimeStamp(self.sampleBuffer)
}
public var duration: CMTime {
return CMSampleBufferGetDuration(self.sampleBuffer)
}
}
@@ -0,0 +1,212 @@
import Foundation
import SwiftSignalKit
import CoreMedia
public enum MediaTrackFrameBufferStatus {
case buffering(progress: Double)
case full(until: Double)
case finished(at: Double)
}
public enum MediaTrackFrameResult {
case noFrames
case skipFrame
case restoreState(frames: [MediaTrackFrame], atTimestamp: CMTime, soft: Bool)
case frame(MediaTrackFrame)
case finished
}
private let traceEvents: Bool = {
#if DEBUG && false
return true
#else
return false
#endif
}()
public final class MediaTrackFrameBuffer {
private let stallDuration: Double
private let lowWaterDuration: Double
private let highWaterDuration: Double
private let frameSource: MediaFrameSource
private let decoder: MediaTrackFrameDecoder
private let type: MediaTrackFrameType
public let startTime: CMTime
public let duration: CMTime
public let rotationAngle: Double
public let aspect: Double
public var statusUpdated: () -> Void = { }
private var frameSourceSinkIndex: Int?
private(set) var frames: [MediaTrackDecodableFrame] = []
private var maxFrameTime: Double?
private var endOfStream = false
private var bufferedUntilTime: CMTime?
private var isWaitingForLowWaterDuration: Bool = false
init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, startTime: CMTime, duration: CMTime, rotationAngle: Double, aspect: Double, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) {
self.frameSource = frameSource
self.type = type
self.decoder = decoder
self.startTime = startTime
self.duration = duration
self.rotationAngle = rotationAngle
self.aspect = aspect
self.stallDuration = stallDuration
self.lowWaterDuration = lowWaterDuration
self.highWaterDuration = highWaterDuration
self.frameSourceSinkIndex = self.frameSource.addEventSink { [weak self] event in
if let strongSelf = self {
switch event {
case let .frames(frames):
var filteredFrames: [MediaTrackDecodableFrame] = []
for frame in frames {
if frame.type == type {
filteredFrames.append(frame)
}
}
if !filteredFrames.isEmpty {
strongSelf.addFrames(filteredFrames)
}
case .endOfStream:
strongSelf.endOfStreamReached()
}
}
}
}
deinit {
if let frameSourceSinkIndex = self.frameSourceSinkIndex {
self.frameSource.removeEventSink(frameSourceSinkIndex)
}
}
private func addFrames(_ frames: [MediaTrackDecodableFrame]) {
self.frames.append(contentsOf: frames)
var maxUntilTime: CMTime?
for frame in frames {
let frameEndTime = CMTimeAdd(frame.pts, frame.duration)
if self.bufferedUntilTime == nil || CMTimeCompare(self.bufferedUntilTime!, frameEndTime) < 0 {
self.bufferedUntilTime = frameEndTime
maxUntilTime = frameEndTime
}
}
if let maxUntilTime = maxUntilTime {
if let maxFrameTime = self.maxFrameTime {
if maxFrameTime < CMTimeGetSeconds(maxUntilTime) {
self.maxFrameTime = CMTimeGetSeconds(maxUntilTime)
}
} else {
self.maxFrameTime = CMTimeGetSeconds(maxUntilTime)
}
if traceEvents {
print("\(self.type) added \(frames.count) frames until \(CMTimeGetSeconds(maxUntilTime)), \(self.frames.count) total")
}
}
self.statusUpdated()
}
private func endOfStreamReached() {
self.endOfStream = true
self.isWaitingForLowWaterDuration = false
self.statusUpdated()
}
public func status(at timestamp: Double) -> MediaTrackFrameBufferStatus {
var bufferedDuration = 0.0
if let bufferedUntilTime = self.bufferedUntilTime {
if CMTimeGetSeconds(self.duration) > 0.0 {
if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 || self.endOfStream {
return .finished(at: CMTimeGetSeconds(bufferedUntilTime))
}
} else if self.endOfStream {
return .finished(at: CMTimeGetSeconds(bufferedUntilTime))
}
bufferedDuration = CMTimeGetSeconds(bufferedUntilTime) - timestamp
} else if self.endOfStream {
if let maxFrameTime = self.maxFrameTime {
return .finished(at: maxFrameTime)
} else {
return .finished(at: CMTimeGetSeconds(self.duration))
}
}
let minTimestamp = timestamp - 1.0
for i in (0 ..< self.frames.count).reversed() {
if CMTimeGetSeconds(self.frames[i].pts) < minTimestamp {
self.frames.remove(at: i)
}
}
if bufferedDuration < self.lowWaterDuration {
if traceEvents {
print("\(self.type) buffered duration: \(bufferedDuration), requesting until \(timestamp) + \(self.highWaterDuration - bufferedDuration)")
}
let delayIncrement = 0.3
var generateUntil = timestamp + delayIncrement
while generateUntil < timestamp + self.highWaterDuration {
self.frameSource.generateFrames(until: min(timestamp + self.highWaterDuration, generateUntil), types: [self.type])
generateUntil += delayIncrement
}
if bufferedDuration > self.stallDuration && !self.isWaitingForLowWaterDuration {
if traceEvents {
print("\(self.type) buffered1 duration: \(bufferedDuration), wait until \(timestamp) + \(self.highWaterDuration - bufferedDuration)")
}
return .full(until: timestamp + self.highWaterDuration)
} else {
self.isWaitingForLowWaterDuration = true
return .buffering(progress: max(0.0, bufferedDuration / self.lowWaterDuration))
}
} else {
self.isWaitingForLowWaterDuration = false
if traceEvents {
print("\(self.type) buffered2 duration: \(bufferedDuration), wait until \(timestamp) + \(bufferedDuration - self.lowWaterDuration)")
}
return .full(until: timestamp + max(0.0, bufferedDuration - self.lowWaterDuration))
}
}
public var hasFrames: Bool {
return !self.frames.isEmpty
}
public func takeFrame() -> MediaTrackFrameResult {
if let decodedFrame = self.decoder.takeQueuedFrame() {
return .frame(decodedFrame)
}
if !self.frames.isEmpty {
let frame = self.frames.removeFirst()
if self.decoder.send(frame: frame) {
if let decodedFrame = self.decoder.decode() {
return .frame(decodedFrame)
} else {
return .skipFrame
}
} else {
return .skipFrame
}
} else {
if self.endOfStream, let decodedFrame = self.decoder.takeRemainingFrame() {
return .frame(decodedFrame)
} else {
if self.endOfStream {
return .finished
} else if let bufferedUntilTime = self.bufferedUntilTime {
if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 {
return .finished
}
}
}
}
return .noFrames
}
}
@@ -0,0 +1,9 @@
protocol MediaTrackFrameDecoder {
func send(frame: MediaTrackDecodableFrame) -> Bool
func decode() -> MediaTrackFrame?
func takeQueuedFrame() -> MediaTrackFrame?
func takeRemainingFrame() -> MediaTrackFrame?
func reset()
func sendEndToDecoder() -> Bool
}
@@ -0,0 +1,73 @@
import Foundation
import Darwin
import RingBuffer
public final class RingByteBuffer {
public let size: Int
private var buffer: TPCircularBuffer
public init(size: Int) {
self.size = size
self.buffer = TPCircularBuffer()
TPCircularBufferInit(&self.buffer, Int32(size))
}
deinit {
TPCircularBufferCleanup(&self.buffer)
}
public func enqueue(data: Data) -> Bool {
return data.withUnsafeBytes { buffer -> Bool in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return false
}
return TPCircularBufferProduceBytes(&self.buffer, UnsafeRawPointer(bytes), Int32(data.count))
}
}
public func enqueue(_ bytes: UnsafeRawPointer, count: Int) -> Bool {
return TPCircularBufferProduceBytes(&self.buffer, bytes, Int32(count))
}
public func withMutableHeadBytes(_ f: (UnsafeMutableRawPointer, Int) -> Int) {
var availableBytes: Int32 = 0
let bytes = TPCircularBufferHead(&self.buffer, &availableBytes)
let enqueuedBytes = f(bytes!, Int(availableBytes))
TPCircularBufferProduce(&self.buffer, Int32(enqueuedBytes))
}
public func dequeue(_ bytes: UnsafeMutableRawPointer, count: Int) -> Int {
var availableBytes: Int32 = 0
let tail = TPCircularBufferTail(&self.buffer, &availableBytes)
let copiedCount = min(count, Int(availableBytes))
memcpy(bytes, tail, copiedCount)
TPCircularBufferConsume(&self.buffer, Int32(copiedCount))
return copiedCount
}
public func dequeue(count: Int) -> Data {
var availableBytes: Int32 = 0
let tail = TPCircularBufferTail(&self.buffer, &availableBytes)
let copiedCount = min(count, Int(availableBytes))
let bytes = malloc(copiedCount)!
memcpy(bytes, tail, copiedCount)
TPCircularBufferConsume(&self.buffer, Int32(copiedCount))
return Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: copiedCount, deallocator: .free)
}
public func clear() {
TPCircularBufferClear(&self.buffer)
}
public var availableBytes: Int {
var count: Int32 = 0
TPCircularBufferTail(&self.buffer, &count)
return Int(count)
}
}
@@ -0,0 +1,960 @@
import Foundation
#if !os(macOS)
import UIKit
#else
import AppKit
import TGUIKit
#endif
import CoreMedia
import SwiftSignalKit
import FFMpegBinding
private func SoftwareVideoSource_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<SoftwareVideoSource>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
let result = read(fd, buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
}
return FFMPEG_CONSTANT_AVERROR_EOF
}
private func SoftwareVideoSource_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<SoftwareVideoSource>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
} else {
lseek(fd, off_t(offset), SEEK_SET)
return offset
}
}
return 0
}
private final class SoftwareVideoStream {
let index: Int
let fps: CMTime
let timebase: CMTime
let startTime: CMTime
let duration: CMTime
let decoder: MediaTrackFrameDecoder
let rotationAngle: Double
let aspect: Double
init(index: Int, fps: CMTime, timebase: CMTime, startTime: CMTime, duration: CMTime, decoder: MediaTrackFrameDecoder, rotationAngle: Double, aspect: Double) {
self.index = index
self.fps = fps
self.timebase = timebase
self.startTime = startTime
self.duration = duration
self.decoder = decoder
self.rotationAngle = rotationAngle
self.aspect = aspect
}
}
public final class SoftwareVideoSource {
private var readingError = false
private var videoStream: SoftwareVideoStream?
private var avIoContext: FFMpegAVIOContext?
private var avFormatContext: FFMpegAVFormatContext?
private let path: String
fileprivate let fd: Int32?
fileprivate let size: Int32
private let hintVP9: Bool
private let unpremultiplyAlpha: Bool
private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = []
private var hasReadToEnd: Bool = false
public private(set) var reportedDuration: CMTime = .invalid
public var hasStream: Bool {
return self.videoStream != nil
}
public init(path: String, hintVP9: Bool, unpremultiplyAlpha: Bool, passthroughDecoder: Bool = false) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
self.hintVP9 = hintVP9
self.unpremultiplyAlpha = unpremultiplyAlpha
var s = stat()
stat(path, &s)
self.size = Int32(s.st_size)
let fd = open(path, O_RDONLY, S_IRUSR)
if fd >= 0 {
self.fd = fd
} else {
self.fd = nil
}
self.path = path
let avFormatContext = FFMpegAVFormatContext()
if hintVP9 {
avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9)
}
let ioBufferSize = 64 * 1024
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: SoftwareVideoSource_readPacketCallback, writePacket: nil, seek: SoftwareVideoSource_seekCallback, isSeekable: true)
self.avIoContext = avIoContext
avFormatContext.setIO(self.avIoContext!)
if !avFormatContext.openInput(withDirectFilePath: nil) {
self.readingError = true
return
}
if !avFormatContext.findStreamInfo() {
self.readingError = true
return
}
self.avFormatContext = avFormatContext
var videoStream: SoftwareVideoStream?
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
let metrics = avFormatContext.metricsForStream(at: streamIndex)
let rotationAngle: Double = metrics.rotationAngle
let aspect = Double(metrics.width) / Double(metrics.height)
if passthroughDecoder {
var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData?
if codecId == FFMpegCodecIdMPEG4 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdH264 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdHEVC {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdAV1 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
}
if let videoFormatData {
videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect)
break
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect)
break
}
}
}
}
}
self.reportedDuration = CMTime(seconds: avFormatContext.duration(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.videoStream = videoStream
if let videoStream = self.videoStream {
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
}
}
deinit {
if let fd = self.fd {
close(fd)
}
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
return packet
} else {
return nil
}
}
func readDecodableFrame() -> (MediaTrackDecodableFrame?, Bool) {
var frames: [MediaTrackDecodableFrame] = []
var endOfStream = false
while !self.readingError && frames.isEmpty {
if let packet = self.readPacketInternal() {
if let videoStream = videoStream, Int(packet.streamIndex) == videoStream.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale)
} else {
duration = videoStream.fps
}
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
frames.append(frame)
}
} else {
if endOfStream {
break
} else {
if let _ = self.avFormatContext, let _ = self.videoStream {
endOfStream = true
break
} else {
endOfStream = true
break
}
}
}
}
return (frames.first, endOfStream)
}
public func getFramerate() -> Int {
if let videoStream = self.videoStream {
return Int(videoStream.fps.seconds)
} else {
return 0
}
}
public func readTrackInfo() -> (offset: CMTime, duration: CMTime)? {
guard let videoStream = self.videoStream else {
return nil
}
return (videoStream.startTime, CMTimeMaximum(CMTime(value: 0, timescale: videoStream.duration.timescale), CMTimeSubtract(videoStream.duration, videoStream.startTime)))
}
public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) {
guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else {
return (nil, 0.0, 1.0, false)
}
if !self.enqueuedFrames.isEmpty {
let value = self.enqueuedFrames.removeFirst()
return (value.0, value.1, value.2, value.3)
}
let (decodableFrame, loop) = self.readDecodableFrame()
var result: (MediaTrackFrame?, CGFloat, CGFloat, Bool)
if let decodableFrame = decodableFrame {
var ptsOffset: CMTime?
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
ptsOffset = maxPts
}
if videoStream.decoder.send(frame: decodableFrame) {
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
result = (decoder.decode(ptsOffset: ptsOffset, forceARGB: self.hintVP9, unpremultiplyAlpha: self.unpremultiplyAlpha), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
} else {
result = (videoStream.decoder.decode(), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
} else {
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
} else {
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
if loop {
let _ = videoStream.decoder.sendEndToDecoder()
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
let remainingFrames = decoder.receiveRemainingFrames(ptsOffset: maxPts)
for i in 0 ..< remainingFrames.count {
self.enqueuedFrames.append((remainingFrames[i], CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), i == remainingFrames.count - 1))
}
} else {
if let remainingFrame = videoStream.decoder.takeRemainingFrame() {
self.enqueuedFrames.append((remainingFrame, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true))
}
}
videoStream.decoder.reset()
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
if result.0 == nil && !self.enqueuedFrames.isEmpty {
result = self.enqueuedFrames.removeFirst()
}
}
return result
}
public func readImage() -> (UIImage?, CGFloat, CGFloat, Bool) {
if let videoStream = self.videoStream {
guard let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder else {
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true)
}
for _ in 0 ..< 10 {
let (decodableFrame, loop) = self.readDecodableFrame()
if let decodableFrame = decodableFrame {
if let renderedFrame = decoder.render(frame: decodableFrame) {
return (renderedFrame, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
}
}
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true)
} else {
return (nil, 0.0, 1.0, false)
}
}
public func seek(timestamp: Double) {
if let stream = self.videoStream, let avFormatContext = self.avFormatContext {
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true)
stream.decoder.reset()
}
}
}
private final class SoftwareAudioStream {
let index: Int
let fps: CMTime
let timebase: CMTime
let duration: CMTime
let decoder: FFMpegAudioFrameDecoder
init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegAudioFrameDecoder) {
self.index = index
self.fps = fps
self.timebase = timebase
self.duration = duration
self.decoder = decoder
}
}
private func SoftwareAudioSource_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<SoftwareAudioSource>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
let result = read(fd, buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
}
return FFMPEG_CONSTANT_AVERROR_EOF
}
private func SoftwareAudioSource_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<SoftwareAudioSource>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
} else {
lseek(fd, off_t(offset), SEEK_SET)
return offset
}
}
return 0
}
public final class SoftwareAudioSource {
private var readingError = false
private var audioStream: SoftwareAudioStream?
private var avIoContext: FFMpegAVIOContext?
private var avFormatContext: FFMpegAVFormatContext?
private let path: String
fileprivate let fd: Int32?
fileprivate let size: Int32
private var hasReadToEnd: Bool = false
public var hasStream: Bool {
return self.audioStream != nil
}
public init(path: String) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
var s = stat()
stat(path, &s)
self.size = Int32(s.st_size)
let fd = open(path, O_RDONLY, S_IRUSR)
if fd >= 0 {
self.fd = fd
} else {
self.fd = nil
}
self.path = path
let avFormatContext = FFMpegAVFormatContext()
let ioBufferSize = 64 * 1024
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: SoftwareAudioSource_readPacketCallback, writePacket: nil, seek: SoftwareAudioSource_seekCallback, isSeekable: true)
self.avIoContext = avIoContext
avFormatContext.setIO(self.avIoContext!)
if !avFormatContext.openInput(withDirectFilePath: nil) {
self.readingError = true
return
}
if !avFormatContext.findStreamInfo() {
self.readingError = true
return
}
self.avFormatContext = avFormatContext
var audioStream: SoftwareAudioStream?
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false)
if let codec = codec {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
audioStream = SoftwareAudioStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext, sampleRate: 48000, channelCount: 1))
break
}
}
}
}
self.audioStream = audioStream
if let audioStream = self.audioStream {
avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false)
}
}
deinit {
if let fd = self.fd {
close(fd)
}
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
return packet
} else {
return nil
}
}
func readDecodableFrame() -> MediaTrackDecodableFrame? {
var frames: [MediaTrackDecodableFrame] = []
while !self.readingError && !self.hasReadToEnd && frames.isEmpty {
if let packet = self.readPacketInternal() {
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: audioStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: audioStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * audioStream.timebase.value, timescale: audioStream.timebase.timescale)
} else {
duration = audioStream.fps
}
let frame = MediaTrackDecodableFrame(type: .audio, packet: packet, pts: pts, dts: dts, duration: duration)
frames.append(frame)
}
} else {
break
}
}
return frames.first
}
public func readFrame() -> Data? {
guard let audioStream = self.audioStream, let _ = self.avFormatContext else {
return nil
}
if let decodableFrame = self.readDecodableFrame() {
return audioStream.decoder.decodeRaw(frame: decodableFrame)
} else {
return nil
}
}
public func readSampleBuffer() -> CMSampleBuffer? {
guard let audioStream = self.audioStream, let _ = self.avFormatContext else {
return nil
}
while true {
if let decodableFrame = self.readDecodableFrame() {
if audioStream.decoder.send(frame: decodableFrame) {
if let result = audioStream.decoder.decode() {
return result.sampleBuffer
}
}
} else {
return nil
}
}
}
public func readEncodedFrame() -> (Data, Int)? {
guard let _ = self.audioStream, let _ = self.avFormatContext else {
return nil
}
if let decodableFrame = self.readDecodableFrame() {
return (decodableFrame.copyPacketData(), Int(decodableFrame.packet.duration))
} else {
return nil
}
}
public func seek(timestamp: Double) {
if let stream = self.audioStream, let avFormatContext = self.avFormatContext {
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: false)
}
}
}
private func SoftwareVideoReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<SoftwareVideoReader>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
let result = read(fd, buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
}
return FFMPEG_CONSTANT_AVERROR_EOF
}
private func SoftwareVideoReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<SoftwareVideoReader>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
} else {
lseek(fd, off_t(offset), SEEK_SET)
return offset
}
}
return 0
}
final class SoftwareVideoReader {
private var readingError = false
private var videoStream: SoftwareVideoStream?
private var avIoContext: FFMpegAVIOContext?
private var avFormatContext: FFMpegAVFormatContext?
private let path: String
fileprivate let fd: Int32?
fileprivate let size: Int32
private var didSendEndToEncoder: Bool = false
private var hasReadToEnd: Bool = false
private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat)] = []
public private(set) var reportedDuration: CMTime = .invalid
public var hasStream: Bool {
return self.videoStream != nil
}
public init(path: String, hintVP9: Bool, passthroughDecoder: Bool = false) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
var s = stat()
stat(path, &s)
self.size = Int32(s.st_size)
let fd = open(path, O_RDONLY, S_IRUSR)
if fd >= 0 {
self.fd = fd
} else {
self.fd = nil
}
self.path = path
let avFormatContext = FFMpegAVFormatContext()
if hintVP9 {
avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9)
}
let ioBufferSize = 64 * 1024
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: SoftwareVideoReader_readPacketCallback, writePacket: nil, seek: SoftwareVideoReader_seekCallback, isSeekable: true)
self.avIoContext = avIoContext
avFormatContext.setIO(self.avIoContext!)
if !avFormatContext.openInput(withDirectFilePath: nil) {
self.readingError = true
return
}
if !avFormatContext.findStreamInfo() {
self.readingError = true
return
}
self.avFormatContext = avFormatContext
var videoStream: SoftwareVideoStream?
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
let metrics = avFormatContext.metricsForStream(at: streamIndex)
let rotationAngle: Double = metrics.rotationAngle
let aspect = Double(metrics.width) / Double(metrics.height)
if passthroughDecoder {
var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData?
if codecId == FFMpegCodecIdMPEG4 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdH264 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdHEVC {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdAV1 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
}
if let videoFormatData {
videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect)
break
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect)
break
}
}
}
}
}
self.reportedDuration = CMTime(seconds: avFormatContext.duration(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.videoStream = videoStream
if let videoStream = self.videoStream {
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
}
}
deinit {
if let fd = self.fd {
close(fd)
}
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
return packet
} else {
return nil
}
}
func readDecodableFrame() -> MediaTrackDecodableFrame? {
if self.hasReadToEnd {
return nil
}
while !self.readingError && !self.hasReadToEnd {
if let packet = self.readPacketInternal() {
if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale)
} else {
duration = videoStream.fps
}
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
return frame
}
} else {
self.hasReadToEnd = true
}
}
return nil
}
public func readFrame() -> MediaTrackFrame? {
guard let videoStream = self.videoStream else {
return nil
}
while !self.readingError && !self.hasReadToEnd {
if let decodableFrame = self.readDecodableFrame() {
var result: (MediaTrackFrame?, CGFloat, CGFloat)
if videoStream.decoder.send(frame: decodableFrame) {
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
result = (decoder.decode(ptsOffset: nil, forceARGB: false, unpremultiplyAlpha: false, displayImmediately: false), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
} else {
result = (videoStream.decoder.decode(), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
}
} else {
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
}
if let frame = result.0 {
return frame
}
} else {
break
}
}
if !self.readingError && self.hasReadToEnd && !self.didSendEndToEncoder {
self.didSendEndToEncoder = true
let _ = videoStream.decoder.sendEndToDecoder()
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
let remainingFrames = decoder.receiveRemainingFrames(ptsOffset: nil)
for i in 0 ..< remainingFrames.count {
self.enqueuedFrames.append((remainingFrames[i], CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect)))
}
} else {
if let remainingFrame = videoStream.decoder.takeRemainingFrame() {
self.enqueuedFrames.append((remainingFrame, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect)))
}
}
}
if !self.enqueuedFrames.isEmpty {
let result = self.enqueuedFrames.removeFirst()
return result.0
} else {
return nil
}
}
}
public final class FFMpegMediaInfo {
public struct Info {
public let index: Int
public let timescale: CMTimeScale
public let startTime: CMTime
public let duration: CMTime
public let fps: CMTime
public let codecName: String?
}
public let audio: Info?
public let video: Info?
public init(audio: Info?, video: Info?) {
self.audio = audio
self.video = video
}
}
private final class FFMpegMediaInfoExtractContext {
let fd: Int32
let size: Int
init(fd: Int32, size: Int) {
self.fd = fd
self.size = size
}
}
private func FFMpegMediaInfoExtractContextReadPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<FFMpegMediaInfoExtractContext>.fromOpaque(userData!).takeUnretainedValue()
let result = read(context.fd, buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
}
private func FFMpegMediaInfoExtractContextSeekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegMediaInfoExtractContext>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
} else {
lseek(context.fd, off_t(offset), SEEK_SET)
return offset
}
}
public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
var s = stat()
stat(path, &s)
let size = Int32(s.st_size)
let fd = open(path, O_RDONLY, S_IRUSR)
if fd < 0 {
return nil
}
defer {
close(fd)
}
let avFormatContext = FFMpegAVFormatContext()
let ioBufferSize = 64 * 1024
let context = FFMpegMediaInfoExtractContext(fd: fd, size: Int(size))
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(context).toOpaque(), readPacket: FFMpegMediaInfoExtractContextReadPacketCallback, writePacket: nil, seek: FFMpegMediaInfoExtractContextSeekCallback, isSeekable: true) else {
return nil
}
avFormatContext.setIO(avIoContext)
if !avFormatContext.openInput(withDirectFilePath: nil) {
return nil
}
if !avFormatContext.findStreamInfo() {
return nil
}
var streamInfos: [(isVideo: Bool, info: FFMpegMediaInfo.Info)] = []
for typeIndex in 0 ..< 2 {
let isVideo = typeIndex == 0
for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime))
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let codecName = resolveFFMpegCodecName(id: codecId)
streamInfos.append((isVideo: isVideo, info: FFMpegMediaInfo.Info(
index: Int(streamIndex),
timescale: timebase.timescale,
startTime: startTime,
duration: duration,
fps: fps,
codecName: codecName
)))
}
}
return FFMpegMediaInfo(audio: streamInfos.first(where: { !$0.isVideo })?.info, video: streamInfos.first(where: { $0.isVideo })?.info)
}
public func resolveFFMpegCodecName(id: Int32) -> String? {
if id == FFMpegCodecIdMPEG4 {
return "mpeg4"
} else if id == FFMpegCodecIdH264 {
return "h264"
} else if id == FFMpegCodecIdHEVC {
return "hevc"
} else if id == FFMpegCodecIdAV1 {
return "av1"
} else if id == FFMpegCodecIdVP9 {
return "vp9"
} else if id == FFMpegCodecIdVP8 {
return "vp8"
} else {
return nil
}
}
@@ -0,0 +1,44 @@
import Foundation
#if !os(macOS)
import UIKit
#else
import AppKit
#endif
import SwiftSignalKit
import Postbox
import TelegramCore
import FFMpegBinding
public func preloadVideoResource(postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, duration: Double) -> Signal<Never, NoError> {
return Signal { subscriber in
let queue = Queue()
let disposable = MetaDisposable()
queue.async {
let maximumFetchSize = 2 * 1024 * 1024 + 128 * 1024
let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, limitedFileRange: nil, streamable: true, isSeekable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize)
let source = QueueLocalObject(queue: queue, generate: {
return sourceImpl
})
let signal = sourceImpl.seek(timestamp: 0.0)
|> deliverOn(queue)
|> mapToSignal { result -> Signal<Never, MediaFrameSourceSeekError> in
let result = result.syncWith({ $0 })
if let videoBuffer = result.buffers.videoBuffer {
let impl = source.syncWith({ $0 })
return impl.ensureHasFrames(until: min(duration, videoBuffer.duration.seconds))
|> ignoreValues
|> castError(MediaFrameSourceSeekError.self)
} else {
return .complete()
}
}
disposable.set(signal.start(error: { _ in
subscriber.putCompletion()
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
//! Project version number for MediaPlayer.
FOUNDATION_EXPORT double MediaPlayerVersionNumber;
//! Project version string for MediaPlayer.
FOUNDATION_EXPORT const unsigned char MediaPlayerVersionString[];
#import <UniversalMediaPlayer/RingBuffer.h>
@@ -0,0 +1,475 @@
import Foundation
#if !os(macOS)
import UIKit
#else
import AppKit
import TGUIKit
#endif
import SwiftSignalKit
import Postbox
import TelegramCore
import FFMpegBinding
import CoreMedia
private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<UniversalSoftwareVideoSourceImpl>.fromOpaque(userData!).takeUnretainedValue()
let data: Signal<(Data, Bool), NoError>
let readCount = min(256 * 1024, Int64(bufferSize))
let requestRange: Range<Int64> = context.readingOffset ..< (context.readingOffset + readCount)
context.currentNumberOfReads += 1
context.currentReadBytes += readCount
let semaphore = DispatchSemaphore(value: 0)
data = context.mediaBox.resourceData(context.source.resource, size: context.size, in: requestRange, mode: .partial)
let requiredDataIsNotLocallyAvailable = context.requiredDataIsNotLocallyAvailable
var fetchedData: Data?
let fetchDisposable = MetaDisposable()
let isInitialized = context.videoStream != nil || context.automaticallyFetchHeader
let mediaBox = context.mediaBox
let source = context.source
let disposable = data.start(next: { result in
let (data, isComplete) = result
if data.count == readCount || isComplete {
fetchedData = data
semaphore.signal()
} else {
if isInitialized {
switch source {
case let .file(userLocation, userContentType, fileReference):
fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(fileReference.media.resource), ranges: [(requestRange, .maximum)]).start())
case .direct:
break
}
}
requiredDataIsNotLocallyAvailable?()
}
})
let cancelDisposable = context.cancelRead.start(next: { value in
if value {
semaphore.signal()
}
})
semaphore.wait()
disposable.dispose()
cancelDisposable.dispose()
fetchDisposable.dispose()
if let fetchedData = fetchedData {
fetchedData.withUnsafeBytes { byteBuffer -> Void in
guard let bytes = byteBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(buffer, bytes, fetchedData.count)
}
let fetchedCount = Int32(fetchedData.count)
context.readingOffset += Int64(fetchedCount)
if fetchedCount == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return fetchedCount
} else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<UniversalSoftwareVideoSourceImpl>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
} else {
context.readingOffset = offset
return offset
}
}
private final class SoftwareVideoStream {
let index: Int
let fps: CMTime
let timebase: CMTime
let duration: CMTime
let decoder: FFMpegMediaVideoFrameDecoder
let rotationAngle: Double
let aspect: Double
init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) {
self.index = index
self.fps = fps
self.timebase = timebase
self.duration = duration
self.decoder = decoder
self.rotationAngle = rotationAngle
self.aspect = aspect
}
}
private final class UniversalSoftwareVideoSourceImpl {
fileprivate let mediaBox: MediaBox
fileprivate let source: UniversalSoftwareVideoSource.Source
fileprivate let size: Int64
fileprivate let automaticallyFetchHeader: Bool
fileprivate let state: ValuePromise<UniversalSoftwareVideoSourceState>
fileprivate var avIoContext: FFMpegAVIOContext!
fileprivate var avFormatContext: FFMpegAVFormatContext!
fileprivate var videoStream: SoftwareVideoStream!
fileprivate var readingOffset: Int64 = 0
fileprivate var cancelRead: Signal<Bool, NoError>
fileprivate var requiredDataIsNotLocallyAvailable: (() -> Void)?
fileprivate var currentNumberOfReads: Int = 0
fileprivate var currentReadBytes: Int64 = 0
init?(
mediaBox: MediaBox,
source: UniversalSoftwareVideoSource.Source,
state: ValuePromise<UniversalSoftwareVideoSourceState>,
cancelInitialization: Signal<Bool, NoError>,
automaticallyFetchHeader: Bool,
hintVP9: Bool = false
) {
switch source {
case let .file(_, _, fileReference):
guard let size = fileReference.media.size else {
return nil
}
self.size = size
case let .direct(_, sizeValue):
self.size = sizeValue
}
self.mediaBox = mediaBox
self.source = source
self.automaticallyFetchHeader = automaticallyFetchHeader
self.state = state
state.set(.initializing)
self.cancelRead = cancelInitialization
let ioBufferSize = 64 * 1024
let isSeekable: Bool
switch source {
case .file:
isSeekable = true
case .direct:
isSeekable = false
}
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else {
return nil
}
self.avIoContext = avIoContext
let avFormatContext = FFMpegAVFormatContext()
if hintVP9 {
avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9)
}
avFormatContext.setIO(avIoContext)
if !avFormatContext.openInput(withDirectFilePath: nil) {
return nil
}
if !avFormatContext.findStreamInfo() {
return nil
}
print("Header read in \(self.currentReadBytes) bytes")
self.currentReadBytes = 0
self.avFormatContext = avFormatContext
var videoStream: SoftwareVideoStream?
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
let metrics = avFormatContext.metricsForStream(at: streamIndex)
let rotationAngle: Double = metrics.rotationAngle
let aspect = Double(metrics.width) / Double(metrics.height)
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect)
break
}
}
}
}
if let videoStream = videoStream {
self.videoStream = videoStream
} else {
return nil
}
state.set(.ready)
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
return packet
} else {
return nil
}
}
func readDecodableFrame() -> (MediaTrackDecodableFrame?, Bool) {
var frames: [MediaTrackDecodableFrame] = []
var endOfStream = false
while frames.isEmpty {
if let packet = self.readPacketInternal() {
if let videoStream = videoStream, Int(packet.streamIndex) == videoStream.index {
let packetPts = packet.pts
let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale)
} else {
duration = videoStream.fps
}
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
frames.append(frame)
}
} else {
endOfStream = true
break
}
}
if endOfStream {
if let videoStream = self.videoStream {
videoStream.decoder.reset()
}
}
return (frames.first, endOfStream)
}
private func seek(timestamp: Double) {
if let stream = self.videoStream, let avFormatContext = self.avFormatContext {
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true)
stream.decoder.reset()
}
}
func readImage(at timestamp: Double) -> (UIImage?, CGFloat, CGFloat, Bool) {
guard let videoStream = self.videoStream, let _ = self.avFormatContext else {
return (nil, 0.0, 1.0, false)
}
self.seek(timestamp: timestamp)
self.currentNumberOfReads = 0
self.currentReadBytes = 0
for i in 0 ..< 10 {
let _ = i
let (decodableFrame, loop) = self.readDecodableFrame()
if let decodableFrame = decodableFrame {
if let renderedFrame = videoStream.decoder.render(frame: decodableFrame) {
print("Frame rendered in \(self.currentNumberOfReads) reads, \(self.currentReadBytes) bytes, total frames read: \(i + 1)")
return (renderedFrame, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
}
}
print("No frame in \(self.currentReadBytes / 1024) KB")
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true)
}
}
private enum UniversalSoftwareVideoSourceState {
case initializing
case failed
case ready
case generatingFrame
}
private final class UniversalSoftwareVideoSourceThreadParams: NSObject {
let mediaBox: MediaBox
let source: UniversalSoftwareVideoSource.Source
let state: ValuePromise<UniversalSoftwareVideoSourceState>
let cancelInitialization: Signal<Bool, NoError>
let automaticallyFetchHeader: Bool
let hintVP9: Bool
init(
mediaBox: MediaBox,
source: UniversalSoftwareVideoSource.Source,
state: ValuePromise<UniversalSoftwareVideoSourceState>,
cancelInitialization: Signal<Bool, NoError>,
automaticallyFetchHeader: Bool,
hintVP9: Bool
) {
self.mediaBox = mediaBox
self.source = source
self.state = state
self.cancelInitialization = cancelInitialization
self.automaticallyFetchHeader = automaticallyFetchHeader
self.hintVP9 = hintVP9
}
}
private final class UniversalSoftwareVideoSourceTakeFrameParams: NSObject {
let timestamp: Double
let completion: (UIImage?) -> Void
let cancel: Signal<Bool, NoError>
let requiredDataIsNotLocallyAvailable: () -> Void
init(timestamp: Double, completion: @escaping (UIImage?) -> Void, cancel: Signal<Bool, NoError>, requiredDataIsNotLocallyAvailable: @escaping () -> Void) {
self.timestamp = timestamp
self.completion = completion
self.cancel = cancel
self.requiredDataIsNotLocallyAvailable = requiredDataIsNotLocallyAvailable
}
}
private final class UniversalSoftwareVideoSourceThread: NSObject {
@objc static func entryPoint(_ params: UniversalSoftwareVideoSourceThreadParams) {
let runLoop = RunLoop.current
let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.none), userInfo: nil, repeats: false)
runLoop.add(timer, forMode: .common)
let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, source: params.source, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader)
Thread.current.threadDictionary["source"] = source
while true {
runLoop.run(mode: .default, before: .distantFuture)
if Thread.current.threadDictionary["UniversalSoftwareVideoSourceThread_stop"] != nil {
break
}
}
Thread.current.threadDictionary.removeObject(forKey: "source")
}
@objc static func none() {
}
@objc static func stop() {
Thread.current.threadDictionary["UniversalSoftwareVideoSourceThread_stop"] = "true"
}
@objc static func takeFrame(_ params: UniversalSoftwareVideoSourceTakeFrameParams) {
guard let source = Thread.current.threadDictionary["source"] as? UniversalSoftwareVideoSourceImpl else {
params.completion(nil)
return
}
source.cancelRead = params.cancel
source.requiredDataIsNotLocallyAvailable = params.requiredDataIsNotLocallyAvailable
source.state.set(.generatingFrame)
let image = source.readImage(at: params.timestamp).0
source.cancelRead = .single(false)
source.requiredDataIsNotLocallyAvailable = nil
source.state.set(.ready)
params.completion(image)
}
}
public enum UniversalSoftwareVideoSourceTakeFrameResult {
case waitingForData
case image(UIImage?)
}
public final class UniversalSoftwareVideoSource {
public enum Source {
case file(
userLocation: MediaResourceUserLocation,
userContentType: MediaResourceUserContentType,
fileReference: FileMediaReference
)
case direct(
resource: MediaResource,
size: Int64
)
var resource: MediaResource {
switch self {
case let .file(_, _, fileReference):
return fileReference.media.resource
case let .direct(resource, _):
return resource
}
}
}
private let thread: Thread
private let stateValue: ValuePromise<UniversalSoftwareVideoSourceState> = ValuePromise(.initializing, ignoreRepeated: true)
private let cancelInitialization: ValuePromise<Bool> = ValuePromise(false)
public var ready: Signal<Bool, NoError> {
return self.stateValue.get()
|> map { value -> Bool in
switch value {
case .ready:
return true
default:
return false
}
}
}
public init(mediaBox: MediaBox, source: Source, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) {
self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, source: source, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9))
self.thread.name = "UniversalSoftwareVideoSource"
self.thread.start()
}
deinit {
UniversalSoftwareVideoSourceThread.self.perform(#selector(UniversalSoftwareVideoSourceThread.stop), on: self.thread, with: nil, waitUntilDone: false)
self.cancelInitialization.set(true)
}
public func takeFrame(at timestamp: Double) -> Signal<UniversalSoftwareVideoSourceTakeFrameResult, NoError> {
return Signal { subscriber in
let cancel = ValuePromise<Bool>(false)
UniversalSoftwareVideoSourceThread.self.perform(#selector(UniversalSoftwareVideoSourceThread.takeFrame(_:)), on: self.thread, with: UniversalSoftwareVideoSourceTakeFrameParams(timestamp: timestamp, completion: { image in
subscriber.putNext(.image(image))
subscriber.putCompletion()
}, cancel: cancel.get(), requiredDataIsNotLocallyAvailable: {
subscriber.putNext(.waitingForData)
}), waitUntilDone: false)
return ActionDisposable {
cancel.set(true)
}
}
}
}
@@ -0,0 +1,125 @@
import Foundation
import SwiftSignalKit
import AVFoundation
private final class VideoPlayerProxyContext {
private let queue: Queue
var updateVideoInHierarchy: ((Bool) -> Void)?
var node: MediaPlayerNode? {
didSet {
self.node?.takeFrameAndQueue = self.takeFrameAndQueue
self.node?.state = state
self.updateVideoInHierarchy?(node?.videoInHierarchy ?? false)
self.node?.updateVideoInHierarchy = { [weak self] value in
self?.updateVideoInHierarchy?(value)
}
}
}
var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)? {
didSet {
self.node?.takeFrameAndQueue = self.takeFrameAndQueue
}
}
var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? {
didSet {
self.node?.state = self.state
}
}
init(queue: Queue) {
self.queue = queue
}
deinit {
assert(self.queue.isCurrent())
}
}
final class VideoPlayerProxy {
var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)? {
didSet {
let updatedTakeFrameAndQueue = self.takeFrameAndQueue
self.withContext { context in
context?.takeFrameAndQueue = updatedTakeFrameAndQueue
}
}
}
var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? {
didSet {
let updatedState = self.state
self.withContext { context in
context?.state = updatedState
}
}
}
private let queue: Queue
private let contextQueue = Queue.mainQueue()
private var contextRef: Unmanaged<VideoPlayerProxyContext>?
var visibility: Bool = false
var visibilityUpdated: ((Bool) -> Void)?
init(queue: Queue) {
self.queue = queue
self.contextQueue.async {
let context = VideoPlayerProxyContext(queue: self.contextQueue)
context.updateVideoInHierarchy = { [weak self] value in
queue.async {
if let strongSelf = self {
if strongSelf.visibility != value {
strongSelf.visibility = value
strongSelf.visibilityUpdated?(value)
}
}
}
}
self.contextRef = Unmanaged.passRetained(context)
}
}
deinit {
let contextRef = self.contextRef
self.contextQueue.async {
if let contextRef = contextRef {
let context = contextRef.takeUnretainedValue()
context.state = nil
contextRef.release()
}
}
}
private func withContext(_ f: @escaping (VideoPlayerProxyContext?) -> Void) {
self.contextQueue.async {
if let contextRef = self.contextRef {
let context = contextRef.takeUnretainedValue()
f(context)
} else {
f(nil)
}
}
}
func attachNodeAndRelease(_ nodeRef: Unmanaged<MediaPlayerNode>) {
self.withContext { context in
if let context = context {
context.node = nodeRef.takeUnretainedValue()
}
nodeRef.release()
}
}
func flush() {
self.withContext { context in
if let context = context {
context.node?.reset()
}
}
}
}