diff --git a/.ai/commit-checkpoints.json b/.ai/commit-checkpoints.json index d6bd15e6..c7f2c90c 100644 --- a/.ai/commit-checkpoints.json +++ b/.ai/commit-checkpoints.json @@ -1,7 +1,7 @@ { "last_commit": { - "timestamp": "2025-12-21T23:15:16.584895+00:00", - "session_id": "60ef8f57-a5e4-4edb-a61d-0a4778a6de32", - "last_entry_timestamp": "2025-12-21T23:15:15.232Z" + "timestamp": "2025-12-21T23:20:32.869030+00:00", + "session_id": "019b4336-8118-7961-95cb-f63fd5f8c638", + "last_entry_timestamp": "2025-12-21T23:20:28.851Z" } } \ No newline at end of file diff --git a/cli/stream-mac.sh b/cli/stream-mac.sh index c8478a2d..742af436 100755 --- a/cli/stream-mac.sh +++ b/cli/stream-mac.sh @@ -21,9 +21,9 @@ if [ -z "$STREAM_KEY" ]; then exit 1 fi -exec ffmpeg -f avfoundation -capture_cursor 1 -framerate 30 -i "2:1" \ - -c:v h264_videotoolbox -b:v 4500k -maxrate 4500k -bufsize 9000k \ +exec ffmpeg -f avfoundation -capture_cursor 1 -framerate 60 -i "2:1" \ + -c:v h264_videotoolbox -b:v 30000k -maxrate 45000k -bufsize 90000k \ -profile:v high -pix_fmt yuv420p \ - -g 60 -keyint_min 60 \ - -c:a aac -b:a 128k -ar 48000 -ac 2 \ + -g 120 -keyint_min 120 \ + -c:a aac -b:a 256k -ar 48000 -ac 2 \ -f flv "${RTMPS_URL}${STREAM_KEY}" diff --git a/cli/stream/Sources/stream-capture/main.swift b/cli/stream/Sources/stream-capture/main.swift index 29cde1f7..d6937b93 100644 --- a/cli/stream/Sources/stream-capture/main.swift +++ b/cli/stream/Sources/stream-capture/main.swift @@ -173,8 +173,9 @@ actor ZeroCPUCapturer: NSObject, SCStreamDelegate, SCStreamOutput { config.width = normalizedSize.width config.height = normalizedSize.height - // 30 FPS for streaming - config.minimumFrameInterval = CMTime(value: 1, timescale: 30) + let targetFrameRate: Int32 = 60 + // 60 FPS for streaming + config.minimumFrameInterval = CMTime(value: 1, timescale: targetFrameRate) // Queue depth for smooth delivery (like OBS) config.queueDepth = 8 @@ -210,7 +211,7 @@ actor ZeroCPUCapturer: NSObject, SCStreamDelegate, SCStreamOutput { // Start capture try await stream?.startCapture() - print("Capture started: \(config.width)x\(config.height) @ 30fps") + print("Capture started: \(config.width)x\(config.height) @ \(targetFrameRate)fps") } func stopCapture() async { @@ -329,10 +330,19 @@ class HardwareEncoder { } // Configure for streaming + let targetFrameRate: Int32 = 60 + let keyframeInterval = Int(targetFrameRate) * 2 + let bitrate = HardwareEncoder.recommendedBitrate( + width: width, + height: height, + frameRate: Int(targetFrameRate) + ) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_High_AutoLevel) - VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: 4_500_000 as CFNumber) // 4.5 Mbps - VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: 60 as CFNumber) // Keyframe every 2s @ 30fps + VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: targetFrameRate as CFNumber) + VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: bitrate as CFNumber) + VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: keyframeInterval as CFNumber) + VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: 2 as CFNumber) VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse) // No B-frames for low latency VTCompressionSessionPrepareToEncodeFrames(session) @@ -374,6 +384,19 @@ class HardwareEncoder { } } + private static func recommendedBitrate(width: Int, height: Int, frameRate: Int) -> Int { + let baseWidth = 2560 + let baseHeight = 1440 + let baseFrameRate = 60 + let baseBitrate = 30_000_000 + + let pixels = max(1, width) * max(1, height) + let basePixels = baseWidth * baseHeight + let fpsScale = Double(max(frameRate, 1)) / Double(baseFrameRate) + let raw = Double(baseBitrate) * (Double(pixels) / Double(basePixels)) * fpsScale + return min(max(Int(raw.rounded()), 12_000_000), 80_000_000) + } + deinit { if let session = session { VTCompressionSessionInvalidate(session) diff --git a/packages/worker/stream/src/receiver.rs b/packages/worker/stream/src/receiver.rs index a3e28619..f14070d7 100644 --- a/packages/worker/stream/src/receiver.rs +++ b/packages/worker/stream/src/receiver.rs @@ -112,9 +112,9 @@ pub async fn start_hls( "-c:a", "aac", "-b:a", - "128k", + "256k", "-ar", - "44100", + "48000", // HLS output settings "-f", "hls", @@ -183,9 +183,9 @@ pub async fn start_youtube( "-c:a", "aac", "-b:a", - "128k", + "256k", "-ar", - "44100", + "48000", // FLV container for RTMP "-f", "flv", @@ -268,9 +268,9 @@ pub async fn start_youtube_with_filter( "-c:a".to_string(), "aac".to_string(), "-b:a".to_string(), - "128k".to_string(), + "256k".to_string(), "-ar".to_string(), - "44100".to_string(), + "48000".to_string(), ]); // Output