//
// VideoHandler.swift
// Pocket Poster
//
// Created by lemin on 6/8/25.
//
import Foundation
import AVFoundation
import UIKit
extension CGImage {
var png: Data? {
guard let mutableData = CFDataCreateMutable(nil, 0),
let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) else { return nil }
CGImageDestinationAddImage(destination, self, nil)
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData as Data
}
var jpg: Data? {
let uiimg = UIImage(cgImage: self)
return uiimg.jpegData(compressionQuality: 0.7)
}
}
class VideoHandler {
static func createCaml(from url: URL, autoReverses: Bool) throws -> URL {
// copy the resources
let descrURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
try FileManager.default.createDirectory(at: descrURL, withIntermediateDirectories: false)
print("pre resource")
guard let rc = Bundle.main.url(forResource: "VideoCAML", withExtension: nil) else { throw URLError(.fileDoesNotExist) }
print("post resource")
try FileManager.default.copyItem(at: rc, to: descrURL.appendingPathComponent("videoCAML"))
print("post copy")
let coreAnimDir = descrURL.appending(path: "videoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Background-810w-1080h@2x~ipad.ca")
let assetsFolder = coreAnimDir.appendingPathComponent("assets", conformingTo: .directory)
if !FileManager.default.fileExists(atPath: assetsFolder.path()) {
try FileManager.default.createDirectory(at: assetsFolder, withIntermediateDirectories: true)
}
// load the video
let asset = AVURLAsset(url: url)
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
assetImageGenerator.appliesPreferredTrackTransform = true
assetImageGenerator.apertureMode = AVAssetImageGenerator.ApertureMode.encodedPixels
// create the caml
guard let track = asset.tracks(withMediaType: .video).first else {
print("Failed to get video track")
throw AVError(.decodeFailed)
}
let size = track.naturalSize.applying(track.preferredTransform)
let width = Int(size.width)
let height = Int(size.height)
let fps = track.nominalFrameRate
let duration = CMTimeGetSeconds(asset.duration)
let totalFrames = Int(fps * Float(duration))
let animDur = Double(totalFrames) / Double(fps)
var camlData = """
\n
"""
// go through every frame and add it to the caml
do {
let reader = try AVAssetReader(asset: asset)
let readerOutputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]
let readerOutput = AVAssetReaderTrackOutput(track: track, outputSettings: readerOutputSettings)
reader.add(readerOutput)
reader.startReading()
var frameCount = 0
while let sampleBuffer = readerOutput.copyNextSampleBuffer(),
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
let name = "\(frameCount).jpg"
UIApplication.shared.change(title: "Generating Video...", body: "Creating assets/\(name)...")
let ciImage = CIImage(cvPixelBuffer: imageBuffer)
let context = CIContext()
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
if let img = cgImage.jpg {
try img.write(to: assetsFolder.appendingPathComponent(name))
camlData += "\t\t\t\n"
frameCount += 1
}
}
}
} catch {
print("Error reading asset: \(error)")
}
UIApplication.shared.change(title: "Generating Video...", body: "Creating CAML data...")
camlData += """
"""
// write the caml
guard let caData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
try caData.write(to: coreAnimDir.appendingPathComponent("main.caml"))
// write the other info xml
camlData = """
assetManifest
assetManifest.caml
documentHeight
\(height)
documentResizesToView
documentWidth
\(width)
dynamicGuidesEnabled
geometryFlipped
guidesEnabled
interactiveMouseEventsEnabled
interactiveShowsCursor
interactiveTouchEventsEnabled
loopEnd
0.0
loopStart
0.0
loopingEnabled
multitouchDisablesMouse
multitouchEnabled
presentationMouseEventsEnabled
presentationShowsCursor
presentationTouchEventsEnabled
rootDocument
main.caml
savesWindowFrame
scalesToFitInPlayer
showsTouches
snappingEnabled
timelineMarkers
[(null)]
touchesColor
1 1 0 0.8
unitsInPixelsInPlayer
"""
guard let xmlData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
try xmlData.write(to: coreAnimDir.appendingPathComponent("index.xml", conformingTo: .xml))
return descrURL
}
}