diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift index 57d2e48466..8dbe1fb517 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift @@ -1506,24 +1506,18 @@ extension RunnerTests { if let requestedFps = command.fps, (requestedFps < minRecordingFps || requestedFps > maxRecordingFps) { return Response(ok: false, error: ErrorPayload(message: "recordStart fps must be between \(minRecordingFps) and \(maxRecordingFps)")) } - if let requestedMaxSize = command.maxSize, requestedMaxSize < 1 { - return Response(ok: false, error: ErrorPayload(message: "recordStart maxSize must be a positive integer")) - } do { let resolvedOutPath = resolveRecordingOutPath(requestedOutPath) let fpsLabel = command.fps.map(String.init) ?? String(RunnerTests.defaultRecordingFps) - let maxSizeLabel = command.maxSize.map(String.init) ?? "native" NSLog( - "AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@ maxSize=%@", + "AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@", requestedOutPath, resolvedOutPath, - fpsLabel, - maxSizeLabel + fpsLabel ) let recorder = ScreenRecorder( outputPath: resolvedOutPath, - fps: command.fps.map { Int32($0) }, - maxSize: command.maxSize + fps: command.fps.map { Int32($0) } ) try recorder.start { [weak self] in return self?.captureRunnerFrame() diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift index b9ea5b4097..ac3ce06ee1 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift @@ -136,7 +136,6 @@ struct Command: Codable { let gesturePlan: RunnerGesturePlan? let outPath: String? let fps: Int? - let maxSize: Int? let interactiveOnly: Bool? let preferredBackend: String? let depth: Int? diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift index 668489454a..40b96ec935 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift @@ -7,7 +7,6 @@ extension RunnerTests { final class ScreenRecorder { private let outputPath: String private let fps: Int32? - private let maxSize: Int? private var effectiveFps: Int32 { max(1, fps ?? RunnerTests.defaultRecordingFps) } @@ -26,10 +25,9 @@ extension RunnerTests { private var startedSession = false private var startError: Error? - init(outputPath: String, fps: Int32?, maxSize: Int?) { + init(outputPath: String, fps: Int32?) { self.outputPath = outputPath self.fps = fps - self.maxSize = maxSize } func start(captureFrame: @escaping () -> RunnerImage?) throws { @@ -50,7 +48,7 @@ extension RunnerTests { while Date() < bootstrapDeadline { if let image = captureFrame(), let cgImage = runnerCGImage(from: image) { bootstrapImage = image - dimensions = scaledDimensions(width: cgImage.width, height: cgImage.height) + dimensions = CGSize(width: cgImage.width, height: cgImage.height) break } Thread.sleep(forTimeInterval: 0.05) @@ -261,23 +259,5 @@ extension RunnerTests { return pixelBuffer } - private func scaledDimensions(width: Int, height: Int) -> CGSize { - guard let maxSize, maxSize > 0 else { - return CGSize(width: width, height: height) - } - let longest = max(width, height) - guard longest > maxSize else { - return CGSize(width: width, height: height) - } - let scale = Double(maxSize) / Double(longest) - return CGSize( - width: scaledEvenDimension(width, scale: scale), - height: scaledEvenDimension(height, scale: scale) - ) - } - - private func scaledEvenDimension(_ value: Int, scale: Double) -> Int { - max(2, Int((Double(value) * scale / 2.0).rounded()) * 2) - } } } diff --git a/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift b/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift deleted file mode 100644 index 1557617bd9..0000000000 --- a/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +++ /dev/null @@ -1,225 +0,0 @@ -import AVFoundation -import Foundation - -enum ResizeError: Error, CustomStringConvertible { - case invalidArgs(String) - case missingVideoTrack - case exportFailed(String) - - var description: String { - switch self { - case .invalidArgs(let message): - return message - case .missingVideoTrack: - return "Input video does not contain a video track." - case .exportFailed(let message): - return message - } - } -} - -do { - try run() -} catch { - fputs("recording-resize: \(error)\n", stderr) - exit(1) -} - -func run() throws { - let arguments = Array(CommandLine.arguments.dropFirst()) - let parsedArgs = try parseArguments(arguments) - let inputURL = URL(fileURLWithPath: parsedArgs.inputPath) - let outputURL = URL(fileURLWithPath: parsedArgs.outputPath) - - if FileManager.default.fileExists(atPath: outputURL.path) { - try FileManager.default.removeItem(at: outputURL) - } - - let asset = AVURLAsset(url: inputURL) - guard let sourceVideoTrack = asset.tracks(withMediaType: .video).first else { - throw ResizeError.missingVideoTrack - } - - let sourceRenderSize = resolvedRenderSize(for: sourceVideoTrack) - if max(sourceRenderSize.width, sourceRenderSize.height) <= CGFloat(parsedArgs.maxSize) { - try FileManager.default.copyItem(at: inputURL, to: outputURL) - return - } - - let renderSize = scaledRenderSize(sourceRenderSize, maxSize: parsedArgs.maxSize) - let composition = AVMutableComposition() - let fullRange = CMTimeRange(start: .zero, duration: asset.duration) - - guard let compositionVideoTrack = composition.addMutableTrack( - withMediaType: .video, - preferredTrackID: kCMPersistentTrackID_Invalid - ) else { - throw ResizeError.exportFailed("Failed to create composition video track.") - } - try compositionVideoTrack.insertTimeRange(fullRange, of: sourceVideoTrack, at: .zero) - - if let sourceAudioTrack = asset.tracks(withMediaType: .audio).first, - let compositionAudioTrack = composition.addMutableTrack( - withMediaType: .audio, - preferredTrackID: kCMPersistentTrackID_Invalid - ) { - try? compositionAudioTrack.insertTimeRange(fullRange, of: sourceAudioTrack, at: .zero) - } - - let scale = renderSize.width / sourceRenderSize.width - let videoComposition = AVMutableVideoComposition() - videoComposition.renderSize = renderSize - videoComposition.frameDuration = resolvedFrameDuration(for: sourceVideoTrack) - - let instruction = AVMutableVideoCompositionInstruction() - instruction.timeRange = fullRange - let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack) - // Scale the full preferred transform (including translation) to match the smaller render canvas. - let scaledTransform = scaledPreferredTransform(sourceVideoTrack.preferredTransform, scale: scale) - layerInstruction.setTransform(scaledTransform, at: .zero) - instruction.layerInstructions = [layerInstruction] - videoComposition.instructions = [instruction] - - let presetName = exportPresetName(for: parsedArgs.exportQuality, compatibleWith: composition) - guard let exporter = AVAssetExportSession(asset: composition, presetName: presetName) else { - throw ResizeError.exportFailed("Failed to create export session.") - } - - exporter.outputURL = outputURL - exporter.outputFileType = .mp4 - exporter.videoComposition = videoComposition - exporter.shouldOptimizeForNetworkUse = true - - let semaphore = DispatchSemaphore(value: 0) - exporter.exportAsynchronously { - semaphore.signal() - } - if semaphore.wait(timeout: .now() + 120) == .timedOut { - exporter.cancelExport() - throw ResizeError.exportFailed("Resize export timed out.") - } - - if exporter.status != .completed { - throw ResizeError.exportFailed(exporter.error?.localizedDescription ?? "Resize export failed.") - } -} - -enum ExportQuality: String { - case medium - case high -} - -func parseArguments( - _ arguments: [String] -) throws -> (inputPath: String, outputPath: String, maxSize: Int, exportQuality: ExportQuality) { - var inputPath: String? - var outputPath: String? - var maxSize: Int? - // Export quality defaults to medium so re-encoded recordings stay fast by default. - // Pass --quality high to opt into a slower highest-quality export. - var exportQuality: ExportQuality = .medium - var index = 0 - - while index < arguments.count { - let argument = arguments[index] - let nextIndex = index + 1 - switch argument { - case "--input": - guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--input requires a value") } - inputPath = arguments[nextIndex] - index += 2 - case "--output": - guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--output requires a value") } - outputPath = arguments[nextIndex] - index += 2 - case "--max-size": - guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--max-size requires a value") } - guard let parsed = Int(arguments[nextIndex]), parsed >= 1 else { - throw ResizeError.invalidArgs("--max-size must be a positive integer") - } - maxSize = parsed - index += 2 - case "--quality": - guard nextIndex < arguments.count else { - throw ResizeError.invalidArgs("--quality requires a value") - } - guard let parsed = ExportQuality(rawValue: arguments[nextIndex]) else { - throw ResizeError.invalidArgs("--quality must be one of: medium, high") - } - exportQuality = parsed - index += 2 - default: - throw ResizeError.invalidArgs("Unknown argument: \(argument)") - } - } - - guard let inputPath, let outputPath, let maxSize else { - throw ResizeError.invalidArgs( - "Usage: recording-resize.swift --input