Skip to content

Commit

Permalink
Merge pull request #43 from rewardStyle/fix/stop-export-timer
Browse files Browse the repository at this point in the history
fix: don't access Timer userInfo if timer is invalidated
  • Loading branch information
MikkoKuivanenRS committed Apr 11, 2024
2 parents 74ca8d2 + 7393533 commit 8f3b8ef
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 96 deletions.
3 changes: 3 additions & 0 deletions Source/Configuration/YPImagePickerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ public struct YPConfigVideo {
/// The video's pixel size will be automatically restricted to fit within this amount (keeping the aspect ratio).
/// NOTE: Additionally, if this value is set, the resulting resolution values will be rounded down to the nearest even numbers.
public var maxVideoResolution: Double?

/// The maximum size of video thumbnail
public var maxVideoThumbnailSize: CGSize?
}

/// Encapsulates gallery specific settings.
Expand Down
3 changes: 3 additions & 0 deletions Source/Helpers/Extensions/AVFoundation+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ internal func thumbnailFromVideoPath(_ path: URL) -> UIImage {
let asset = AVURLAsset(url: path, options: nil)
let gen = AVAssetImageGenerator(asset: asset)
gen.appliesPreferredTrackTransform = true
if let maximumSize = YPConfig.video.maxVideoThumbnailSize {
gen.maximumSize = maximumSize
}
let time = CMTimeMakeWithSeconds(0.0, preferredTimescale: 600)
var actualTime = CMTimeMake(value: 0, timescale: 0)
let image: CGImage
Expand Down
145 changes: 49 additions & 96 deletions Source/Pages/Gallery/LibraryMediaManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ open class LibraryMediaManager {
internal var previousPreheatRect: CGRect = .zero
internal var imageManager: PHCachingImageManager?
internal var exportTimer: Timer?
internal var exportTimers: [Timer] = []
internal var currentExportSessions: [ExportData] = []
private let currentExportSessionsAccessQueue = DispatchQueue(label: "LibraryMediaManagerExportArrayAccessQueue")
internal var currentExportSessions: [ExportData] = [] {
didSet {
if exportTimer == nil, !currentExportSessions.isEmpty {
startExportTimer()
}
}
}

internal var progress: Float {
guard !currentExportSessions.isEmpty else { return 0 }
return currentExportSessions.map { $0.session }.filter { $0.status != .cancelled }.map { $0.progress }.reduce(0.0, +) / Float(currentExportSessions.map { $0.session }.filter { $0.status != .cancelled }.count)
}

/// If true then library has items to show. If false the user didn't allow any item to show in picker library.
internal var hasResultItems: Bool {
Expand All @@ -42,6 +52,37 @@ open class LibraryMediaManager {

public init() {}

private func startExportTimer() {
DispatchQueue.main.async {
self.exportTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
guard let self else { return }
NotificationCenter.default.post(name: .LibraryMediaManagerExportProgressUpdate, object: self, userInfo: ["progress": progress])
if let v {
v.updateProgress(progress)
}

if !self.currentExportSessions.isEmpty, self.currentExportSessions.map({ $0.session }).allSatisfy( { $0.status == .completed || $0.status == .cancelled }) {
self.clearExportSessions()
}
})
}
}

public func clearExportSessions() {
DispatchQueue.main.async {
self.exportTimer?.invalidate()
self.exportTimer = nil
self.v?.updateProgress(0)
}
currentExportSessions = []
}

public func cancelExports() {
currentExportSessions.forEach { exportData in
exportData.session.cancelExport()
}
}

public func initialize() {
imageManager = PHCachingImageManager()
resetCachedAssets()
Expand Down Expand Up @@ -116,14 +157,11 @@ open class LibraryMediaManager {
.appendingUniquePathComponent(pathExtension: YPConfig.video.fileType.fileExtension)
let exportSession = asset
.export(to: fileURL,
presetName: presetName) { [weak self] session in
presetName: presetName) { session in
DispatchQueue.main.async {
switch session.status {
case .completed:
if let url = session.outputURL {
if let index = self?.currentExportSessions.firstIndex(where: { $0.session == session }) {
self?.removeExportData(at: index)
}
callback(url)
} else {
ypLog("LibraryMediaManager -> Don't have URL.")
Expand All @@ -139,15 +177,6 @@ open class LibraryMediaManager {
}
}

// 6. Exporting
DispatchQueue.main.async {
self.exportTimer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(self.onTickExportTimer),
userInfo: exportSession,
repeats: true)
}

if let s = exportSession {
self.appendExportData(ExportData(localIdentifier: videoAsset.localIdentifier, session: s))
}
Expand All @@ -159,10 +188,6 @@ open class LibraryMediaManager {
}

open func fetchVideoUrlAndCrop(for videoAsset: PHAsset, cropRect: CGRect, timeRange: CMTimeRange = CMTimeRange(start: CMTime.zero, end: CMTime.zero), shouldMute: Bool = false, compressionTypeOverride: String? = nil, processingFailedRetryCount: Int = 0, callback: @escaping (_ videoURL: URL?) -> Void) {
if currentExportSessions.contains(where: { $0.localIdentifier == videoAsset.localIdentifier }) {
cancelExport(for: videoAsset.localIdentifier)
}

let videosOptions = PHVideoRequestOptions()
videosOptions.isNetworkAccessAllowed = true
videosOptions.deliveryMode = .highQualityFormat
Expand Down Expand Up @@ -276,13 +301,9 @@ open class LibraryMediaManager {
switch session.status {
case .completed:
if let url = session.outputURL {
if let index = self?.currentExportSessions.firstIndex(where: { $0.session == session }) {
self?.removeExportData(at: index)
}
callback(url)
} else {
ypLog("LibraryMediaManager -> Don't have URL.")
self?.stopExportTimer(for: session)
callback(nil)
}
case .failed:
Expand All @@ -292,7 +313,6 @@ open class LibraryMediaManager {
// Try one more time to process with the export settings on the YPConfig.
let compressionOverride = YPConfig.video.compression
ypLog("LibraryMediaManager -> Export of the video failed. Reason: \(String(describing: session.error))\n--- Retrying with compression type \(compressionOverride)")
self.stopExportTimer(for: session)
if retryCount > 1 {
callback(nil)
} else {
Expand All @@ -303,23 +323,11 @@ open class LibraryMediaManager {
}
default:
ypLog("LibraryMediaManager -> Export session completed with \(session.status) status. Not handling.")
self?.stopExportTimer(for: session)
callback(nil)
}
}
}

// 6. Exporting
DispatchQueue.main.async {
self.exportTimers.append(
Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(self.onTickExportTimer),
userInfo: exportSession,
repeats: true)
)
}

if let s = exportSession {
self.appendExportData(ExportData(localIdentifier: videoAsset.localIdentifier, session: s))
}
Expand All @@ -328,32 +336,6 @@ open class LibraryMediaManager {
}
}
}

@objc func onTickExportTimer(sender: Timer) {
if let exportSession = sender.userInfo as? AVAssetExportSession {
if let v = v {
if exportSession.progress > 0 {
v.updateProgress(exportSession.progress)
}
} else {
// dispatch notification
let progress = [
"session": exportSession,
"progress": exportSession.progress
] as [String : Any]
NotificationCenter.default.post(name: .LibraryMediaManagerExportProgressUpdate, object: self, userInfo: progress)
}

if exportSession.progress > 0.99 {
stopExportTimer(timer: sender)
let progress = [
"session": exportSession,
"progress": 1.0
] as [String : Any]
NotificationCenter.default.post(name: .LibraryMediaManagerExportProgressUpdate, object: self, userInfo: progress)
}
}
}

private func getConstrainedSize(size: CGSize) -> CGSize {
if let maxVideoResolution = YPConfig.video.maxVideoResolution, size.width * size.height > maxVideoResolution {
Expand All @@ -365,32 +347,6 @@ open class LibraryMediaManager {
return size
}

private func stopExportTimer() {
exportTimer?.invalidate()
exportTimer = nil

// also reset progress view if one is available
v?.updateProgress(0)
}

private func stopExportTimer(timer: Timer) {
timer.invalidate()
if let index = exportTimers.firstIndex(of: timer) {
exportTimers.remove(at: index)
}
// also reset progress view if one is available
v?.updateProgress(0)
}

private func stopExportTimer(for session: AVAssetExportSession) {
let timer = exportTimers.first { timer in
timer.userInfo as? AVAssetExportSession == session
}
if let timer {
stopExportTimer(timer: timer)
}
}

func forceCancelExporting() {
currentExportSessions.forEach {
$0.session.cancelExport()
Expand Down Expand Up @@ -444,17 +400,14 @@ open class LibraryMediaManager {
return imageAsset
}

private func cancelExport(for localIdentifier: String) {
guard let index = currentExportSessions.firstIndex(where: { $0.localIdentifier == localIdentifier }) else { return }
let exportData = currentExportSessions[index]
stopExportTimer(for: exportData.session)
exportData.session.cancelExport()
removeExportData(at: index)
}

private func appendExportData(_ exportData: ExportData) {
currentExportSessionsAccessQueue.async { [weak self] in
guard let self else { return }
if let index = currentExportSessions.firstIndex(where: { $0.localIdentifier == exportData.localIdentifier }) {
let session = currentExportSessions[index].session
session.cancelExport()
currentExportSessions.remove(at: index)
}
currentExportSessions.append(exportData)
}
}
Expand Down

0 comments on commit 8f3b8ef

Please sign in to comment.