mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
280 lines
10 KiB
Swift
280 lines
10 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AVFoundation
|
|
import SwiftSignalKit
|
|
import UniversalMediaPlayer
|
|
import Postbox
|
|
import TelegramCore
|
|
import AccountContext
|
|
import TelegramAudio
|
|
import Display
|
|
import TelegramVoip
|
|
import RangeSet
|
|
import ManagedFile
|
|
import FFMpegBinding
|
|
import TelegramUniversalVideoContent
|
|
|
|
final class LivestreamVideoViewV1: UIView {
|
|
private final class PartContext {
|
|
let part: DirectMediaStreamingContext.Playlist.Part
|
|
let disposable = MetaDisposable()
|
|
var resolvedTimeOffset: Double?
|
|
var data: TempBoxFile?
|
|
var info: FFMpegMediaInfo?
|
|
|
|
init(part: DirectMediaStreamingContext.Playlist.Part) {
|
|
self.part = part
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let audioSessionManager: ManagedAudioSession
|
|
private let call: PresentationGroupCall
|
|
|
|
private let chunkPlayerPartsState = Promise<ChunkMediaPlayerPartsState>(ChunkMediaPlayerPartsState(duration: 10000000.0, content: .parts([])))
|
|
private var parts: [ChunkMediaPlayerPart] = [] {
|
|
didSet {
|
|
self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: 10000000.0, content: .parts(self.parts))))
|
|
}
|
|
}
|
|
|
|
private let player: ChunkMediaPlayer
|
|
private let playerNode: MediaPlayerNode
|
|
|
|
private var playerStatus: MediaPlayerStatus?
|
|
private var playerStatusDisposable: Disposable?
|
|
|
|
private var streamingContextDisposable: Disposable?
|
|
private var streamingContext: DirectMediaStreamingContext?
|
|
private var playlistDisposable: Disposable?
|
|
|
|
private var partContexts: [Int: PartContext] = [:]
|
|
|
|
private var requestedSeekTimestamp: Double?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
audioSessionManager: ManagedAudioSession,
|
|
call: PresentationGroupCall
|
|
) {
|
|
self.context = context
|
|
self.audioSessionManager = audioSessionManager
|
|
self.call = call
|
|
|
|
self.playerNode = MediaPlayerNode()
|
|
|
|
var onSeeked: (() -> Void)?
|
|
self.player = ChunkMediaPlayerV2(
|
|
params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context),
|
|
audioContext: ChunkMediaPlayerV2.AudioContext(audioSessionManager: audioSessionManager),
|
|
source: .externalParts(self.chunkPlayerPartsState.get()),
|
|
video: true,
|
|
enableSound: true,
|
|
baseRate: 1.0,
|
|
onSeeked: {
|
|
onSeeked?()
|
|
},
|
|
playerNode: self.playerNode
|
|
)
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.addSubview(self.playerNode.view)
|
|
|
|
onSeeked = {
|
|
}
|
|
|
|
self.playerStatusDisposable = (self.player.status
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updatePlayerStatus(status: status)
|
|
})
|
|
|
|
var didProcessFramesToDisplay = false
|
|
self.playerNode.isHidden = true
|
|
self.playerNode.hasSentFramesToDisplay = { [weak self] in
|
|
guard let self, !didProcessFramesToDisplay else {
|
|
return
|
|
}
|
|
didProcessFramesToDisplay = true
|
|
self.playerNode.isHidden = false
|
|
}
|
|
|
|
if let call = call as? PresentationGroupCallImpl {
|
|
self.streamingContextDisposable = (call.externalMediaStream.get()
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] externalMediaStream in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.streamingContext = externalMediaStream
|
|
self.resetPlayback()
|
|
})
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.playerStatusDisposable?.dispose()
|
|
self.streamingContextDisposable?.dispose()
|
|
self.playlistDisposable?.dispose()
|
|
}
|
|
|
|
private func updatePlayerStatus(status: MediaPlayerStatus) {
|
|
self.playerStatus = status
|
|
|
|
self.updatePlaybackPositionIfNeeded()
|
|
}
|
|
|
|
private func resetPlayback() {
|
|
self.parts = []
|
|
|
|
self.playlistDisposable?.dispose()
|
|
self.playlistDisposable = nil
|
|
|
|
guard let streamingContext = self.streamingContext else {
|
|
return
|
|
}
|
|
self.playlistDisposable = (streamingContext.playlistData()
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] playlist in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updatePlaylist(playlist: playlist)
|
|
})
|
|
}
|
|
|
|
private func updatePlaylist(playlist: DirectMediaStreamingContext.Playlist) {
|
|
var validPartIds: [Int] = []
|
|
for part in playlist.parts.prefix(upTo: 4) {
|
|
validPartIds.append(part.index)
|
|
|
|
if self.partContexts[part.index] == nil {
|
|
let partContext = PartContext(part: part)
|
|
self.partContexts[part.index] = partContext
|
|
|
|
if let streamingContext = self.streamingContext {
|
|
partContext.disposable.set((streamingContext.partData(index: part.index)
|
|
|> deliverOn(Queue.concurrentDefaultQueue())
|
|
|> map { data -> (file: TempBoxFile, info: FFMpegMediaInfo)? in
|
|
guard let data else {
|
|
return nil
|
|
}
|
|
let tempFile = TempBox.shared.tempFile(fileName: "part.mp4")
|
|
if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) {
|
|
if let info = extractFFMpegMediaInfo(path: tempFile.path) {
|
|
return (tempFile, info)
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
TempBox.shared.dispose(tempFile)
|
|
return nil
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self, weak partContext] fileAndInfo in
|
|
guard let self, let partContext else {
|
|
return
|
|
}
|
|
if let (file, info) = fileAndInfo {
|
|
partContext.data = file
|
|
partContext.info = info
|
|
} else {
|
|
partContext.data = nil
|
|
}
|
|
self.updatePartContexts()
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
var removedPartIds: [Int] = []
|
|
for (id, _) in self.partContexts {
|
|
if !validPartIds.contains(id) {
|
|
removedPartIds.append(id)
|
|
}
|
|
}
|
|
for id in removedPartIds {
|
|
self.partContexts.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
private func updatePartContexts() {
|
|
var readyParts: [ChunkMediaPlayerPart] = []
|
|
let sortedContexts = self.partContexts.values.sorted(by: { $0.part.timestamp < $1.part.timestamp })
|
|
outer: for i in 0 ..< sortedContexts.count {
|
|
let partContext = sortedContexts[i]
|
|
|
|
if let data = partContext.data {
|
|
let offsetTime: Double
|
|
if i != 0 {
|
|
var foundOffset: Double?
|
|
inner: for j in 0 ..< i {
|
|
let previousContext = sortedContexts[j]
|
|
if previousContext.part.index == partContext.part.index - 1 {
|
|
if let previousInfo = previousContext.info {
|
|
if let previousResolvedOffset = previousContext.resolvedTimeOffset {
|
|
if let audio = previousInfo.audio {
|
|
foundOffset = previousResolvedOffset + audio.duration.seconds
|
|
} else {
|
|
foundOffset = partContext.part.timestamp
|
|
}
|
|
}
|
|
}
|
|
break inner
|
|
}
|
|
}
|
|
if let foundOffset {
|
|
partContext.resolvedTimeOffset = foundOffset
|
|
offsetTime = foundOffset
|
|
} else {
|
|
continue outer
|
|
}
|
|
} else {
|
|
if let resolvedOffset = partContext.resolvedTimeOffset {
|
|
offsetTime = resolvedOffset
|
|
} else {
|
|
offsetTime = partContext.part.timestamp
|
|
partContext.resolvedTimeOffset = offsetTime
|
|
}
|
|
}
|
|
|
|
readyParts.append(ChunkMediaPlayerPart(
|
|
startTime: partContext.part.timestamp,
|
|
endTime: partContext.part.timestamp + partContext.part.duration,
|
|
content: ChunkMediaPlayerPart.TempFile(file: data),
|
|
codecName: nil,
|
|
offsetTime: offsetTime
|
|
))
|
|
}
|
|
}
|
|
readyParts.sort(by: { $0.startTime < $1.startTime })
|
|
self.parts = readyParts
|
|
self.updatePlaybackPositionIfNeeded()
|
|
}
|
|
|
|
private func updatePlaybackPositionIfNeeded() {
|
|
if let part = self.parts.first {
|
|
if let playerStatus = self.playerStatus, playerStatus.timestamp < part.startTime {
|
|
if self.requestedSeekTimestamp != part.startTime {
|
|
self.requestedSeekTimestamp = part.startTime
|
|
self.player.seek(timestamp: part.startTime, play: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
//transition.updateFrame(view: self.playerNode.view, frame: CGRect(origin: CGPoint(), size: size))
|
|
self.playerNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
}
|