限制錄影 sidecar 落盤時機並釋放失敗 writer
Docs/ChangeHistory.mdliveAPP/Capture/SegmentedRecordingWriter.swift
commit bc9fa71b537e4637edd700e2a301ae85c4b94550Author: Nuclear0709 <agp05432@gmail.com>Date: Thu Nov 12 09:45:27 2026 +0800 限制錄影 sidecar 落盤時機並釋放失敗 writer diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.md--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -2,6 +2,7 @@ ## 2026.11.10 音訊主時鐘與 RTMP 診斷可讀性 +- 錄影 manifest(sidecar)只在開始、完成或中斷等 phase 轉換時持久化 `sourceStartPTS`/`sourceEndPTS`;進度回報不把尾端累積在記憶體等待落盤,避免長錄影產生額外的狀態寫入。失敗收尾會主動釋放 `AVAssetWriter` 與檔案 URL,不讓失敗片段持有 handle。 - 錄影 manifest(sidecar)新增 `sourceStartPTS`/`sourceEndPTS`/`fileStartPTS`,由 `SegmentedRecordingWriter` 在第一個 video sample 與完成時寫入;`filePTS = sourcePTS - sourceStartPTS + fileStartPTS` 可對照 App 擷取時鐘與檔案時間軸。此階段只建立對照,不改檔案時間戳。 - A/V Lag 新增 `sourcePTSDeltaDrift`/`sourcePTSDeltaPerMinute`/`avClockDrift`/`avSyncState`,以第一筆有效來源差為基準區分 stable/drifting/jumped;報告頁與 JSON 都能看到漂移趨勢,不直接用 `publish throughput avOffset` 判斷口型同步。 - 編碼輸入緩衝 UI 改稱「佇列保留」,新增 `received`/`consumed` 累計交接數;文件明確說明直接 handoff 時 `bytes=0`、`peakBytes=0` 是正常,不代表影格沒有進入編碼,也不代表沒有配置容量。diff --git a/liveAPP/Capture/SegmentedRecordingWriter.swift b/liveAPP/Capture/SegmentedRecordingWriter.swift--- a/liveAPP/Capture/SegmentedRecordingWriter.swift+++ b/liveAPP/Capture/SegmentedRecordingWriter.swift@@ -345,8 +345,14 @@ actor RecordingSegmentWriter { var sourceEnd: Double? if lastVideo.isValid { sourceEnd = lastVideo.seconds } if audioEnd.isValid { sourceEnd = max(sourceEnd ?? audioEnd.seconds, audioEnd.seconds) }- await library.recordSourceRange(id, sourceStartPTS: sourceStart, sourceEndPTS: sourceEnd,- persist: phase != nil)+ // 進度回報不把 sourceEnd 留在記憶體等落盤;只有 phase 轉換時才一次寫入+ // 起點與目前尾端,避免長時間錄影的反覆狀態更新。+ let persistSourceRange = phase != nil+ await library.recordSourceRange(+ id,+ sourceStartPTS: sourceStart,+ sourceEndPTS: persistSourceRange ? sourceEnd : nil,+ persist: persistSourceRange) await library.update(id, phase: phase, duration: duration, bytes: bytes, message: "可控錄製 · 第 \(number) 段 · \(bitrate) · 寫入端丟棄 \(dropped) 個樣本" + sizeWarning) }@@ -356,6 +362,13 @@ actor RecordingSegmentWriter { func failed(_ failure: Error) async { writer?.cancelWriting() await report(phase: .interrupted)+ // 失敗後仍可能被 router 持有到整場結束;主動釋放 writer 與檔案 URL,+ // 不要讓 AVAssetWriter 的檔案 handle 跟著失敗片段一起留在記憶體。+ writer = nil+ video = nil+ audio = nil+ id = nil+ url = nil sendlog(message: "[SegmentedRecording] group=\(group) failed=\(failure)") } }