feat: capture ffmpeg stderr on failure for diagnostics

- Accumulate stderr in FFmpegRecorder during recording
- Log last 2000 chars of stderr when ffmpeg exits non-zero
- Log ffmpeg input URL and args at start
- RecordingError.processFailed now includes optional stderr string
- Error message shows ffmpeg error details to user
This commit is contained in:
yumoqing 2026-07-17 20:36:09 +08:00
parent 6f2661c3d1
commit fa38caa5a5
2 changed files with 16 additions and 4 deletions

View File

@ -37,6 +37,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
private var process: Process?
private var duration: Double = 0
private var stopRequested = false
private var stderrAccumulator = ""
public init() {}
@ -44,6 +45,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
guard !isRecording else { return }
self.duration = duration
self.stderrAccumulator = ""
// ffmpeg
let ffmpegPath = findFFmpeg()
@ -53,6 +55,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
// HLS
let resolvedURL = HLSVariantResolver.resolve(url: url, maxWidth: 1280) ?? url
NSLog("[Recorder] ffmpeg input URL: %@", resolvedURL.absoluteString)
//
if FileManager.default.fileExists(atPath: outputPath) {
@ -78,12 +81,13 @@ public final class FFmpegRecorder: StreamRecorderEngine {
outputPath
]
proc.arguments = args
NSLog("[Recorder] ffmpeg args: %@", args.joined(separator: " "))
let stderrPipe = Pipe()
proc.standardError = stderrPipe
proc.standardOutput = FileHandle.nullDevice
//
// + stderr
let progressState = ProgressState()
stderrPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
@ -91,6 +95,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
guard !data.isEmpty else { return }
let str = String(data: data, encoding: .utf8) ?? ""
self.stderrAccumulator += str
if let timeRange = str.range(of: "time=") {
let sub = String(str[timeRange.upperBound...])
if let endRange = sub.range(of: " ") ?? sub.range(of: "\n") ?? sub.range(of: "\r") {
@ -128,7 +133,9 @@ public final class FFmpegRecorder: StreamRecorderEngine {
isRecording = false
guard proc.terminationStatus == 0 || stopRequested else {
throw RecordingError.processFailed(Int(proc.terminationStatus))
let lastStderr = String(stderrAccumulator.suffix(2000))
NSLog("[Recorder] ffmpeg stderr (last 2000 chars):\n%@", lastStderr)
throw RecordingError.processFailed(Int(proc.terminationStatus), lastStderr)
}
guard FileManager.default.fileExists(atPath: outputPath) else {

View File

@ -57,7 +57,7 @@ public enum RecordingError: Error, LocalizedError {
case invalidURL
case noFFmpeg
case noMediaTracks
case processFailed(Int)
case processFailed(Int, String? = nil)
case outputNotFound
case unknown(String)
@ -66,7 +66,12 @@ public enum RecordingError: Error, LocalizedError {
case .invalidURL: return "无效的 URL"
case .noFFmpeg: return "未找到 ffmpeg请安装: brew install ffmpeg"
case .noMediaTracks: return "未找到任何媒体轨道"
case .processFailed(let code): return "录制失败 (exit code: \(code))"
case .processFailed(let code, let stderr):
if let s = stderr, !s.isEmpty {
let preview = String(s.prefix(300))
return "录制失败 (exit code: \(code)):\n\(preview)"
}
return "录制失败 (exit code: \(code))"
case .outputNotFound: return "输出文件不存在"
case .unknown(let msg): return msg
}