Generate video thumbnails

This commit is contained in:
Christoph Hagen
2025-01-25 22:14:31 +01:00
parent 200fdc813d
commit 06b4c1ed76
10 changed files with 254 additions and 54 deletions

View File

@ -2,6 +2,7 @@ import Foundation
import AppKit
import SDWebImageAVIFCoder
import SDWebImageWebPCoder
import AVFoundation
final class ImageGenerator {
@ -162,10 +163,9 @@ final class ImageGenerator {
}
private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
return Data()
// let newImage = NSImage(size: image.size)
// newImage.addRepresentation(image)
// return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
let newImage = NSImage(size: image.size)
newImage.addRepresentation(image)
return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
}
private func createWebp(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
@ -173,4 +173,63 @@ final class ImageGenerator {
newImage.addRepresentation(image)
return SDImageWebPCoder.shared.encodedData(with: newImage, format: .webP, options: [.encodeCompressionQuality: quality])
}
// MARK: Video thumbnails
@discardableResult
func createVideoThumbnail(for videoId: String) async -> Bool {
guard let image = await storage.with(file: videoId, perform: generateThumbnail) else {
print("Failed to generate thumbnail image for video \(videoId)")
return false
}
let scaled = create(image: image, width: image.size.width, height: image.size.height)
guard let data = scaled.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)]) else {
print("Failed to get thumbnail jpg data of video \(videoId)")
return false
}
if !storage.save(thumbnail: data, for: videoId) {
print("Failed to save thumbnail of video \(videoId)")
}
print("Generated video thumbnail for \(videoId)")
return true
}
private func generateThumbnail(for url: URL) async -> NSImage? {
let time = CMTime(seconds: 1, preferredTimescale: 600)
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true // Correct for orientation
return await withCheckedContinuation { continuation in
imageGenerator.generateCGImageAsynchronously(for: time) { cgImage, _, error in
if let error {
print("Error generating thumbnail for \(url.path()): \(error.localizedDescription)")
}
if let cgImage {
let image = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
continuation.resume(returning: image)
} else {
continuation.resume(returning: nil)
}
}
}
}
func getVideoDuration(for videoId: String) async -> TimeInterval? {
guard let duration = await storage.with(file: videoId, perform: getVideoDuration) else {
print("Failed to determine duration for video \(videoId)")
return nil
}
return duration
}
private func getVideoDuration(url: URL) async -> TimeInterval? {
let asset = AVURLAsset(url: url)
do {
let duration = try await asset.load(.duration)
return CMTimeGetSeconds(duration)
} catch {
print("ImageGenerator: Failed to determine video duration: \(error.localizedDescription)")
return nil
}
}
}