Skip to content

Commit

Permalink
Merge pull request #55 from TheAcharya/progress-refinements
Browse files Browse the repository at this point in the history
Hotfix
  • Loading branch information
orchetect authored Nov 1, 2023
2 parents 158c909 + 61f8fec commit ff57364
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 87 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# MarkersExtractor Change Log

## [0.2.3](https://github.com/TheAcharya/MarkersExtractor/releases/tag/0.2.3) (2023-10-31)

### New Features

- Added `--no-progress` CLI flag to suppress progress output to console (#31)

### Bug Fixes

- Resolved issue where CLI was not outputting progress to the console in a release build (#31)
- Performance and reliability improvements for thumbnail image generation (#49)

## [0.2.2](https://github.com/TheAcharya/MarkersExtractor/releases/tag/0.2.2) (2023-10-31)

### Refinements
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@

### Pre-compiled Binary (Recommended)

Download the latest release of the CLI universal binary [here](https://github.com/TheAcharya/MarkersExtractor/releases/download/0.2.2/markers-extractor-cli-0.2.2.zip).
Download the latest release of the CLI universal binary [here](https://github.com/TheAcharya/MarkersExtractor/releases/download/0.2.3/markers-extractor-cli-0.2.3.zip).

### From Source

```shell
VERSION=0.2.2 # replace this with the git tag of the version you need
VERSION=0.2.3 # replace this with the git tag of the version you need
git clone https://github.com/TheAcharya/MarkersExtractor.git
cd MarkersExtractor
git checkout "tags/$VERSION"
Expand All @@ -71,7 +71,7 @@ Once the build has finished, the `markers-extractor-cli` executable will be loca

## Usage

### CLI 0.2.2
### CLI 0.2.3

```plain
$ markers-extractor-cli --help
Expand Down Expand Up @@ -145,6 +145,7 @@ OPTIONS:
--log-level <trace, debug, info, notice, warning, error, critical>
Log level. (default: info)
--quiet Disable log.
--no-progress Disable progress logging.
--no-media Bypass media. No thumbnails will be generated.
--media-search-path <media-search-path>
Media search path. This argument can be supplied more
Expand Down
4 changes: 2 additions & 2 deletions Sources/MarkersExtractor/Export/ExportProfile Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class AnimatedImagesWriter: NSObject, ImageWriterProtocol {
imageFilter: { [weak self] inputImage in
if let self, let label = descriptor.label {
var labeler = ImageLabeler(labelProperties: self.imageLabelProperties, logger: self.logger)
return await labeler.labelImage(image: inputImage, text: label)
return labeler.labelImage(image: inputImage, text: label)
} else {
return inputImage
}
Expand Down Expand Up @@ -170,7 +170,7 @@ class ImagesWriter: NSObject, ImageWriterProtocol {
imageFilter: { inputImage, label in
if let label {
var labeler = ImageLabeler(labelProperties: imageLabelProperties, logger: logger)
return await labeler.labelImage(image: inputImage, text: label)
return labeler.labelImage(image: inputImage, text: label)
} else {
return inputImage
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/MarkersExtractor/Export/Support/ImageLabeler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ImageLabeler {
properties = labelProperties
}

mutating func labelImage(image: CGImage, text: String) async -> CGImage {
mutating func labelImage(image: CGImage, text: String) -> CGImage {
guard let context = initImageContext(for: image) else {
logger.warning("Failed to initialize new image context. Bypassing original image.")
return image
Expand All @@ -34,7 +34,7 @@ struct ImageLabeler {
in: CGRect(x: 0, y: 0, width: image.width, height: image.height)
)

await drawText(text: text, context: context, textRect: textRect)
drawText(text: text, context: context, textRect: textRect)

guard let newImage = context.makeImage() else {
logger.warning("Failed to create labeled image. Bypassing original image.")
Expand Down Expand Up @@ -72,7 +72,7 @@ struct ImageLabeler {
)
}

private mutating func drawText(text: String, context: CGContext, textRect: CGRect) async {
private mutating func drawText(text: String, context: CGContext, textRect: CGRect) {
let paragraphStyle = NSMutableParagraphStyle()

switch properties.alignHorizontal {
Expand All @@ -90,7 +90,7 @@ struct ImageLabeler {
.paragraphStyle: paragraphStyle
]

let fontSize = await calcFontSize(
let fontSize = calcFontSize(
for: text,
attributes: stringAttributes,
restraint: textRect.size
Expand Down Expand Up @@ -207,7 +207,7 @@ struct ImageLabeler {
for string: String,
attributes: [NSAttributedString.Key: Any],
restraint: CGSize
) async -> CGFloat {
) -> CGFloat {
let sizeHash = [
string,
String(Int(restraint.height)),
Expand Down
154 changes: 113 additions & 41 deletions Sources/MarkersExtractor/Extensions/AVAssetImageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,41 @@ extension AVAssetImageGenerator {
progress?.completedUnitCount = Int64(exactly: count) ?? 0
}

for descriptor in descriptors {
let requestedTime = descriptor.timecode.cmTimeValue
let result = try await imageCompat(at: requestedTime)

completedCount.increment()

await completionHandler(
descriptor,
.success(
CompletionHandlerResult(
image: result.image,
requestedTime: requestedTime,
actualTime: result.actualTime,
completedCount: completedCount.count,
totalCount: totalCount.count,
isFinished: completedCount.count == totalCount.count,
isFinishedIgnoreImage: false
await withThrowingTaskGroup(of: Void.self) { [weak self] taskGroup in
for descriptor in descriptors {
taskGroup.addTask { [weak self] in
guard let self else { return }

let requestedTime = descriptor.timecode.cmTimeValue

let result = try await self.imageCompat(at: requestedTime)
let hasImage = result.image != nil
let imageForHandlerResult = result.image ?? CGImage.empty!

if hasImage {
completedCount.increment()
} else {
totalCount.decrement()
}

let isFinished = completedCount.count == totalCount.count

await completionHandler(
descriptor,
.success(
CompletionHandlerResult(
image: imageForHandlerResult,
requestedTime: requestedTime,
actualTime: result.actualTime,
completedCount: completedCount.count,
totalCount: totalCount.count,
isFinished: isFinished,
isFinishedIgnoreImage: isFinished && !hasImage
)
)
)
)
)
}
}
}
}

Expand All @@ -67,53 +82,110 @@ extension AVAssetImageGenerator {
progress?.completedUnitCount = Int64(exactly: count) ?? 0
}

for requestedTime in times {
let result = try await imageCompat(at: requestedTime)

completedCount.increment()

completionHandler(
requestedTime,
.success(
CompletionHandlerResult(
image: result.image,
requestedTime: requestedTime,
actualTime: result.actualTime,
completedCount: completedCount.count,
totalCount: totalCount.count,
isFinished: completedCount.count == totalCount.count,
isFinishedIgnoreImage: false
await withThrowingTaskGroup(of: Void.self) { [weak self] taskGroup in
for requestedTime in times {
taskGroup.addTask { [weak self] in
guard let self else { return }

let result = try await self.imageCompat(at: requestedTime)
let hasImage = result.image != nil
let imageForHandlerResult = result.image ?? CGImage.empty!

if hasImage {
completedCount.increment()
} else {
totalCount.decrement()
}

let isFinished = completedCount.count == totalCount.count

completionHandler(
requestedTime,
.success(
CompletionHandlerResult(
image: imageForHandlerResult,
requestedTime: requestedTime,
actualTime: result.actualTime,
completedCount: completedCount.count,
totalCount: totalCount.count,
isFinished: isFinished,
isFinishedIgnoreImage: isFinished && !hasImage
)
)
)
)
)
}
}
}
}

/// Backward-compatible implementation of Apple's `image(at time: CMTime)`.
func imageCompat(at time: CMTime) async throws -> (image: CGImage, actualTime: CMTime) {
func imageCompat(at time: CMTime) async throws -> (image: CGImage?, actualTime: CMTime) {
if #available(macOS 13.0, *) {
return try await image(at: time)
}

var resultImage: CGImage? = nil
var resultError: Error? = nil
var resultActualTime: CMTime? = nil
var isNoFrame: Bool = false

let group = DispatchGroup()
group.enter()

let nsValue = NSValue(time: time)
generateCGImagesAsynchronously(forTimes: [nsValue])
{ /*requestedTime*/ _, image, actualTime, /*result*/ _, error in
{ /*requestedTime*/ _, image, actualTime, result, error in
defer { group.leave() }

resultImage = image
resultError = error
resultActualTime = actualTime
group.leave()

switch result {
case .succeeded:
break

case .failed:
guard let error else {
resultError = MarkersExtractorError.extraction(.image(.generic(
"Image generator failed but no additional error information is available."
)))
return
}

// Handle blank frames
switch error {
case let avError as AVError:
switch avError.code {
case .noImageAtTime:
// We ignore blank frames.
isNoFrame = true
case .decodeFailed:
// macOS 11 (still an issue in macOS 11.2) started throwing “decode failed”
// error for some frames in screen recordings.
// As a workaround, we ignore these as the GIF seems fine still.
isNoFrame = true
default:
break
}

default:
break
}
case .cancelled:
resultError = CancellationError()

@unknown default:
resultError = MarkersExtractorError.extraction(.image(.generic("Unhandled image result case.")))
}
}

return try await withCheckedThrowingContinuation { continuation in
group.notify(queue: .main) {
if let resultError {
if isNoFrame, let resultActualTime {
let tuple = (image: CGImage?.none, actualTime: resultActualTime)
continuation.resume(with: .success(tuple))
} else if let resultError {
continuation.resume(throwing: resultError)
} else if let resultImage, let resultActualTime {
let tuple = (image: resultImage, actualTime: resultActualTime)
Expand Down
Loading

0 comments on commit ff57364

Please sign in to comment.