this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add complete stream-video command implementation

- Implements video streaming from iOS simulators to stdout
- Supports BGRA format (working), H264/MJPEG (not working due to FBSimulatorControl)
- Includes comprehensive tests and example scripts
- Adds video stream queue for thread safety
- Updates test runner to include StreamVideoTests

Pedro e6d257b2 1614f5a6

+582 -2
+192
Sources/AXe/Commands/StreamVideo.swift
··· 1 + import ArgumentParser 2 + import Foundation 3 + import FBSimulatorControl 4 + @preconcurrency import FBControlCore 5 + 6 + struct StreamVideo: AsyncParsableCommand { 7 + static let configuration = CommandConfiguration( 8 + commandName: "stream-video", 9 + abstract: "Stream video from a simulator to stdout", 10 + discussion: """ 11 + Streams the simulator's screen as video data to stdout. Supports multiple formats including H264, MJPEG, and raw BGRA. 12 + 13 + CURRENT STATUS: 14 + - BGRA format: WORKING - Outputs raw uncompressed pixel data (4 bytes per pixel) 15 + - H264 format: NOT WORKING - No output due to FBSimulatorControl issues 16 + - MJPEG format: NOT WORKING - No output due to FBSimulatorControl issues 17 + - Minicap format: NOT WORKING - No output due to FBSimulatorControl issues 18 + 19 + For BGRA format, the output is raw pixel data that can be processed with tools like FFmpeg: 20 + axe stream-video --format bgra --udid <UDID> | ffmpeg -f rawvideo -pixel_format bgra -video_size 393x852 -i - output.mp4 21 + 22 + Known issues: 23 + - Apple removed support for streaming to stdout in xcrun simctl io recordVideo 24 + - FBSimulatorControl's video compression (H264/MJPEG) has unresolved issues (see facebook/idb#787, #841) 25 + 26 + Alternative approaches: 27 + - Use xcrun simctl io recordVideo to record to a file: xcrun simctl io <UDID> recordVideo output.mp4 28 + - Use screen recording software like OBS to capture the simulator window 29 + """ 30 + ) 31 + 32 + @Option(name: .customLong("udid"), help: "The UDID of the simulator.") 33 + var simulatorUDID: String 34 + 35 + @Option(help: "Video format: h264, mjpeg, bgra, minicap") 36 + var format: String = "h264" 37 + 38 + @Option(help: "Frames per second (omit for lazy/on-damage streaming)") 39 + var fps: Int? 40 + 41 + @Option(help: "Compression quality (0.0-1.0, default: 0.2)") 42 + var quality: Double = 0.2 43 + 44 + @Option(help: "Scale factor (0.0-1.0, default: 1.0)") 45 + var scale: Double = 1.0 46 + 47 + @Option(help: "Average bitrate in bits per second (H264 only)") 48 + var bitrate: Int? 49 + 50 + @Option(help: "Key frame interval in seconds (H264 only, default: 10.0)") 51 + var keyFrameInterval: Double = 10.0 52 + 53 + func validate() throws { 54 + // Validate format 55 + let validFormats = ["h264", "mjpeg", "bgra", "minicap"] 56 + guard validFormats.contains(format.lowercased()) else { 57 + throw ValidationError("Invalid format. Must be one of: \(validFormats.joined(separator: ", "))") 58 + } 59 + 60 + // Validate quality 61 + guard quality >= 0.0 && quality <= 1.0 else { 62 + throw ValidationError("Quality must be between 0.0 and 1.0") 63 + } 64 + 65 + // Validate scale 66 + guard scale > 0.0 && scale <= 1.0 else { 67 + throw ValidationError("Scale must be between 0.0 and 1.0") 68 + } 69 + 70 + // Validate FPS if provided 71 + if let fps = fps { 72 + guard fps > 0 && fps <= 60 else { 73 + throw ValidationError("FPS must be between 1 and 60") 74 + } 75 + } 76 + 77 + // Validate key frame interval 78 + guard keyFrameInterval > 0 else { 79 + throw ValidationError("Key frame interval must be greater than 0") 80 + } 81 + } 82 + 83 + func run() async throws { 84 + let logger = AxeLogger() 85 + try await setup(logger: logger) 86 + try await performGlobalSetup(logger: logger) 87 + 88 + // Get simulator set 89 + let simulatorSet = try await getSimulatorSet(deviceSetPath: nil, logger: logger, reporter: EmptyEventReporter.shared) 90 + 91 + // Find target simulator 92 + guard let targetSimulator = simulatorSet.allSimulators.first(where: { $0.udid == simulatorUDID }) else { 93 + throw CLIError(errorDescription: "Simulator with UDID \(simulatorUDID) not found.") 94 + } 95 + 96 + // Ensure simulator is booted 97 + guard targetSimulator.state == .booted else { 98 + throw CLIError(errorDescription: "Simulator \(simulatorUDID) is not booted. Current state: \(FBiOSTargetStateStringFromState(targetSimulator.state))") 99 + } 100 + 101 + // Create video stream configuration 102 + let encoding: FBVideoStreamEncoding = switch format.lowercased() { 103 + case "h264": .H264 104 + case "mjpeg": .MJPEG 105 + case "bgra": .BGRA 106 + case "minicap": .minicap 107 + default: .H264 108 + } 109 + 110 + let config = FBVideoStreamConfiguration( 111 + encoding: encoding, 112 + framesPerSecond: fps.map { NSNumber(value: $0) }, 113 + compressionQuality: NSNumber(value: quality), 114 + scaleFactor: NSNumber(value: scale), 115 + avgBitrate: (encoding == .H264 && bitrate != nil) ? NSNumber(value: bitrate!) : nil, 116 + keyFrameRate: encoding == .H264 ? NSNumber(value: keyFrameInterval) : nil 117 + ) 118 + 119 + // Log to stderr so it doesn't mix with video data on stdout 120 + FileHandle.standardError.write(Data("Starting video stream from simulator \(targetSimulator.udid)...\n".utf8)) 121 + FileHandle.standardError.write(Data("Format: \(format), FPS: \(fps.map { String($0) } ?? "lazy"), Quality: \(quality), Scale: \(scale)\n".utf8)) 122 + if format.lowercased() != "bgra" { 123 + FileHandle.standardError.write(Data("\nWARNING: Only BGRA format currently works. H264/MJPEG formats have known issues.\n".utf8)) 124 + FileHandle.standardError.write(Data("Consider using --format bgra or 'xcrun simctl io \(targetSimulator.udid) recordVideo output.mp4'\n".utf8)) 125 + } 126 + FileHandle.standardError.write(Data("Press Ctrl+C to stop streaming\n".utf8)) 127 + 128 + do { 129 + // Note: We don't need to explicitly connect to framebuffer because 130 + // targetSimulator.createStream will do it internally 131 + 132 + // Create consumer that writes to stdout 133 + let stdoutConsumer = FBFileWriter.syncWriter(withFileDescriptor: STDOUT_FILENO, closeOnEndOfFile: false) 134 + 135 + // Create video stream using the simulator's createStream method 136 + let videoStreamFuture = targetSimulator.createStream(with: config) 137 + let videoStream = try await FutureBridge.value(videoStreamFuture) 138 + 139 + // Start streaming to stdout 140 + let startFuture = videoStream.startStreaming(stdoutConsumer) 141 + 142 + // Note: FBSimulatorControl's startStreaming often doesn't complete its future 143 + // but the stream may still work. We'll continue without waiting. 144 + startFuture.onQueue(BridgeQueues.videoStreamQueue, notifyOfCompletion: { future in 145 + if let error = future.error { 146 + FileHandle.standardError.write(Data("Stream initialization error: \(error)\n".utf8)) 147 + } 148 + }) 149 + 150 + // Give the stream time to start 151 + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second 152 + 153 + FileHandle.standardError.write(Data("Stream is now running...\n".utf8)) 154 + 155 + // Set up cancellation handler 156 + await withTaskCancellationHandler { 157 + // Keep the stream running until cancelled 158 + while !Task.isCancelled { 159 + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms 160 + } 161 + } onCancel: { 162 + Task { 163 + FileHandle.standardError.write(Data("\nStopping video stream...\n".utf8)) 164 + 165 + // Stop the video stream on the same queue 166 + do { 167 + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in 168 + BridgeQueues.videoStreamQueue.async { 169 + let stopFuture = videoStream.stopStreaming() 170 + 171 + stopFuture.onQueue(BridgeQueues.videoStreamQueue, notifyOfCompletion: { future in 172 + if let error = future.error { 173 + FileHandle.standardError.write(Data("Stream stop error: \(error)\n".utf8)) 174 + continuation.resume(throwing: error) 175 + } else { 176 + FileHandle.standardError.write(Data("Stream stopped successfully\n".utf8)) 177 + continuation.resume() 178 + } 179 + }) 180 + } 181 + } 182 + } catch { 183 + FileHandle.standardError.write(Data("Error stopping stream: \(error)\n".utf8)) 184 + } 185 + } 186 + } 187 + 188 + } catch { 189 + throw CLIError(errorDescription: "Failed to stream video: \(error.localizedDescription)") 190 + } 191 + } 192 + }
+3
Sources/AXe/Utilities/BridgeQueues.swift
··· 25 25 /// ``` 26 26 /// 27 27 public static let miscEventReaderQueue = DispatchQueue(label: "com.axe.miscellaneous.reader", qos: .userInitiated, attributes: .concurrent) 28 + 29 + /// Dedicated serial queue for video streaming operations to prevent race conditions 30 + public static let videoStreamQueue = DispatchQueue(label: "com.axe.video.stream", qos: .userInitiated) 28 31 }
+2 -1
Sources/AXe/main.swift
··· 23 23 Key.self, 24 24 KeySequence.self, 25 25 Touch.self, 26 - Gesture.self 26 + Gesture.self, 27 + StreamVideo.self 27 28 ] 28 29 ) 29 30 }
+38
Tests/StreamVideoDebugTest.swift
··· 1 + import Testing 2 + import Foundation 3 + 4 + @Suite("Stream Video Debug Tests") 5 + struct StreamVideoDebugTests { 6 + @Test("Stream video command runs without hanging") 7 + func streamVideoBasicExecution() async throws { 8 + // This test just verifies the command can be executed and terminated 9 + // without hanging indefinitely 10 + 11 + guard let udid = defaultSimulatorUDID else { 12 + throw TestError.commandError("No simulator UDID specified") 13 + } 14 + 15 + // Launch any app to have something on screen 16 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 17 + 18 + // Create a task to run the command 19 + let commandTask = Task { 20 + try await TestHelpers.runAxeCommand( 21 + "stream-video --format mjpeg --fps 1", 22 + simulatorUDID: udid 23 + ) 24 + } 25 + 26 + // Wait a bit 27 + try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds 28 + 29 + // Cancel the task 30 + commandTask.cancel() 31 + 32 + // Give it time to clean up 33 + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second 34 + 35 + // If we get here without hanging, the test passes 36 + #expect(commandTask.isCancelled, "Command task should be cancelled") 37 + } 38 + }
+187
Tests/StreamVideoTests.swift
··· 1 + import Testing 2 + import Foundation 3 + 4 + @Suite("Stream Video Command Tests") 5 + struct StreamVideoTests { 6 + @Test("Stream video outputs data to stdout for BGRA format") 7 + func streamVideoBGRA() async throws { 8 + // Arrange 9 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") // Any screen is fine for video streaming 10 + 11 + // Act - Stream for 2 seconds 12 + let result = try await streamVideoForDuration(format: "bgra", duration: 2.0) 13 + 14 + // Assert 15 + #expect(result.exitCode == 0, "Command should exit successfully") 16 + #expect(!result.output.isEmpty, "Should have output messages") 17 + #expect(result.dataSize > 0, "Should have received raw video data bytes") 18 + #expect(result.output.contains("Starting video stream"), "Should show startup message") 19 + #expect(result.output.contains("Format: bgra"), "Should show format") 20 + // BGRA should produce roughly width*height*4 bytes per frame 21 + #expect(result.dataSize > 1_000_000, "Should have substantial raw video data for 2 seconds") 22 + } 23 + 24 + @Test("Stream video shows warning for H264 format") 25 + func streamVideoH264Warning() async throws { 26 + // Arrange 27 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 28 + 29 + // Act 30 + let result = try await streamVideoForDuration(format: "h264", duration: 1.0) 31 + 32 + // Assert 33 + #expect(result.exitCode == 0, "Command should exit successfully") 34 + #expect(result.output.contains("WARNING: Only BGRA format currently works"), "Should show warning about H264") 35 + #expect(result.output.contains("Format: h264"), "Should show format") 36 + // H264 currently doesn't produce data due to FBSimulatorControl issues 37 + #expect(result.dataSize == 0, "H264 format currently produces no data") 38 + } 39 + 40 + @Test("Stream video shows warning for MJPEG format") 41 + func streamVideoMJPEGWarning() async throws { 42 + // Arrange 43 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 44 + 45 + // Act 46 + let result = try await streamVideoForDuration(format: "mjpeg", duration: 1.0) 47 + 48 + // Assert 49 + #expect(result.exitCode == 0, "Command should exit successfully") 50 + #expect(result.output.contains("WARNING: Only BGRA format currently works"), "Should show warning about MJPEG") 51 + #expect(result.output.contains("Format: mjpeg"), "Should show format") 52 + // MJPEG currently doesn't produce data due to FBSimulatorControl issues 53 + #expect(result.dataSize == 0, "MJPEG format currently produces no data") 54 + } 55 + 56 + @Test("Stream BGRA video with custom FPS") 57 + func streamBGRAVideoWithFPS() async throws { 58 + // Arrange 59 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 60 + 61 + // Act 62 + let result = try await streamVideoForDuration(format: "bgra", fps: 5, duration: 1.0) 63 + 64 + // Assert 65 + #expect(result.exitCode == 0, "Command should exit successfully") 66 + #expect(result.output.contains("FPS: 5"), "Should show custom FPS") 67 + #expect(result.dataSize > 0, "Should have received video data") 68 + // With 5 FPS for 1 second, we expect roughly 5 frames worth of data 69 + let expectedMinSize = 393 * 852 * 4 * 3 // At least 3 frames (allowing for timing) 70 + #expect(result.dataSize > expectedMinSize, "Should have received multiple frames") 71 + } 72 + 73 + @Test("Stream BGRA video with quality and scale settings") 74 + func streamBGRAVideoWithQualityAndScale() async throws { 75 + // Arrange 76 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 77 + 78 + // Act 79 + let result = try await streamVideoForDuration( 80 + format: "bgra", 81 + fps: 5, 82 + quality: 0.5, 83 + scale: 0.5, 84 + duration: 1.0 85 + ) 86 + 87 + // Assert 88 + #expect(result.exitCode == 0, "Command should exit successfully") 89 + #expect(result.output.contains("Quality: 0.5"), "Should show quality setting") 90 + #expect(result.output.contains("Scale: 0.5"), "Should show scale setting") 91 + #expect(result.dataSize > 0, "Should have received video data") 92 + // Note: Scale might not affect BGRA raw output in current implementation 93 + } 94 + 95 + @Test("Stream video can be cancelled gracefully") 96 + func streamVideoCancellation() async throws { 97 + // Arrange 98 + try await TestHelpers.launchPlaygroundApp(to: "tap-test") 99 + 100 + // Act - Start streaming and cancel quickly 101 + let task = Task { 102 + try await TestHelpers.runAxeCommand( 103 + "stream-video --format bgra --fps 30", 104 + simulatorUDID: defaultSimulatorUDID 105 + ) 106 + } 107 + 108 + // Wait a bit then cancel 109 + try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds 110 + task.cancel() 111 + 112 + // Assert - Task should complete without throwing 113 + do { 114 + _ = try await task.value 115 + } catch { 116 + // Cancellation is expected 117 + #expect(error is CancellationError || String(describing: error).contains("cancel")) 118 + } 119 + } 120 + 121 + // MARK: - Helper Methods 122 + 123 + private func streamVideoForDuration( 124 + format: String = "h264", 125 + fps: Int? = nil, 126 + quality: Double = 0.2, 127 + scale: Double = 1.0, 128 + bitrate: Int? = nil, 129 + keyFrameInterval: Int = 10, 130 + duration: TimeInterval = 2.0 131 + ) async throws -> (output: String, dataSize: Int, firstBytes: [UInt8], exitCode: Int32) { 132 + // Build command 133 + var command = "stream-video --format \(format)" 134 + if let fps = fps { 135 + command += " --fps \(fps)" 136 + } 137 + command += " --quality \(quality) --scale \(scale)" 138 + if let bitrate = bitrate { 139 + command += " --bitrate \(bitrate)" 140 + } 141 + command += " --key-frame-interval \(keyFrameInterval)" 142 + 143 + // Run command with timeout 144 + let task = Task { 145 + try await TestHelpers.runAxeCommand(command, simulatorUDID: defaultSimulatorUDID) 146 + } 147 + 148 + // Wait for duration 149 + try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) 150 + 151 + // Cancel the streaming 152 + task.cancel() 153 + 154 + // Get the result 155 + do { 156 + let result = try await task.value 157 + 158 + // Parse stdout (video data) and stderr (messages) 159 + let videoData = result.stdout 160 + let output = result.stderr 161 + 162 + // Get first few bytes for format verification 163 + let firstBytes = Array(videoData.prefix(10)) 164 + 165 + return ( 166 + output: output, 167 + dataSize: videoData.count, 168 + firstBytes: firstBytes, 169 + exitCode: result.exitCode 170 + ) 171 + } catch { 172 + // If cancelled, still try to get output 173 + if let commandError = error as? CommandRunner.RunError, 174 + case .nonZeroExitCode(let code, let stdout, let stderr) = commandError { 175 + let firstBytes = Array(stdout.prefix(10)) 176 + return ( 177 + output: stderr, 178 + dataSize: stdout.count, 179 + firstBytes: firstBytes, 180 + exitCode: code 181 + ) 182 + } 183 + // For cancellation, return empty result 184 + return (output: "", dataSize: 0, firstBytes: [], exitCode: 0) 185 + } 186 + } 187 + }
+65
examples/stream-video-bgra-example.sh
··· 1 + #!/bin/bash 2 + 3 + # Example: Working video streaming from iOS Simulator using BGRA format 4 + # This demonstrates how to use the stream-video command with raw BGRA output 5 + 6 + # Get simulator UDID (use first booted iPhone) 7 + UDID=$(xcrun simctl list devices | grep "iPhone.*Booted" | head -1 | grep -o "[A-F0-9-]\{36\}") 8 + 9 + if [ -z "$UDID" ]; then 10 + echo "No booted iPhone simulator found. Please boot a simulator first." 11 + exit 1 12 + fi 13 + 14 + echo "Using simulator: $UDID" 15 + 16 + # Get simulator dimensions (common iPhone sizes) 17 + # You may need to adjust these based on your simulator model 18 + WIDTH=393 # iPhone 15 Pro width 19 + HEIGHT=852 # iPhone 15 Pro height 20 + 21 + echo "Assuming dimensions: ${WIDTH}x${HEIGHT}" 22 + 23 + # Example 1: Stream BGRA to a raw file 24 + echo "" 25 + echo "Example 1: Streaming raw BGRA video to file..." 26 + echo "Press Ctrl+C to stop recording" 27 + swift run axe stream-video --udid "$UDID" --format bgra --fps 10 > raw-video.bgra 28 + 29 + echo "Raw video saved to: raw-video.bgra" 30 + echo "File size: $(ls -lh raw-video.bgra | awk '{print $5}')" 31 + 32 + # Example 2: Stream and convert to MP4 in real-time (requires ffmpeg) 33 + if command -v ffmpeg &> /dev/null; then 34 + echo "" 35 + echo "Example 2: Streaming and converting to MP4 in real-time..." 36 + echo "Press Ctrl+C to stop recording" 37 + 38 + swift run axe stream-video --udid "$UDID" --format bgra --fps 10 | \ 39 + ffmpeg -f rawvideo -pixel_format bgra -video_size ${WIDTH}x${HEIGHT} -framerate 10 -i - \ 40 + -c:v libx264 -pix_fmt yuv420p -preset fast \ 41 + -y realtime-capture.mp4 42 + 43 + echo "Video saved to: realtime-capture.mp4" 44 + else 45 + echo "" 46 + echo "FFmpeg not installed. To convert BGRA to MP4:" 47 + echo "brew install ffmpeg" 48 + echo "Then run:" 49 + echo "ffmpeg -f rawvideo -pixel_format bgra -video_size ${WIDTH}x${HEIGHT} -i raw-video.bgra -c:v libx264 -pix_fmt yuv420p output.mp4" 50 + fi 51 + 52 + # Example 3: Extract frames from raw BGRA 53 + echo "" 54 + echo "Example 3: Extracting first frame as PNG..." 55 + if command -v ffmpeg &> /dev/null && [ -f raw-video.bgra ]; then 56 + BYTES_PER_FRAME=$((WIDTH * HEIGHT * 4)) 57 + dd if=raw-video.bgra of=first-frame.raw bs=$BYTES_PER_FRAME count=1 2>/dev/null 58 + ffmpeg -f rawvideo -pixel_format bgra -video_size ${WIDTH}x${HEIGHT} -i first-frame.raw -frames:v 1 first-frame.png -y 2>/dev/null 59 + rm -f first-frame.raw 60 + echo "First frame saved to: first-frame.png" 61 + fi 62 + 63 + echo "" 64 + echo "Note: H264 and MJPEG formats are currently not working due to FBSimulatorControl issues." 65 + echo "Use BGRA format for raw pixel data or 'xcrun simctl io recordVideo' for compressed video."
+50
examples/stream-video-example.sh
··· 1 + #!/bin/bash 2 + 3 + # Example script demonstrating video streaming with AXe 4 + 5 + # Get simulator UDID (use first booted iPhone) 6 + UDID=$(xcrun simctl list devices | grep "iPhone.*Booted" | head -1 | grep -o "[A-F0-9-]\{36\}") 7 + 8 + if [ -z "$UDID" ]; then 9 + echo "No booted iPhone simulator found. Please boot a simulator first." 10 + exit 1 11 + fi 12 + 13 + echo "Using simulator: $UDID" 14 + 15 + # Example 1: Stream H264 video to file 16 + echo "Example 1: Streaming H264 video to file for 5 seconds..." 17 + swift run axe stream-video --udid "$UDID" --format h264 --fps 30 > recording.h264 & 18 + PID=$! 19 + sleep 5 20 + kill -INT $PID 21 + wait $PID 2>/dev/null 22 + echo "Video saved to recording.h264" 23 + echo "You can play it with: ffplay -f h264 recording.h264" 24 + echo "" 25 + 26 + # Example 2: Stream MJPEG to stdout and pipe to ffplay (if available) 27 + if command -v ffplay &> /dev/null; then 28 + echo "Example 2: Streaming MJPEG video to ffplay for real-time viewing..." 29 + echo "Press Ctrl+C to stop" 30 + swift run axe stream-video --udid "$UDID" --format mjpeg --fps 15 --quality 0.5 | ffplay -f mjpeg -i - 2>/dev/null 31 + else 32 + echo "Example 2: ffplay not found. Install ffmpeg to view video in real-time." 33 + echo "You can install it with: brew install ffmpeg" 34 + fi 35 + 36 + # Example 3: Stream with scaling 37 + echo "" 38 + echo "Example 3: Streaming scaled video (50% size) to file..." 39 + swift run axe stream-video --udid "$UDID" --format h264 --scale 0.5 --fps 20 > recording_scaled.h264 & 40 + PID=$! 41 + sleep 3 42 + kill -INT $PID 43 + wait $PID 2>/dev/null 44 + echo "Scaled video saved to recording_scaled.h264" 45 + 46 + # Cleanup 47 + rm -f recording.h264 recording_scaled.h264 48 + 49 + echo "" 50 + echo "Video streaming examples completed!"
+43
examples/video-recording-alternative.sh
··· 1 + #!/bin/bash 2 + 3 + # Alternative video recording script for iOS Simulator 4 + # Since streaming to stdout is no longer supported by Apple and FBSimulatorControl has issues 5 + 6 + # Get simulator UDID (use first booted iPhone) 7 + UDID=$(xcrun simctl list devices | grep "iPhone.*Booted" | head -1 | grep -o "[A-F0-9-]\{36\}") 8 + 9 + if [ -z "$UDID" ]; then 10 + echo "No booted iPhone simulator found. Please boot a simulator first." 11 + exit 1 12 + fi 13 + 14 + echo "Using simulator: $UDID" 15 + 16 + # Method 1: Record to file using xcrun simctl 17 + echo "Method 1: Recording video to file using xcrun simctl..." 18 + echo "Press Ctrl+C to stop recording" 19 + OUTPUT_FILE="simulator-recording-$(date +%Y%m%d-%H%M%S).mp4" 20 + xcrun simctl io "$UDID" recordVideo --codec=h264 "$OUTPUT_FILE" 21 + echo "Video saved to: $OUTPUT_FILE" 22 + 23 + # Method 2: Use AXe stream-video (may not produce output due to known issues) 24 + echo "" 25 + echo "Method 2: Attempting to stream using AXe (may not work due to FBSimulatorControl issues)..." 26 + echo "Press Ctrl+C to stop if it hangs" 27 + timeout 10 swift run axe stream-video --udid "$UDID" --format mjpeg > test-stream.mjpeg 2>&1 || true 28 + 29 + if [ -s test-stream.mjpeg ]; then 30 + echo "Stream data saved to test-stream.mjpeg" 31 + else 32 + echo "No stream data produced (expected due to known issues)" 33 + rm -f test-stream.mjpeg 34 + fi 35 + 36 + # Method 3: Screen recording recommendation 37 + echo "" 38 + echo "Method 3: For live streaming, consider using:" 39 + echo "- OBS Studio (https://obsproject.com/) - Free and open source" 40 + echo "- QuickTime Player - Built-in screen recording" 41 + echo "- ScreenFlow or similar commercial tools" 42 + echo "" 43 + echo "These tools can capture the simulator window and stream/record as needed."
+2 -1
test-runner.sh
··· 63 63 echo " ButtonTests Run only button tests" 64 64 echo " GestureTests Run only gesture tests" 65 65 echo " ListSimulatorsTests Run only list simulators tests" 66 + echo " StreamVideoTests Run only stream video tests" 66 67 echo "" 67 68 echo "Examples:" 68 69 echo " $0 # Build everything and run all tests" ··· 106 107 VERBOSE=true 107 108 shift 108 109 ;; 109 - SwipeTests|TapTests|KeyTests|TouchTests|TypeTests|ButtonTests|GestureTests|ListSimulatorsTests) 110 + SwipeTests|TapTests|KeyTests|TouchTests|TypeTests|ButtonTests|GestureTests|ListSimulatorsTests|StreamVideoTests) 110 111 TEST_FILTER="$1" 111 112 shift 112 113 ;;