2023-09-16 12:38:42 +04:00

1013 lines
50 KiB
Swift

import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import FFMpegBinding
import LocalMediaResources
import LegacyMediaPickerUI
import MediaEditor
import Photos
private final class AVURLAssetCopyItem: MediaResourceDataFetchCopyLocalItem {
private let url: URL
init(url: URL) {
self.url = url
}
func copyTo(url: URL) -> Bool {
var success = true
do {
try FileManager.default.copyItem(at: self.url, to: url)
} catch {
success = false
}
return success
}
}
struct VideoConversionConfiguration {
static var defaultValue: VideoConversionConfiguration {
return VideoConversionConfiguration(remuxToFMp4: false)
}
public let remuxToFMp4: Bool
fileprivate init(remuxToFMp4: Bool) {
self.remuxToFMp4 = remuxToFMp4
}
static func with(appConfiguration: AppConfiguration) -> VideoConversionConfiguration {
// #if DEBUG
// return VideoConversionConfiguration(remuxToFMp4: true)
// #endif
if let data = appConfiguration.data, let conversion = data["video_conversion"] as? [String: Any] {
let remuxToFMp4 = conversion["remux_fmp4"] as? Bool ?? VideoConversionConfiguration.defaultValue.remuxToFMp4
return VideoConversionConfiguration(remuxToFMp4: remuxToFMp4)
} else {
return .defaultValue
}
}
}
private final class FetchVideoLibraryMediaResourceItem {
let priority: MediaBoxFetchPriority
let signal: Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>
let next: (MediaResourceDataFetchResult) -> Void
let error: (MediaResourceDataFetchError) -> Void
let completion: () -> Void
var isActive: Bool = false
var disposable: Disposable?
init(priority: MediaBoxFetchPriority, signal: Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, next: @escaping (MediaResourceDataFetchResult) -> Void, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) {
self.priority = priority
self.signal = signal
self.next = next
self.error = error
self.completion = completion
}
deinit {
self.disposable?.dispose()
}
}
private let fetchVideoLimit: Int = 2
private final class FetchVideoLibraryMediaResourceContextImpl {
private let queue: Queue
var items: [FetchVideoLibraryMediaResourceItem] = []
init(queue: Queue) {
self.queue = queue
}
func add(priority: MediaBoxFetchPriority, signal: Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, next: @escaping (MediaResourceDataFetchResult) -> Void, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) -> Disposable {
let queue = self.queue
let item = FetchVideoLibraryMediaResourceItem(priority: priority, signal: signal, next: next, error: error, completion: completion)
self.items.append(item)
self.update()
return ActionDisposable { [weak self, weak item] in
queue.async {
guard let strongSelf = self, let item = item else {
return
}
for i in 0 ..< strongSelf.items.count {
if strongSelf.items[i] === item {
strongSelf.items[i].disposable?.dispose()
strongSelf.items.remove(at: i)
strongSelf.update()
break
}
}
}
}
}
func update() {
let queue = self.queue
var activeCount = 0
for item in self.items {
if item.isActive {
activeCount += 1
}
}
while activeCount < fetchVideoLimit {
var maxPriorityIndex: Int?
for i in 0 ..< self.items.count {
if !self.items[i].isActive {
if let maxPriorityIndexValue = maxPriorityIndex {
if self.items[i].priority.rawValue > self.items[maxPriorityIndexValue].priority.rawValue {
maxPriorityIndex = i
}
} else {
maxPriorityIndex = i
}
}
}
if let maxPriorityIndex = maxPriorityIndex {
let item = self.items[maxPriorityIndex]
item.isActive = true
activeCount += 1
assert(item.disposable == nil)
item.disposable = self.items[maxPriorityIndex].signal.start(next: { [weak item] value in
queue.async {
item?.next(value)
}
}, error: { [weak self, weak item] value in
queue.async {
guard let strongSelf = self, let item = item else {
return
}
for i in 0 ..< strongSelf.items.count {
if strongSelf.items[i] === item {
strongSelf.items.remove(at: i)
item.error(value)
strongSelf.update()
break
}
}
}
}, completed: { [weak self, weak item] in
queue.async {
guard let strongSelf = self, let item = item else {
return
}
for i in 0 ..< strongSelf.items.count {
if strongSelf.items[i] === item {
strongSelf.items.remove(at: i)
item.completion()
strongSelf.update()
break
}
}
}
})
} else {
break
}
}
}
}
private final class FetchVideoLibraryMediaResourceContext {
private let queue = Queue()
private let impl: QueueLocalObject<FetchVideoLibraryMediaResourceContextImpl>
init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return FetchVideoLibraryMediaResourceContextImpl(queue: queue)
})
}
func wrap(priority: MediaBoxFetchPriority, signal: Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.add(priority: priority, signal: signal, next: { value in
subscriber.putNext(value)
}, error: { error in
subscriber.putError(error)
}, completion: {
subscriber.putCompletion()
}))
}
return disposable
}
}
}
private let throttlingContext = FetchVideoLibraryMediaResourceContext()
public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibraryMediaResource, alwaysUseModernPipeline: Bool = true) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
subscriber.putNext(.reset)
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil)
var requestId: PHImageRequestID?
let disposable = MetaDisposable()
if fetchResult.count != 0 {
let asset = fetchResult.object(at: 0)
let alreadyReceivedAsset = Atomic<Bool>(value: false)
if asset.mediaType == .image {
Logger.shared.log("FetchVideoResource", "Getting asset image \(asset.localIdentifier)")
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
requestId = PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options, resultHandler: { image, _ in
if alreadyReceivedAsset.swap(true) {
return
}
guard let image else {
return
}
Logger.shared.log("FetchVideoResource", "Got asset image \(asset.localIdentifier)")
var mediaEditorValues: MediaEditorValues?
if case let .compress(adjustmentsValue) = resource.conversion, let adjustmentsValue, adjustmentsValue.isStory {
if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: adjustmentsValue.data.makeData()) {
mediaEditorValues = values
}
} else {
fatalError()
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
let updatedSize = Atomic<Int64>(value: 0)
if let mediaEditorValues {
Logger.shared.log("FetchVideoResource", "Requesting video export")
let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, duration: 5.0, image: true, frameRate: 30.0)
let videoExport = MediaEditorVideoExport(postbox: postbox, subject: .image(image: image), configuration: configuration, outputPath: tempFile.path)
videoExport.start()
let statusDisposable = videoExport.status.start(next: { status in
switch status {
case .completed:
var value = stat()
if stat(tempFile.path, &value) == 0 {
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
if !"".isEmpty, let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) {
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true))
}
}
} else {
subscriber.putError(.generic)
}
subscriber.putCompletion()
EngineTempBox.shared.dispose(tempFile)
case .failed:
subscriber.putError(.generic)
case let .progress(progress):
subscriber.putNext(.progressUpdated(progress))
default:
break
}
})
disposable.set(ActionDisposable {
statusDisposable.dispose()
videoExport.cancel()
})
}
})
} else {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
let dimensions = PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: options, resultHandler: { avAsset, _, _ in
if alreadyReceivedAsset.swap(true) {
return
}
guard let avAsset else {
return
}
var isStory = false
var adjustments: TGVideoEditAdjustments?
var mediaEditorValues: MediaEditorValues?
switch resource.conversion {
case .passthrough:
if let asset = avAsset as? AVURLAsset {
var value = stat()
if stat(asset.url.path, &value) == 0 {
subscriber.putNext(.copyLocalItem(AVURLAssetCopyItem(url: asset.url)))
subscriber.putCompletion()
} else {
subscriber.putError(.generic)
}
return
} else {
adjustments = nil
}
case let .compress(adjustmentsValue):
let defaultPreset = TGMediaVideoConversionPreset(rawValue: UInt32(UserDefaults.standard.integer(forKey: "TG_preferredVideoPreset_v0")))
let qualityPreset = MediaQualityPreset(preset: defaultPreset)
if let adjustmentsValue = adjustmentsValue {
if adjustmentsValue.isStory {
isStory = true
if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: adjustmentsValue.data.makeData()) {
mediaEditorValues = values
}
} else if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any], let legacyAdjustments = TGVideoEditAdjustments(dictionary: dict) {
if alwaysUseModernPipeline {
mediaEditorValues = MediaEditorValues(legacyAdjustments: legacyAdjustments, defaultPreset: qualityPreset)
} else {
adjustments = legacyAdjustments
}
}
} else {
if alwaysUseModernPipeline {
mediaEditorValues = MediaEditorValues(dimensions: dimensions, qualityPreset: qualityPreset)
}
}
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
let updatedSize = Atomic<Int64>(value: 0)
if let mediaEditorValues {
let duration: Double = avAsset.duration.seconds
let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, duration: duration, frameRate: 30.0)
let videoExport = MediaEditorVideoExport(postbox: postbox, subject: .video(asset: avAsset, isStory: isStory), configuration: configuration, outputPath: tempFile.path)
videoExport.start()
let statusDisposable = videoExport.status.start(next: { status in
switch status {
case .completed:
var value = stat()
if stat(tempFile.path, &value) == 0 {
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
if !"".isEmpty, let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) {
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true))
}
}
} else {
subscriber.putError(.generic)
}
subscriber.putCompletion()
EngineTempBox.shared.dispose(tempFile)
case .failed:
subscriber.putError(.generic)
case let .progress(progress):
subscriber.putNext(.progressUpdated(progress))
default:
break
}
})
disposable.set(ActionDisposable {
statusDisposable.dispose()
videoExport.cancel()
})
} else {
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in
if let paintingData = adjustments.paintingData, paintingData.hasAnimation {
return LegacyPaintEntityRenderer(postbox: postbox, adjustments: adjustments)
} else {
return nil
}
}
let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in
/*var value = stat()
if stat(path, &value) == 0 {
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
if FFMpegRemuxer.remux(path, to: remuxedTempFile.path) {
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
}
}
}*/
}), entityRenderer: entityRenderer)!
let signalDisposable = signal.start(next: { next in
if let result = next as? TGMediaVideoConversionResult {
var value = stat()
if stat(result.fileURL.path, &value) == 0 {
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
if !"".isEmpty, let size = fileSize(result.fileURL.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(result.fileURL.path, to: remuxedTempFile.path) {
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true))
}
}
} else {
subscriber.putError(.generic)
}
subscriber.putCompletion()
EngineTempBox.shared.dispose(tempFile)
}
}, error: { _ in
subscriber.putError(.generic)
}, completed: nil)
disposable.set(ActionDisposable {
signalDisposable?.dispose()
})
}
})
}
}
return ActionDisposable {
if let requestId = requestId {
PHImageManager.default().cancelImageRequest(requestId)
}
disposable.dispose()
}
}
return throttlingContext.wrap(priority: .default, signal: signal)
}
public func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideoMediaResource, alwaysUseModernPipeline: Bool = true) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
subscriber.putNext(.reset)
var filteredPath = resource.path
if filteredPath.hasPrefix("file://") {
filteredPath = String(filteredPath[filteredPath.index(filteredPath.startIndex, offsetBy: "file://".count)])
}
let defaultPreset = TGMediaVideoConversionPreset(rawValue: UInt32(UserDefaults.standard.integer(forKey: "TG_preferredVideoPreset_v0")))
let qualityPreset = MediaQualityPreset(preset: defaultPreset)
let isImage = filteredPath.contains(".jpg")
var isStory = false
let avAsset = AVURLAsset(url: URL(fileURLWithPath: filteredPath))
var adjustments: TGVideoEditAdjustments?
var mediaEditorValues: MediaEditorValues?
if let videoAdjustments = resource.adjustments {
if videoAdjustments.isStory {
isStory = true
if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: videoAdjustments.data.makeData()) {
mediaEditorValues = values
}
} else if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any], let legacyAdjustments = TGVideoEditAdjustments(dictionary: dict) {
if alwaysUseModernPipeline && !isImage {
mediaEditorValues = MediaEditorValues(legacyAdjustments: legacyAdjustments, defaultPreset: qualityPreset)
} else {
adjustments = legacyAdjustments
}
}
} else {
if alwaysUseModernPipeline && !isImage, let track = avAsset.tracks(withMediaType: .video).first {
let dimensions = track.naturalSize.applying(track.preferredTransform)
mediaEditorValues = MediaEditorValues(dimensions: PixelDimensions(dimensions), qualityPreset: qualityPreset)
}
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
let updatedSize = Atomic<Int64>(value: 0)
if let mediaEditorValues {
let duration: Double = avAsset.duration.seconds
let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, duration: duration, frameRate: 30.0)
let subject: MediaEditorVideoExport.Subject
if isImage, let data = try? Data(contentsOf: URL(fileURLWithPath: filteredPath), options: [.mappedRead]), let image = UIImage(data: data) {
subject = .image(image: image)
} else {
subject = .video(asset: avAsset, isStory: isStory)
}
let videoExport = MediaEditorVideoExport(postbox: postbox, subject: subject, configuration: configuration, outputPath: tempFile.path)
videoExport.start()
let statusDisposable = videoExport.status.start(next: { status in
switch status {
case .completed:
var value = stat()
if stat(tempFile.path, &value) == 0 {
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
if !"".isEmpty, let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) {
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true))
}
}
} else {
subscriber.putError(.generic)
}
subscriber.putCompletion()
EngineTempBox.shared.dispose(tempFile)
case .failed:
subscriber.putError(.generic)
case let .progress(progress):
subscriber.putNext(.progressUpdated(progress))
default:
break
}
})
let disposable = MetaDisposable()
disposable.set(ActionDisposable {
statusDisposable.dispose()
videoExport.cancel()
})
return ActionDisposable {
disposable.dispose()
}
} else {
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in
if let paintingData = adjustments.paintingData, paintingData.hasAnimation {
return LegacyPaintEntityRenderer(postbox: postbox, adjustments: adjustments)
} else {
return nil
}
}
let signal: SSignal
if isImage, let entityRenderer = entityRenderer {
if let data = try? Data(contentsOf: URL(fileURLWithPath: filteredPath), options: [.mappedRead]), let image = UIImage(data: data) {
let durationSignal: SSignal = SSignal(generator: { subscriber in
let disposable = (entityRenderer.duration()).start(next: { duration in
subscriber.putNext(duration)
subscriber.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
signal = durationSignal.map(toSignal: { duration -> SSignal in
if let duration = duration as? Double {
return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in
var value = stat()
if stat(path, &value) == 0 {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
}
}
}), entityRenderer: entityRenderer)!
} else {
return SSignal.single(nil)
}
})
} else {
signal = SSignal.single(nil)
}
} else {
signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in
var value = stat()
if stat(path, &value) == 0 {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< Int64(value.st_size)
return value.st_size
}
//print("size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
}
}
}), entityRenderer: entityRenderer)!
}
let signalDisposable = signal.start(next: { next in
if let result = next as? TGMediaVideoConversionResult {
var value = stat()
if stat(result.fileURL.path, &value) == 0 {
// if config.remuxToFMp4 {
// let tempFile = TempBox.shared.tempFile(fileName: "video.mp4")
// if FFMpegRemuxer.remux(result.fileURL.path, to: tempFile.path) {
// let _ = try? FileManager.default.removeItem(atPath: result.fileURL.path)
// subscriber.putNext(.moveTempFile(file: tempFile))
// } else {
// TempBox.shared.dispose(tempFile)
// subscriber.putNext(.moveLocalFile(path: result.fileURL.path))
// }
// } else {
// subscriber.putNext(.moveLocalFile(path: result.fileURL.path))
// }
if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) {
var range: Range<Int64>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< value.st_size
return value.st_size
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true))
EngineTempBox.shared.dispose(tempFile)
}
}
subscriber.putCompletion()
}
}, error: { _ in
}, completed: nil)
let disposable = ActionDisposable {
signalDisposable?.dispose()
}
return ActionDisposable {
disposable.dispose()
}
}
}
return throttlingContext.wrap(priority: .default, signal: signal)
}
public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResource) -> Signal<Data?, NoError> {
return Signal { subscriber in
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil)
var requestId: PHImageRequestID?
let disposable = MetaDisposable()
if fetchResult.count != 0 {
let asset = fetchResult.object(at: 0)
let option = PHVideoRequestOptions()
option.isNetworkAccessAllowed = true
option.deliveryMode = .highQualityFormat
let alreadyReceivedAsset = Atomic<Bool>(value: false)
requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, info in
if avAsset == nil {
subscriber.putNext(nil)
subscriber.putCompletion()
return
}
if alreadyReceivedAsset.swap(true) {
return
}
var adjustments: TGVideoEditAdjustments?
var isPassthrough = false
switch resource.conversion {
case .passthrough:
isPassthrough = true
adjustments = nil
case let .compress(adjustmentsValue):
if let adjustmentsValue = adjustmentsValue {
if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any], let legacyAdjustments = TGVideoEditAdjustments(dictionary: dict) {
adjustments = legacyAdjustments
}
}
}
let signal = TGMediaVideoConverter.hash(for: avAsset, adjustments: adjustments)!
let signalDisposable = signal.start(next: { next in
if let next = next as? String, let data = next.data(using: .utf8) {
var updatedData = data
if isPassthrough {
updatedData.reverse()
}
#if DEBUG
if "".isEmpty {
subscriber.putNext(nil)
}
#endif
subscriber.putNext(updatedData)
} else {
subscriber.putNext(nil)
}
subscriber.putCompletion()
}, error: { _ in
}, completed: nil)
disposable.set(ActionDisposable {
signalDisposable?.dispose()
})
})
}
return ActionDisposable {
if let requestId = requestId {
PHImageManager.default().cancelImageRequest(requestId)
}
disposable.dispose()
}
}
}
public func fetchLocalFileGifMediaResource(resource: LocalFileGifMediaResource) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return Signal { subscriber in
subscriber.putNext(.reset)
let disposable = MetaDisposable()
if let data = try? Data(contentsOf: URL(fileURLWithPath: resource.path), options: Data.ReadingOptions.mappedIfSafe) {
let signal = TGGifConverter.convertGif(toMp4: data)!
let signalDisposable = signal.start(next: { next in
if let result = next as? NSDictionary, let path = result["path"] as? String {
var value = stat()
if stat(path, &value) == 0 {
subscriber.putNext(.moveLocalFile(path: path))
/*if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) {
var range: Range<Int>?
let _ = updatedSize.modify { updatedSize in
range = updatedSize ..< Int(value.st_size)
return Int(value.st_size)
}
//print("finish size = \(Int(value.st_size)), range: \(range!)")
subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false))
subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024))
subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true))
}*/
}
subscriber.putCompletion()
}
}, error: { _ in
}, completed: nil)
disposable.set(ActionDisposable {
signalDisposable?.dispose()
})
}
return ActionDisposable {
disposable.dispose()
}
}
}
private extension MediaQualityPreset {
init(preset: TGMediaVideoConversionPreset) {
var qualityPreset: MediaQualityPreset
switch preset {
case TGMediaVideoConversionPresetCompressedDefault:
qualityPreset = .compressedDefault
case TGMediaVideoConversionPresetCompressedVeryLow:
qualityPreset = .compressedVeryLow
case TGMediaVideoConversionPresetCompressedLow:
qualityPreset = .compressedLow
case TGMediaVideoConversionPresetCompressedMedium:
qualityPreset = .compressedMedium
case TGMediaVideoConversionPresetCompressedHigh:
qualityPreset = .compressedHigh
case TGMediaVideoConversionPresetCompressedVeryHigh:
qualityPreset = .compressedVeryHigh
case TGMediaVideoConversionPresetProfileLow:
qualityPreset = .profileLow
case TGMediaVideoConversionPresetProfile:
qualityPreset = .profile
case TGMediaVideoConversionPresetProfileHigh:
qualityPreset = .profileHigh
case TGMediaVideoConversionPresetProfileVeryHigh:
qualityPreset = .profileVeryHigh
case TGMediaVideoConversionPresetAnimation:
qualityPreset = .animation
case TGMediaVideoConversionPresetVideoMessage:
qualityPreset = .videoMessage
default:
qualityPreset = .compressedMedium
}
self = qualityPreset
}
}
private extension UIImage.Orientation {
var cropOrientation: MediaCropOrientation {
switch self {
case .up:
return .up
case .down:
return .down
case .left:
return .left
case .right:
return .right
default:
return .up
}
}
}
private extension MediaEditorValues {
convenience init(dimensions: PixelDimensions, qualityPreset: MediaQualityPreset) {
self.init(
peerId: EnginePeer.Id(0),
originalDimensions: dimensions,
cropOffset: .zero,
cropRect: nil,
cropScale: 1.0,
cropRotation: 0.0,
cropMirroring: false,
cropOrientation: nil,
gradientColors: nil,
videoTrimRange: nil,
videoIsMuted: false,
videoIsFullHd: true,
videoIsMirrored: false,
additionalVideoPath: nil,
additionalVideoPosition: nil,
additionalVideoScale: nil,
additionalVideoRotation: nil,
additionalVideoPositionChanges: [],
drawing: nil,
entities: [],
toolValues: [:],
audioTrack: nil,
audioTrackTrimRange: nil,
audioTrackOffset: nil,
audioTrackVolume: nil,
audioTrackSamples: nil,
qualityPreset: qualityPreset
)
}
convenience init(legacyAdjustments: TGVideoEditAdjustments, defaultPreset: MediaQualityPreset) {
var videoTrimRange: Range<Double>?
if legacyAdjustments.trimStartValue > 0.0 || !legacyAdjustments.trimEndValue.isZero {
videoTrimRange = legacyAdjustments.trimStartValue ..< legacyAdjustments.trimEndValue
}
var entities: [CodableDrawingEntity] = []
var drawing: UIImage?
if let paintingData = legacyAdjustments.paintingData {
if let entitiesData = paintingData.entitiesData {
entities = decodeCodableDrawingEntities(data: entitiesData)
}
if let imagePath = paintingData.imagePath, let image = UIImage(contentsOfFile: imagePath) {
drawing = image
}
}
var toolValues: [EditorToolKey: Any] = [:]
if let tools = legacyAdjustments.toolValues {
for (key, value) in tools {
if let floatValue = (value as? NSNumber)?.floatValue {
if key == AnyHashable("enhance") {
toolValues[.enhance] = floatValue / 100.0
}
if key == AnyHashable("exposure") {
toolValues[.brightness] = floatValue / 100.0
}
if key == AnyHashable("contrast") {
toolValues[.contrast] = floatValue / 100.0
}
if key == AnyHashable("saturation") {
toolValues[.saturation] = floatValue / 100.0
}
if key == AnyHashable("warmth") {
toolValues[.warmth] = floatValue / 100.0
}
if key == AnyHashable("fade") {
toolValues[.fade] = floatValue / 100.0
}
if key == AnyHashable("vignette") {
toolValues[.vignette] = floatValue / 100.0
}
if key == AnyHashable("grain") {
toolValues[.grain] = floatValue / 100.0
}
if key == AnyHashable("highlights") {
toolValues[.highlights] = floatValue / 100.0
}
if key == AnyHashable("shadows") {
toolValues[.shadows] = floatValue / 100.0
}
}
}
}
if let value = legacyAdjustments.tintValue() {
let shadowsColor = value["shadowsColor"] as? UIColor
let shadowsIntensity = (value["shadowsIntensity"] as? NSNumber)?.floatValue
let highlightsColor = value["highlightsColor"] as? UIColor
let highlightsIntensity = (value["highlightsIntensity"] as? NSNumber)?.floatValue
if let shadowsColor, let shadowsIntensity, shadowsColor.alpha > 0.0 {
let shadowsTintValue = TintValue(color: shadowsColor, intensity: shadowsIntensity / 100.0)
toolValues[.shadowsTint] = shadowsTintValue
}
if let highlightsColor, let highlightsIntensity, highlightsColor.alpha > 0.0 {
let highlightsTintValue = TintValue(color: highlightsColor, intensity: highlightsIntensity / 100.0)
toolValues[.highlightsTint] = highlightsTintValue
}
}
if let value = legacyAdjustments.curvesValue() {
func readValue(_ key: String) -> CurvesValue.CurveValue? {
if let values = value[key] as? [AnyHashable: Any] {
if let blacks = values["blacks"] as? NSNumber, let shadows = values["shadows"] as? NSNumber, let midtones = values["midtones"] as? NSNumber, let highlights = values["highlights"] as? NSNumber, let whites = values["whites"] as? NSNumber {
return CurvesValue.CurveValue(
blacks: blacks.floatValue / 100.0,
shadows: shadows.floatValue / 100.0,
midtones: midtones.floatValue / 100.0,
highlights: highlights.floatValue / 100.0,
whites: whites.floatValue / 100.0
)
}
}
return nil
}
if let all = readValue("luminance"), let red = readValue("red"), let green = readValue("green"), let blue = readValue("blue") {
toolValues[.curves] = CurvesValue(
all: all,
red: red,
green: green,
blue: blue
)
}
}
var qualityPreset = MediaQualityPreset(preset: legacyAdjustments.preset)
if qualityPreset == .compressedDefault {
qualityPreset = defaultPreset
}
self.init(
peerId: EnginePeer.Id(0),
originalDimensions: PixelDimensions(legacyAdjustments.originalSize),
cropOffset: .zero,
cropRect: legacyAdjustments.cropRect,
cropScale: 1.0,
cropRotation: legacyAdjustments.cropRotation,
cropMirroring: legacyAdjustments.cropMirrored,
cropOrientation: legacyAdjustments.cropOrientation.cropOrientation,
gradientColors: nil,
videoTrimRange: videoTrimRange,
videoIsMuted: legacyAdjustments.sendAsGif,
videoIsFullHd: true,
videoIsMirrored: false,
additionalVideoPath: nil,
additionalVideoPosition: nil,
additionalVideoScale: nil,
additionalVideoRotation: nil,
additionalVideoPositionChanges: [],
drawing: drawing,
entities: entities,
toolValues: toolValues,
audioTrack: nil,
audioTrackTrimRange: nil,
audioTrackOffset: nil,
audioTrackVolume: nil,
audioTrackSamples: nil,
qualityPreset: qualityPreset
)
}
}