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:
parent
6f2661c3d1
commit
fa38caa5a5
@ -37,6 +37,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
|
|||||||
private var process: Process?
|
private var process: Process?
|
||||||
private var duration: Double = 0
|
private var duration: Double = 0
|
||||||
private var stopRequested = false
|
private var stopRequested = false
|
||||||
|
private var stderrAccumulator = ""
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
|
|||||||
guard !isRecording else { return }
|
guard !isRecording else { return }
|
||||||
|
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
self.stderrAccumulator = ""
|
||||||
|
|
||||||
// 查找 ffmpeg
|
// 查找 ffmpeg
|
||||||
let ffmpegPath = findFFmpeg()
|
let ffmpegPath = findFFmpeg()
|
||||||
@ -53,6 +55,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
|
|||||||
|
|
||||||
// 解析 HLS 变体
|
// 解析 HLS 变体
|
||||||
let resolvedURL = HLSVariantResolver.resolve(url: url, maxWidth: 1280) ?? url
|
let resolvedURL = HLSVariantResolver.resolve(url: url, maxWidth: 1280) ?? url
|
||||||
|
NSLog("[Recorder] ffmpeg input URL: %@", resolvedURL.absoluteString)
|
||||||
|
|
||||||
// 删除已存在的输出文件
|
// 删除已存在的输出文件
|
||||||
if FileManager.default.fileExists(atPath: outputPath) {
|
if FileManager.default.fileExists(atPath: outputPath) {
|
||||||
@ -78,12 +81,13 @@ public final class FFmpegRecorder: StreamRecorderEngine {
|
|||||||
outputPath
|
outputPath
|
||||||
]
|
]
|
||||||
proc.arguments = args
|
proc.arguments = args
|
||||||
|
NSLog("[Recorder] ffmpeg args: %@", args.joined(separator: " "))
|
||||||
|
|
||||||
let stderrPipe = Pipe()
|
let stderrPipe = Pipe()
|
||||||
proc.standardError = stderrPipe
|
proc.standardError = stderrPipe
|
||||||
proc.standardOutput = FileHandle.nullDevice
|
proc.standardOutput = FileHandle.nullDevice
|
||||||
|
|
||||||
// 解析进度
|
// 解析进度 + 收集 stderr
|
||||||
let progressState = ProgressState()
|
let progressState = ProgressState()
|
||||||
stderrPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
stderrPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@ -91,6 +95,7 @@ public final class FFmpegRecorder: StreamRecorderEngine {
|
|||||||
guard !data.isEmpty else { return }
|
guard !data.isEmpty else { return }
|
||||||
|
|
||||||
let str = String(data: data, encoding: .utf8) ?? ""
|
let str = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
self.stderrAccumulator += str
|
||||||
if let timeRange = str.range(of: "time=") {
|
if let timeRange = str.range(of: "time=") {
|
||||||
let sub = String(str[timeRange.upperBound...])
|
let sub = String(str[timeRange.upperBound...])
|
||||||
if let endRange = sub.range(of: " ") ?? sub.range(of: "\n") ?? sub.range(of: "\r") {
|
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
|
isRecording = false
|
||||||
|
|
||||||
guard proc.terminationStatus == 0 || stopRequested else {
|
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 {
|
guard FileManager.default.fileExists(atPath: outputPath) else {
|
||||||
|
|||||||
@ -57,7 +57,7 @@ public enum RecordingError: Error, LocalizedError {
|
|||||||
case invalidURL
|
case invalidURL
|
||||||
case noFFmpeg
|
case noFFmpeg
|
||||||
case noMediaTracks
|
case noMediaTracks
|
||||||
case processFailed(Int)
|
case processFailed(Int, String? = nil)
|
||||||
case outputNotFound
|
case outputNotFound
|
||||||
case unknown(String)
|
case unknown(String)
|
||||||
|
|
||||||
@ -66,7 +66,12 @@ public enum RecordingError: Error, LocalizedError {
|
|||||||
case .invalidURL: return "无效的 URL"
|
case .invalidURL: return "无效的 URL"
|
||||||
case .noFFmpeg: return "未找到 ffmpeg,请安装: brew install ffmpeg"
|
case .noFFmpeg: return "未找到 ffmpeg,请安装: brew install ffmpeg"
|
||||||
case .noMediaTracks: return "未找到任何媒体轨道"
|
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 .outputNotFound: return "输出文件不存在"
|
||||||
case .unknown(let msg): return msg
|
case .unknown(let msg): return msg
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user