Swiftgram/submodules/TelegramCallsUI/Sources/Components/LivestreamVideoViewV1.swift
2025-04-29 16:44:43 +02:00

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)
}
}