mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
d7fadef9ef
commit
3371078944
@ -200,14 +200,10 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
|
||||
}
|
||||
}
|
||||
}
|
||||
if point.x < activeEdgeWidth(width: size.width) {
|
||||
if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) {
|
||||
activeSide = false
|
||||
}
|
||||
} else if point.x > size.width - activeEdgeWidth(width: size.width) {
|
||||
if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) {
|
||||
activeSide = true
|
||||
}
|
||||
if point.x < activeEdgeWidth(width: size.width), let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) {
|
||||
activeSide = false
|
||||
} else if point.x > 0.0, let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) {
|
||||
activeSide = true
|
||||
}
|
||||
|
||||
if !strongSelf.pagingEnabled {
|
||||
@ -250,14 +246,10 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
|
||||
}
|
||||
}
|
||||
}
|
||||
if point.x < activeEdgeWidth(width: size.width) {
|
||||
if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) {
|
||||
activeSide = false
|
||||
}
|
||||
} else if point.x > size.width - activeEdgeWidth(width: size.width) {
|
||||
if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) {
|
||||
activeSide = true
|
||||
}
|
||||
if point.x < activeEdgeWidth(width: size.width), let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) {
|
||||
activeSide = false
|
||||
} else if point.x > 0.0, let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) {
|
||||
activeSide = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1336,7 +1336,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private let playbackRatePromise = ValuePromise<Double>()
|
||||
private let videoQualityPromise = ValuePromise<UniversalVideoContentVideoQuality>()
|
||||
|
||||
private var playerStatusValue: MediaPlayerStatus?
|
||||
private let statusDisposable = MetaDisposable()
|
||||
|
||||
private let moreButtonStateDisposable = MetaDisposable()
|
||||
private let settingsButtonStateDisposable = MetaDisposable()
|
||||
private let mediaPlaybackStateDisposable = MetaDisposable()
|
||||
@ -1927,6 +1929,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
|
||||
if let strongSelf = self {
|
||||
strongSelf.playerStatusValue = value
|
||||
|
||||
var initialBuffering = false
|
||||
var isPlaying = false
|
||||
var isPaused = true
|
||||
@ -3598,84 +3602,109 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
var allFiles: [FileMediaReference] = []
|
||||
allFiles.append(content.fileReference)
|
||||
allFiles.append(contentsOf: qualitySet.qualityFiles.values)
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c?.popItems()
|
||||
})))
|
||||
|
||||
let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in
|
||||
guard let qualityFileSize = qualityFile.media.size else {
|
||||
let qualitySignals = allFiles.map { file -> Signal<(fileId: MediaId, isCached: Bool), NoError> in
|
||||
return self.context.account.postbox.mediaBox.resourceStatus(file.media.resource)
|
||||
|> take(1)
|
||||
|> map { status -> (fileId: MediaId, isCached: Bool) in
|
||||
return (file.media.fileId, status == .Local)
|
||||
}
|
||||
}
|
||||
let _ = (combineLatest(queue: .mainQueue(), qualitySignals)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak c] fileStatuses in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
|
||||
let title: String
|
||||
if let quality {
|
||||
title = self.presentationData.strings.Gallery_SaveToGallery_Quality("\(quality)").string
|
||||
} else {
|
||||
title = self.presentationData.strings.Gallery_SaveToGallery_Original
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: .secondLineWithValue(fileSizeString), icon: { _ in
|
||||
return nil
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let controller = self.galleryController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let saveScreen = SaveProgressScreen(context: self.context, content: .progress(self.presentationData.strings.Story_TooltipSaving, 0.0))
|
||||
controller.present(saveScreen, in: .current)
|
||||
|
||||
let stringSaving = self.presentationData.strings.Story_TooltipSaving
|
||||
let stringSaved = self.presentationData.strings.Story_TooltipSaved
|
||||
|
||||
let saveFileReference: AnyMediaReference = qualityFile.abstract
|
||||
let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference)
|
||||
|
||||
let disposable = (saveSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak saveScreen] progress in
|
||||
guard let saveScreen else {
|
||||
return
|
||||
}
|
||||
saveScreen.content = .progress(stringSaving, progress)
|
||||
}, completed: { [weak saveScreen] in
|
||||
guard let saveScreen else {
|
||||
return
|
||||
}
|
||||
saveScreen.content = .completion(stringSaved)
|
||||
Queue.mainQueue().after(3.0, { [weak saveScreen] in
|
||||
saveScreen?.dismiss()
|
||||
})
|
||||
})
|
||||
|
||||
saveScreen.cancelled = {
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c?.popItems()
|
||||
})))
|
||||
}
|
||||
|
||||
if self.context.isPremium {
|
||||
addItem(nil, content.fileReference)
|
||||
} else {
|
||||
#if DEBUG
|
||||
addItem(nil, content.fileReference)
|
||||
#endif
|
||||
}
|
||||
|
||||
for quality in qualityState.available {
|
||||
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
||||
continue
|
||||
|
||||
let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in
|
||||
guard let qualityFileSize = qualityFile.media.size else {
|
||||
return
|
||||
}
|
||||
var fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
|
||||
let title: String
|
||||
if let quality {
|
||||
title = self.presentationData.strings.Gallery_SaveToGallery_Quality("\(quality)").string
|
||||
} else {
|
||||
title = self.presentationData.strings.Gallery_SaveToGallery_Original
|
||||
}
|
||||
|
||||
if let statusValue = fileStatuses.first(where: { $0.fileId == qualityFile.media.fileId }), statusValue.isCached {
|
||||
fileSizeString.append(" • cached")
|
||||
} else {
|
||||
fileSizeString.insert(contentsOf: "↓ ", at: fileSizeString.startIndex)
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: .secondLineWithValue(fileSizeString), icon: { _ in
|
||||
return nil
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let controller = self.galleryController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let saveScreen = SaveProgressScreen(context: self.context, content: .progress(self.presentationData.strings.Story_TooltipSaving, 0.0))
|
||||
controller.present(saveScreen, in: .current)
|
||||
|
||||
let stringSaving = self.presentationData.strings.Story_TooltipSaving
|
||||
let stringSaved = self.presentationData.strings.Story_TooltipSaved
|
||||
|
||||
let saveFileReference: AnyMediaReference = qualityFile.abstract
|
||||
let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference)
|
||||
|
||||
let disposable = (saveSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak saveScreen] progress in
|
||||
guard let saveScreen else {
|
||||
return
|
||||
}
|
||||
saveScreen.content = .progress(stringSaving, progress)
|
||||
}, completed: { [weak saveScreen] in
|
||||
guard let saveScreen else {
|
||||
return
|
||||
}
|
||||
saveScreen.content = .completion(stringSaved)
|
||||
Queue.mainQueue().after(3.0, { [weak saveScreen] in
|
||||
saveScreen?.dismiss()
|
||||
})
|
||||
})
|
||||
|
||||
saveScreen.cancelled = {
|
||||
disposable.dispose()
|
||||
}
|
||||
})))
|
||||
}
|
||||
addItem(quality, qualityFile)
|
||||
}
|
||||
|
||||
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
|
||||
|
||||
if self.context.isPremium {
|
||||
addItem(nil, content.fileReference)
|
||||
} else {
|
||||
#if DEBUG
|
||||
addItem(nil, content.fileReference)
|
||||
#endif
|
||||
}
|
||||
|
||||
for quality in qualityState.available {
|
||||
guard let qualityFile = qualitySet.qualityFiles[quality] else {
|
||||
continue
|
||||
}
|
||||
addItem(quality, qualityFile)
|
||||
}
|
||||
|
||||
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
|
||||
})
|
||||
} else {
|
||||
c?.dismiss(result: .default, completion: nil)
|
||||
|
||||
@ -3984,7 +4013,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
override func hasActiveEdgeAction(edge: ActiveEdge) -> Bool {
|
||||
if case .right = edge {
|
||||
return true
|
||||
if let playerStatusValue = self.playerStatusValue, case .playing = playerStatusValue.status {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@ -3997,13 +4030,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if let edge, case .right = edge {
|
||||
let effectiveRate: Double
|
||||
if let current = self.activeEdgeRateState {
|
||||
effectiveRate = min(2.5, current.initialRate + 0.5)
|
||||
effectiveRate = min(4.0, current.initialRate + 1.0)
|
||||
self.activeEdgeRateState = (current.initialRate, effectiveRate)
|
||||
} else {
|
||||
guard let playbackRate = self.playbackRate else {
|
||||
return
|
||||
}
|
||||
effectiveRate = min(2.5, playbackRate + 0.5)
|
||||
effectiveRate = min(4.0, playbackRate + 1.0)
|
||||
self.activeEdgeRateState = (playbackRate, effectiveRate)
|
||||
}
|
||||
videoNode.setBaseRate(effectiveRate)
|
||||
@ -4023,9 +4056,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
if let current = self.activeEdgeRateState {
|
||||
var rateFraction = Double(distance) / 100.0
|
||||
rateFraction = max(0.0, min(1.0, rateFraction))
|
||||
let rateDistance = (current.initialRate + 0.5) * (1.0 - rateFraction) + 2.5 * rateFraction
|
||||
let effectiveRate = max(1.0, min(2.5, rateDistance))
|
||||
rateFraction = max(-1.0, min(1.0, rateFraction))
|
||||
|
||||
let effectiveRate: Double
|
||||
if rateFraction < 0.0 {
|
||||
let rateDistance = (current.initialRate + 1.0) * (1.0 - (-rateFraction)) + 1.0 * (-rateFraction)
|
||||
effectiveRate = max(1.0, min(4.0, rateDistance))
|
||||
} else {
|
||||
let rateDistance = (current.initialRate + 1.0) * (1.0 - rateFraction) + 3.0 * rateFraction
|
||||
effectiveRate = max(1.0, min(4.0, rateDistance))
|
||||
}
|
||||
self.activeEdgeRateState = (current.initialRate, effectiveRate)
|
||||
videoNode.setBaseRate(effectiveRate)
|
||||
|
||||
|
@ -317,6 +317,15 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func storeResourceData(_ id: MediaResourceId, range: Range<Int64>, data: Data) {
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, dispose) = self.fileContext(for: id) {
|
||||
fileContext.internalStore(data: data, range: range)
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func moveResourceData(_ id: MediaResourceId, fromTempPath: String) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
|
@ -748,139 +748,3 @@ private final class MediaBoxFileMissingRanges {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum MediaBoxFileContent {
|
||||
case complete(String, Int64)
|
||||
case partial(MediaBoxPartialFile)
|
||||
}
|
||||
|
||||
final class MediaBoxFileContextImpl: MediaBoxFileContext {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private let partialPath: String
|
||||
private let metaPath: String
|
||||
|
||||
private var content: MediaBoxFileContent
|
||||
|
||||
private let references = CounterBag()
|
||||
|
||||
var isEmpty: Bool {
|
||||
return self.references.isEmpty
|
||||
}
|
||||
|
||||
init?(queue: Queue, manager: MediaBoxFileManager, storageBox: StorageBox, resourceId: Data, path: String, partialPath: String, metaPath: String) {
|
||||
assert(queue.isCurrent())
|
||||
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
self.partialPath = partialPath
|
||||
self.metaPath = metaPath
|
||||
|
||||
var completeImpl: ((Int64) -> Void)?
|
||||
if let size = fileSize(path) {
|
||||
self.content = .complete(path, size)
|
||||
} else if let file = MediaBoxPartialFile(queue: queue, manager: manager, storageBox: storageBox, resourceId: resourceId, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in
|
||||
completeImpl?(size)
|
||||
}) {
|
||||
self.content = .partial(file)
|
||||
completeImpl = { [weak self] size in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.content = .complete(path, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func addReference() -> Int {
|
||||
return self.references.add()
|
||||
}
|
||||
|
||||
func removeReference(_ index: Int) {
|
||||
self.references.remove(index)
|
||||
}
|
||||
|
||||
func data(range: Range<Int64>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case let .complete(path, size):
|
||||
var lowerBound = range.lowerBound
|
||||
if lowerBound < 0 {
|
||||
lowerBound = 0
|
||||
}
|
||||
if lowerBound > size {
|
||||
lowerBound = size
|
||||
}
|
||||
var upperBound = range.upperBound
|
||||
if upperBound < 0 {
|
||||
upperBound = 0
|
||||
}
|
||||
if upperBound > size {
|
||||
upperBound = size
|
||||
}
|
||||
if upperBound < lowerBound {
|
||||
upperBound = lowerBound
|
||||
}
|
||||
|
||||
next(MediaResourceData(path: path, offset: lowerBound, size: upperBound - lowerBound, complete: true))
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.data(range: range, waitUntilAfterInitialFetch: waitUntilAfterInitialFetch, next: next)
|
||||
}
|
||||
}
|
||||
|
||||
func fetched(range: Range<Int64>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.fetched(range: range, priority: priority, fetch: fetch, error: error, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchedFullRange(fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.fetchedFullRange(fetch: fetch, error: error, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFullRangeFetches() {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
break
|
||||
case let .partial(file):
|
||||
file.cancelFullRangeFetches()
|
||||
}
|
||||
}
|
||||
|
||||
func rangeStatus(next: @escaping (RangeSet<Int64>) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case let .complete(_, size):
|
||||
next(RangeSet<Int64>(0 ..< size))
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.rangeStatus(next: next, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
next(.Local)
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.status(next: next, completed: completed, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,4 +15,6 @@ protocol MediaBoxFileContext: AnyObject {
|
||||
func cancelFullRangeFetches()
|
||||
func rangeStatus(next: @escaping (RangeSet<Int64>) -> Void, completed: @escaping () -> Void) -> Disposable
|
||||
func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable
|
||||
|
||||
func internalStore(data: Data, range: Range<Int64>)
|
||||
}
|
||||
|
@ -296,6 +296,16 @@ public final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
}
|
||||
}
|
||||
|
||||
func internalStore(data: Data, range: Range<Int64>) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if data.count == Int(range.upperBound - range.lowerBound) {
|
||||
self.processFetchResult(result: .dataPart(resourceOffset: range.lowerBound, data: data, range: 0 ..< Int64(data.count), complete: false))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRequests() {
|
||||
var rangesByPriority: [MediaBoxFetchPriority: RangeSet<Int64>] = [:]
|
||||
for (index, rangeRequest) in self.rangeRequests.copyItemsWithIndices() {
|
||||
@ -795,4 +805,13 @@ public final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func internalStore(data: Data, range: Range<Int64>) {
|
||||
if let _ = fileSize(self.path) {
|
||||
} else {
|
||||
self.withPartialState { partialState in
|
||||
partialState.internalStore(data: data, range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1491,15 +1491,9 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
|
||||
let timestamp = Int(CFAbsoluteTimeGetCurrent())
|
||||
let minReindexTimestamp = timestamp - 2 * 24 * 60 * 60
|
||||
if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp") as? NSNumber, indexTimestamp.intValue >= minReindexTimestamp {
|
||||
#if DEBUG && false
|
||||
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground")
|
||||
let _ = self.runCacheReindexTasks(lowImpact: true, completion: {
|
||||
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done1")
|
||||
})
|
||||
#endif
|
||||
if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp_v2") as? NSNumber, indexTimestamp.intValue >= minReindexTimestamp {
|
||||
} else {
|
||||
UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp")
|
||||
UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp_v2")
|
||||
|
||||
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground")
|
||||
let _ = self.runCacheReindexTasks(lowImpact: true, completion: {
|
||||
|
@ -146,7 +146,7 @@ final class HLSJSServerSource: SharedHLSServer.Source {
|
||||
guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else {
|
||||
return .single(nil)
|
||||
}
|
||||
let _ = quality
|
||||
|
||||
guard let size = file.media.size else {
|
||||
return .single(nil)
|
||||
}
|
||||
@ -154,115 +154,174 @@ final class HLSJSServerSource: SharedHLSServer.Source {
|
||||
let postbox = self.postbox
|
||||
let userLocation = self.userLocation
|
||||
|
||||
let playlistPreloadRange = self.playlistData(quality: quality)
|
||||
|> mapToSignal { playlistString -> Signal<Range<Int64>?, NoError> in
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prefixSeconds = 10
|
||||
var rangeUpperBound: Int64 = 0
|
||||
if durations.count == byteRanges.count {
|
||||
var remainingSeconds = prefixSeconds
|
||||
|
||||
for i in 0 ..< durations.count {
|
||||
if remainingSeconds <= 0 {
|
||||
break
|
||||
}
|
||||
let duration = durations[i]
|
||||
let byteRange = byteRanges[i]
|
||||
|
||||
remainingSeconds -= duration
|
||||
rangeUpperBound = max(rangeUpperBound, Int64(byteRange.upperBound))
|
||||
}
|
||||
}
|
||||
|
||||
if rangeUpperBound != 0 {
|
||||
return .single(0 ..< rangeUpperBound)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
let mappedRange: Range<Int64> = Int64(range.lowerBound) ..< Int64(range.upperBound)
|
||||
|
||||
let queue = postbox.mediaBox.dataQueue
|
||||
let fetchFromRemote: Signal<(TempBoxFile, Range<Int>, Int)?, NoError> = Signal { subscriber in
|
||||
let partialFile = TempBox.shared.tempFile(fileName: "data")
|
||||
|
||||
if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) {
|
||||
let fetchFromRemote: Signal<(TempBoxFile, Range<Int>, Int)?, NoError> = playlistPreloadRange
|
||||
|> mapToSignal { preloadRange -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> in
|
||||
return Signal { subscriber in
|
||||
let partialFile = TempBox.shared.tempFile(fileName: "data")
|
||||
|
||||
if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) {
|
||||
#if DEBUG
|
||||
print("Fetched \(quality)p part from cache")
|
||||
#endif
|
||||
|
||||
let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite)
|
||||
if let outputFile {
|
||||
let blockSize = 128 * 1024
|
||||
var tempBuffer = Data(count: blockSize)
|
||||
var blockOffset = 0
|
||||
while blockOffset < cachedData.length {
|
||||
let currentBlockSize = min(cachedData.length - blockOffset, blockSize)
|
||||
|
||||
tempBuffer.withUnsafeMutableBytes { bytes -> Void in
|
||||
let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize)
|
||||
let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize)
|
||||
}
|
||||
|
||||
blockOffset += blockSize
|
||||
}
|
||||
outputFile._unsafeClose()
|
||||
subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("Error writing cached file to disk")
|
||||
#endif
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
guard let fetchResource = postbox.mediaBox.fetchResource else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource))
|
||||
let params = MediaResourceFetchParameters(
|
||||
tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video),
|
||||
info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true),
|
||||
location: location,
|
||||
contentType: .video,
|
||||
isRandomAccessAllowed: true
|
||||
)
|
||||
|
||||
let completeFile = TempBox.shared.tempFile(fileName: "data")
|
||||
let metaFile = TempBox.shared.tempFile(fileName: "data")
|
||||
|
||||
guard let fileContext = MediaBoxFileContextV2Impl(
|
||||
queue: queue,
|
||||
manager: postbox.mediaBox.dataFileManager,
|
||||
storageBox: nil,
|
||||
resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!,
|
||||
path: completeFile.path,
|
||||
partialPath: partialFile.path,
|
||||
metaPath: metaFile.path
|
||||
) else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let fetchDisposable = fileContext.fetched(
|
||||
range: mappedRange,
|
||||
priority: .default,
|
||||
fetch: { intervals in
|
||||
return fetchResource(file.media.resource, intervals, params)
|
||||
},
|
||||
error: { _ in
|
||||
},
|
||||
completed: {
|
||||
}
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
print("Fetched \(quality)p part from cache")
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
#endif
|
||||
|
||||
let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite)
|
||||
if let outputFile {
|
||||
let blockSize = 128 * 1024
|
||||
var tempBuffer = Data(count: blockSize)
|
||||
var blockOffset = 0
|
||||
while blockOffset < cachedData.length {
|
||||
let currentBlockSize = min(cachedData.length - blockOffset, blockSize)
|
||||
|
||||
tempBuffer.withUnsafeMutableBytes { bytes -> Void in
|
||||
let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize)
|
||||
let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize)
|
||||
let dataDisposable = fileContext.data(
|
||||
range: mappedRange,
|
||||
waitUntilAfterInitialFetch: true,
|
||||
next: { result in
|
||||
if result.complete {
|
||||
#if DEBUG
|
||||
let fetchTime = CFAbsoluteTimeGetCurrent() - startTime
|
||||
print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms")
|
||||
#endif
|
||||
if let preloadRange, Int(preloadRange.lowerBound) <= range.upperBound && Int(preloadRange.upperBound) >= range.lowerBound {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: partialFile.path), options: .alwaysMapped) {
|
||||
let subData = data.subdata(in: Int(result.offset) ..< Int(result.offset + result.size))
|
||||
postbox.mediaBox.storeResourceData(file.media.resource.id, range: Int64(range.lowerBound) ..< Int64(range.upperBound), data: subData)
|
||||
}
|
||||
}
|
||||
subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size)))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
blockOffset += blockSize
|
||||
}
|
||||
outputFile._unsafeClose()
|
||||
subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("Error writing cached file to disk")
|
||||
#endif
|
||||
}
|
||||
)
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
guard let fetchResource = postbox.mediaBox.fetchResource else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource))
|
||||
let params = MediaResourceFetchParameters(
|
||||
tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video),
|
||||
info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true),
|
||||
location: location,
|
||||
contentType: .video,
|
||||
isRandomAccessAllowed: true
|
||||
)
|
||||
|
||||
let completeFile = TempBox.shared.tempFile(fileName: "data")
|
||||
let metaFile = TempBox.shared.tempFile(fileName: "data")
|
||||
|
||||
guard let fileContext = MediaBoxFileContextV2Impl(
|
||||
queue: queue,
|
||||
manager: postbox.mediaBox.dataFileManager,
|
||||
storageBox: nil,
|
||||
resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!,
|
||||
path: completeFile.path,
|
||||
partialPath: partialFile.path,
|
||||
metaPath: metaFile.path
|
||||
) else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let fetchDisposable = fileContext.fetched(
|
||||
range: mappedRange,
|
||||
priority: .default,
|
||||
fetch: { intervals in
|
||||
return fetchResource(file.media.resource, intervals, params)
|
||||
},
|
||||
error: { _ in
|
||||
},
|
||||
completed: {
|
||||
}
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
#endif
|
||||
|
||||
let dataDisposable = fileContext.data(
|
||||
range: mappedRange,
|
||||
waitUntilAfterInitialFetch: true,
|
||||
next: { result in
|
||||
if result.complete {
|
||||
#if DEBUG
|
||||
let fetchTime = CFAbsoluteTimeGetCurrent() - startTime
|
||||
print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms")
|
||||
#endif
|
||||
subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size)))
|
||||
subscriber.putCompletion()
|
||||
return ActionDisposable {
|
||||
queue.async {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposable.dispose()
|
||||
fileContext.cancelFullRangeFetches()
|
||||
|
||||
TempBox.shared.dispose(completeFile)
|
||||
TempBox.shared.dispose(metaFile)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return ActionDisposable {
|
||||
queue.async {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposable.dispose()
|
||||
fileContext.cancelFullRangeFetches()
|
||||
|
||||
TempBox.shared.dispose(completeFile)
|
||||
TempBox.shared.dispose(metaFile)
|
||||
}
|
||||
}
|
||||
|> runOn(queue)
|
||||
}
|
||||
|> runOn(queue)
|
||||
|
||||
return fetchFromRemote
|
||||
}
|
||||
@ -1287,8 +1346,15 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
||||
if !self.hasRequestedPlayerLoad {
|
||||
if !self.playerAvailableLevels.isEmpty {
|
||||
var selectedLevelIndex: Int?
|
||||
if let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file {
|
||||
if let dimensions = minimizedQualityFile.media.dimensions {
|
||||
|
||||
if let qualityFiles = HLSQualitySet(baseFile: self.fileReference)?.qualityFiles.values, let maxQualityFile = qualityFiles.max(by: { lhs, rhs in
|
||||
if let lhsDimensions = lhs.media.dimensions, let rhsDimensions = rhs.media.dimensions {
|
||||
return lhsDimensions.width < rhsDimensions.width
|
||||
} else {
|
||||
return lhs.media.fileId.id < rhs.media.fileId.id
|
||||
}
|
||||
}), let dimensions = maxQualityFile.media.dimensions {
|
||||
if self.postbox.mediaBox.completedResourcePath(maxQualityFile.media.resource) != nil {
|
||||
for (index, level) in self.playerAvailableLevels {
|
||||
if level.height == Int(dimensions.height) {
|
||||
selectedLevelIndex = index
|
||||
@ -1297,6 +1363,19 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedLevelIndex == nil {
|
||||
if let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file {
|
||||
if let dimensions = minimizedQualityFile.media.dimensions {
|
||||
for (index, level) in self.playerAvailableLevels {
|
||||
if level.height == Int(dimensions.height) {
|
||||
selectedLevelIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if selectedLevelIndex == nil {
|
||||
selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key
|
||||
}
|
||||
@ -1612,10 +1691,29 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
|
||||
}
|
||||
}
|
||||
|
||||
guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else {
|
||||
return nil
|
||||
let currentLevelIndex: Int
|
||||
if let playerCurrentLevelIndex = self.playerCurrentLevelIndex {
|
||||
currentLevelIndex = playerCurrentLevelIndex
|
||||
} else {
|
||||
if let minQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file, let dimensions = minQualityFile.media.dimensions {
|
||||
var foundIndex: Int?
|
||||
for (index, level) in self.playerAvailableLevels {
|
||||
if level.width == Int(dimensions.width) && level.height == Int(dimensions.height) {
|
||||
foundIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if let foundIndex {
|
||||
currentLevelIndex = foundIndex
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
guard let currentLevel = self.playerAvailableLevels[playerCurrentLevelIndex] else {
|
||||
|
||||
guard let currentLevel = self.playerAvailableLevels[currentLevelIndex] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user