Skip to content
RPRichard PruittTech & Cyber
Navigation
repo://simon-says-duo/MyApp/VisualCueExport.swift

MyApp/VisualCueExport.swift

2.9 KB · 53 lines · swift · Download from the repository page

MyApp/VisualCueExport.swift
import AVFoundationimport UIKitimport CoreImage @MainActorenum VisualCueExport {    static func export(raw: URL, cue: VisualCue, timing: CueTiming, to destination: URL) async throws {        let asset = AVURLAsset(url: raw)        let duration = try await asset.load(.duration)        guard timing.start.isFinite, timing.duration.isFinite, timing.start >= 0, timing.duration > 0,              timing.start + timing.duration <= duration.seconds,              !(try await asset.loadTracks(withMediaType: .video)).isEmpty else {            throw PartyError.message("Visual cue timing or video is unavailable.")        }        // Rasterize only the neutral badge once; AVFoundation provides upright source frames.        let format = UIGraphicsImageRendererFormat()        format.scale = 1        let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512), format: format)        let badge = renderer.image { _ in            UIColor.black.withAlphaComponent(0.85).setFill()            UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 512, height: 512), cornerRadius: 76).fill()            let attributes: [NSAttributedString.Key: Any] = [                .font: UIFont.systemFont(ofSize: 384, weight: .black), .foregroundColor: UIColor.white            ]            let text = cue.letter as NSString            let size = text.size(withAttributes: attributes)            text.draw(at: CGPoint(x: (512 - size.width) / 2, y: (512 - size.height) / 2), withAttributes: attributes)        }        guard let cgImage = badge.cgImage else { throw PartyError.message("Could not render visual cue.") }        let overlay = CIImage(cgImage: cgImage)        let start = timing.start        let end = timing.start + timing.duration        let composition = try await AVVideoComposition(applyingFiltersTo: asset) { parameters in            let source = parameters.sourceImage            let time = parameters.compositionTime.seconds            guard time >= start && time < end else {                return AVCIImageFilteringResult(resultImage: source, ciContext: nil)            }            let bounds = source.extent            let side = min(bounds.width, bounds.height) * 0.44            let scaled = overlay.transformed(by: CGAffineTransform(scaleX: side / 512, y: side / 512))            let positioned = scaled.transformed(by: CGAffineTransform(translationX: bounds.midX - side / 2, y: bounds.midY - side / 2))            return AVCIImageFilteringResult(resultImage: positioned.composited(over: source).cropped(to: bounds), ciContext: nil)        }        guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {            throw PartyError.message("Visual cue export is unavailable.")        }        exporter.videoComposition = composition        // Preserve microphone audio, including the original action; never announce the letter.        try await exporter.export(to: destination, as: .mp4)    }}