app · 55140b1

Dev index Recent changes / versions Changelog About TODO

修正通知中心造成的圖表與 PiP 停滯

55140b1 · 2026-11-11 21:55 · app
Docs/ChangeHistory.mdTODO.mdliveAPP/FileLogView.swiftliveAPP/OtherView.swiftliveAPP/PIPService.swift
commit 55140b18a754c30cbf8e1296d2bbe622daff3e6fAuthor: Nuclear0709 <agp05432@gmail.com>Date:   Wed Nov 11 21:55:52 2026 +0800     修正通知中心造成的圖表與 PiP 停滯 diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.mdindex af7fdc9..2a2e3d8 100644--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -20,6 +20,7 @@ - 新增 base64 `data:image/...` 圖片來源:聊天頭像/禮物、行內 emoji、贊助橫幅與通知共用本機解碼,不進 URLSession;限制單張 256 KiB、單訊息 768 KiB、4 MP,拒絕 SVG/非 base64/超量,HTTP URL 仍保留給大圖。日誌以長度遮蔽 base64,TTS 與通知使用清洗後文字。 - TTS 過濾頁改為分組卡片:測試與設定/基本清理/排除關鍵字/替換關鍵字;匯入匯出改為圖示按鈕,重複字元選單加上可見標題與說明,並在測試結果為空時明示「會被過濾,不朗讀」。 - 診斷新鮮度不再以 `Int(age)` 截成 `0 秒`:新增 `DiagnosticAge`,顯示「剛剛/0.4 秒/8.6 秒/12 秒/1 分 15 秒」;`CapturePipelineStatus`、StreamDiagnostics 與 ReplayKit 新鮮度改用實際目前時間計算,避免 `sampleDate` 掩蓋過期資料。串流摘要的 health 也改為獨立顏色(healthy 綠、failure 紅、其餘橘/灰)。+- 通知中心/控制中心造成的 `scenePhase=.inactive` 不再停止 DeviceView/FileLogView 的週期更新,只有真正 `.background` 才停止;避免下拉面板時圖表與日誌頁看起來卡住。PiP 回到 `.active` 時會 flush 可能失效或空白的 `AVSampleBufferDisplayLayer` 並重畫;render 遇到 `.failed` 先 flush 再 enqueue,降低 PiP 黑畫面卡住的機率。 - ReplayKit 的 `StreamDiagnosticsSnapshot` 現在也會帶 `audioTracks`;主 App 的「Audio Pipeline · 混音輸出」在 ReplayKit 與 ScreenCaptureKit 都能看到 App/Mic 的 AVCon、ring buffer、align 與 RMS。轉換邏輯集中在 `StreamDiagnosticsSnapshot.audioTrackDiagnostics(from:)`,避免兩條路徑各寫一份。 - `[IdleResources]` 新增 `warmIdleBaselineMB`/`deltaWarmIdleMB`:每次進入擷取前記錄最近一次已穩定的 idle footprint,包含 DeviceInfo/Settings/Audio/PiP 等 UI 常駐成本;`coldIdleBaselineMB` 保留作冷啟動對照,避免把 100 MB warm idle 誤算成串流增量。 - 修正 DeviceView 與 FileLogView 的週期 task:不再使用 `.task(id:)` 由布林狀態重啟,改成頁面可見且前景時才啟動單一 `Task`,離頁/背景立即取消;避免隱藏分頁或重複 view 持續每秒取樣、刷新日誌而累積記憶體。診斷 log 的 sampling start/stop 會用來確認沒有重複 task。diff --git a/TODO.md b/TODO.mdindex 88e5d6f..45a1b12 100644--- a/TODO.md+++ b/TODO.md@@ -54,6 +54,7 @@ API 遷移屬後續技術評估,不作為以上功能完成的條件;需要 - [x] 設備頁顯示實體介面上行/下行差分,註明包含其他 App 流量。 - [x] **ScreenCaptureKit 混合輸出圖表實機驗收**:使用者提供 2026-10-20 15:50 設備頁截圖,Video/Audio Pipeline 均顯示 ScreenCaptureKit、歷史曲線與輸出速率;兩張截圖的更新時間與影片 FPS 有變化,本機來源佇列及混音摘要亦可見。 - [ ] **其餘管線圖表驗收**:ReplayKit 圖表、停止與重啟、本地可控錄製、回前景與介面切換;截圖中的 ReplayKit 尚未收到資料,不作為該來源驗收。+- [ ] **通知中心/控制中心回歸**:下拉與關閉面板後 DeviceView 圖表應持續更新、FileLogView 不卡住;PiP 在回到 active 後不應黑畫面。已改為僅 `.background` 停止週期 task,並在 active 時 flush/重畫 PiP layer;待實機驗證。 - [x] 底層公開編碼後格式快照:從實際壓縮樣本更新影片 codec/寬高/色彩、音訊 codec/取樣率/聲道/封包資訊,定義 session/generation 與重連重設語意。 - [x] App/ReplayKit 接入上述快照,替換「實際編碼輸出格式」的缺口說明;不以設定或文字日誌冒充實測。 - [x] 底層 7ce4304b 前置工作已由使用者於 2026-10-20 確認完成;主 App 兩份鎖定檔均接入該版本。diff --git a/liveAPP/FileLogView.swift b/liveAPP/FileLogView.swiftindex 9a31c07..e922f30 100644--- a/liveAPP/FileLogView.swift+++ b/liveAPP/FileLogView.swift@@ -280,10 +280,11 @@ struct FileLogView: View {             startRefreshIfNeeded()         }         .onChange(of: scenePhase) { phase in-            if phase == .active, isVisible {-                startRefreshIfNeeded()-            } else {+            // inactive 可能是通知中心/控制中心;只有真正進背景才停止刷新。+            if phase == .background {                 stopRefresh()+            } else if isVisible {+                startRefreshIfNeeded()             }         }         .onDisappear {@@ -295,7 +296,7 @@ struct FileLogView: View {      /// 單一刷新 task;頁面切換或進背景時停止,避免隱藏分頁持續每秒讀取。     private func startRefreshIfNeeded() {-        guard refreshTask == nil, isVisible, scenePhase == .active else { return }+        guard refreshTask == nil, isVisible, scenePhase != .background else { return }         refreshGeneration &+= 1         let generation = refreshGeneration         refreshTask = Task { @MainActor indiff --git a/liveAPP/OtherView.swift b/liveAPP/OtherView.swiftindex 99b409a..2131b46 100644--- a/liveAPP/OtherView.swift+++ b/liveAPP/OtherView.swift@@ -609,10 +609,11 @@ struct DeviceView: View {             startSamplingIfNeeded()         }         .onChange(of: scenePhase) { phase in-            if phase == .active, isVisible {-                startSamplingIfNeeded()-            } else {+            // 通知中心/控制中心只會讓 App 進入 inactive;不要因此停掉圖表取樣。+            if phase == .background {                 stopSampling()+            } else if isVisible {+                startSamplingIfNeeded()             }         }         .onDisappear {@@ -1121,7 +1122,7 @@ struct DeviceView: View {      /// 單一採樣 task;避開 `.task(id:)` 在頁面切換時重複啟動。     private func startSamplingIfNeeded() {-        guard samplingTask == nil, isVisible, scenePhase == .active else { return }+        guard samplingTask == nil, isVisible, scenePhase != .background else { return }         cpuInfo = SystemCPU()         diskIO = SystemDiskIO()         networkTraffic = NetworkTrafficSampler()diff --git a/liveAPP/PIPService.swift b/liveAPP/PIPService.swiftindex e133eb7..84547ad 100644--- a/liveAPP/PIPService.swift+++ b/liveAPP/PIPService.swift@@ -1219,6 +1219,12 @@ final class PIPService: NSObject, ObservableObject, @unchecked Sendable {             return false         } +        if displayLayer.status == .failed {+            // AVSampleBufferDisplayLayer 在 App inactive/系統切換後可能進入 failed;+            // flush 後再 enqueue,否則 isReadyForMoreMediaData 會一直 false 形成黑畫面。+            PIPLogTo("displayLayer failed: \(String(describing: displayLayer.error));flush 後重試")+            displayLayer.flush()+        }         guard displayLayer.isReadyForMoreMediaData else {             backpressureSkips += 1             decayFPSIfNeeded()@@ -1311,6 +1317,9 @@ final class PIPService: NSObject, ObservableObject, @unchecked Sendable {      func appWillEnterForeground() {         isInBackground = false+        // 通知中心/控制中心可能讓 sample-buffer layer 留下空白畫面;+        // 回到 active 時清掉舊畫面,再由 forceRender 補一張新幀。+        displayLayer?.flush()         reAttachDisplayLayerIfNeeded()          if pixelBufferPool == nil && OframeSize.width > 0 && OframeSize.height > 0 {