// // VideoHandler.swift // Pocket Poster // // Created by lemin on 6/8/25. // import Foundation import AVFoundation import UIKit import CoreTransferable 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) } } struct Movie: Transferable { let url: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .movie) { movie in SentTransferredFile(movie.url) } importing: { received in let videoFolder = SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory) if !FileManager.default.fileExists(atPath: videoFolder.path()) { try? FileManager.default.createDirectory(at: videoFolder, withIntermediateDirectories: true) } let copy = videoFolder.appending(path: "\(UUID()).mp4") if FileManager.default.fileExists(atPath: copy.path()) { try FileManager.default.removeItem(at: copy) } try FileManager.default.copyItem(at: received.file, to: copy) return Self.init(url: copy) } } } enum LoadState { case unknown, loading, loaded(Movie), failed } struct LoadInfo: Identifiable, Equatable { static func == (lhs: LoadInfo, rhs: LoadInfo) -> Bool { lhs.id == rhs.id } var id = UUID() var autoReverses: Bool = false var loadState: LoadState } class VideoHandler { static let MaxDurationSecs = 12.0 static func isVideoTooLong(at url: URL) -> Bool { let asset = AVAsset(url: url) let duration = asset.duration let durationTime = CMTimeGetSeconds(duration) return durationTime > MaxDurationSecs } 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 preferredTransform = track.preferredTransform let size = track.naturalSize.applying(preferredTransform) let width = Int(abs(size.width)) let height = Int(abs(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 = """ """ // 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 } }