app · c5c2cba

Dev index Recent changes / versions Changelog About TODO

記錄錄影音訊起點供 A/V origin 量測

c5c2cba · 2026-11-12 09:48 · app
Docs/ChangeHistory.mdliveAPP/Capture/CaptureSessionState.swiftliveAPP/Capture/RecordingLibrary.swiftliveAPP/Capture/SegmentedRecordingWriter.swift
commit c5c2cbaae69330bca38b97b44117ff78e71d7dbfAuthor: Nuclear0709 <agp05432@gmail.com>Date:   Thu Nov 12 09:48:44 2026 +0800     記錄錄影音訊起點供 A/V origin 量測 diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.mdindex 8ca4710..f3933d9 100644--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -3,6 +3,7 @@ ## 2026.11.10 音訊主時鐘與 RTMP 診斷可讀性  - 錄影 manifest(sidecar)只在開始、完成或中斷等 phase 轉換時持久化 `sourceStartPTS`/`sourceEndPTS`;進度回報不把尾端累積在記憶體等待落盤,避免長錄影產生額外的狀態寫入。失敗收尾會主動釋放 `AVAssetWriter` 與檔案 URL,不讓失敗片段持有 handle。+- 錄影 manifest(sidecar)新增 `sourceAudioStartPTS`,用來量測第一個實際寫入音訊 sample 與 `sourceStartPTS`(第一個 video)的起點差;這只在 phase 轉換時落盤。 - 錄影 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/CaptureSessionState.swift b/liveAPP/Capture/CaptureSessionState.swiftindex 4ad171b..a2be0cc 100644--- a/liveAPP/Capture/CaptureSessionState.swift+++ b/liveAPP/Capture/CaptureSessionState.swift@@ -97,6 +97,8 @@ struct LocalRecording: Identifiable, Codable {     var sourceStartPTS: Double?     /// 本段最後成功寫入的 source PTS(影片與音訊尾端取較大者)。     var sourceEndPTS: Double?+    /// 本段第一個實際寫入的音訊 source PTS;用來量測 A/V 起點差。+    var sourceAudioStartPTS: Double?     /// MP4 檔內第一個可播放 sample 的 PTS;正常由 writer 設為 0。     var fileStartPTS: Double?     /// 合併產物所屬群組;與原始片段分開保存,不參與再次合併。diff --git a/liveAPP/Capture/RecordingLibrary.swift b/liveAPP/Capture/RecordingLibrary.swiftindex 10003a2..0eb31c9 100644--- a/liveAPP/Capture/RecordingLibrary.swift+++ b/liveAPP/Capture/RecordingLibrary.swift@@ -84,13 +84,17 @@ import Photos     }     /// 寫入此片段與 App 擷取時間軸的對照;manifest JSON 就是 sidecar。     /// sourceStartPTS 是本段第一個 video PTS,fileStartPTS 正常為 0。-    func recordSourceRange(_ id: UUID, sourceStartPTS: Double?, sourceEndPTS: Double?, persist: Bool = true) {+    func recordSourceRange(_ id: UUID, sourceStartPTS: Double?, sourceEndPTS: Double?,+                           sourceAudioStartPTS: Double? = nil, persist: Bool = true) {         guard let index = recordings.firstIndex(where: { $0.id == id }) else { return }         var item = recordings[index]         if let sourceStartPTS, item.sourceStartPTS == nil {             item.sourceStartPTS = sourceStartPTS             item.fileStartPTS = item.fileStartPTS ?? 0         }+        if let sourceAudioStartPTS, item.sourceAudioStartPTS == nil {+            item.sourceAudioStartPTS = sourceAudioStartPTS+        }         if let sourceEndPTS, sourceEndPTS.isFinite {             item.sourceEndPTS = max(item.sourceEndPTS ?? sourceEndPTS, sourceEndPTS)         }diff --git a/liveAPP/Capture/SegmentedRecordingWriter.swift b/liveAPP/Capture/SegmentedRecordingWriter.swiftindex dd2255e..8e96e3d 100644--- a/liveAPP/Capture/SegmentedRecordingWriter.swift+++ b/liveAPP/Capture/SegmentedRecordingWriter.swift@@ -119,6 +119,8 @@ actor RecordingSegmentWriter {     private var lastVideo: CMTime = .invalid     /// 本段最後成功寫入的音訊 PTS;尚未寫入時為 invalid。     private var lastAudio: CMTime = .invalid+    /// 本段第一個實際寫入的音訊 PTS;用來量測檔案 A/V 起點。+    private var firstAudio: CMTime = .invalid     /// 最後一次報告的時間戳(秒)。     private var lastReport = -Double.infinity     /// 本段最近成功讀取的檔案大小;讀取失敗時保留,不以零覆寫。@@ -205,6 +207,7 @@ actor RecordingSegmentWriter {             lastVideo = pts             videoCount += 1         } else {+            if !firstAudio.isValid { firstAudio = pts }             lastAudio = pts             audioCount += 1             audioEnd = sample.duration.isValid ? pts + sample.duration : pts@@ -352,6 +355,7 @@ actor RecordingSegmentWriter {             id,             sourceStartPTS: sourceStart,             sourceEndPTS: persistSourceRange ? sourceEnd : nil,+            sourceAudioStartPTS: persistSourceRange && firstAudio.isValid ? firstAudio.seconds : nil,             persist: persistSourceRange)         await library.update(id, phase: phase, duration: duration, bytes: bytes,             message: "可控錄製 · 第 \(number) 段 · \(bitrate) · 寫入端丟棄 \(dropped) 個樣本" + sizeWarning)