Jakeuj's Notes Help

線上課程背景播放 JavaScript:套用、還原與計時驗證

讓教材正常播放後,在課程主頁的 Console 執行下方完整 JavaScript,即可暫時停用頁面隱藏時的計時處理與定期回應確認。這段程式保留實際的物件及函式名稱,供之後直接複製使用;僅適用具備相同 CourseReader 實作的一般網頁教材,不是所有學習平台通用的腳本。

這份筆記整理一次 2026-10-19 的瀏覽器觀察與暫時修改。公司、網域、課程、帳號及紀錄識別碼已移除;套用與還原程式保留原名稱,其餘 API 動作及 JSON 則使用概念範例。

直接複製使用

執行前先確認教材正在正常播放,而且沒有等待回應的確認視窗。Console 的執行環境要選課程主頁 Home.html 的最上層 frame,不能選內嵌影片或閱讀器 iframe。

程式會檢查 readerStatus === 3 與 _ContentType === "0" 。不符合時會停止;若平台改版後找不到物件或函式,也要重新確認實作,不能只刪掉檢查繼續執行。

(() => { const r = window.oUIManager?.CourseReader; const key = "__courseBackgroundPatch"; if (!r || r.readerStatus !== 3) { throw new Error("請先讓教材正常播放,再執行。"); } if (r._ContentType !== "0") { throw new Error("此修改僅適用剛才分析的一般網頁教材。"); } if (window[key]) { console.info("已套用,無須重複執行。"); return; } const original = { visibilitychangeFn: r.visibilitychangeFn, enableResponse: r._enableResponse }; // 必須先停止既有確認計時器,再停用確認功能。 r._stopCountResponseTime(r._coffer.data("currentWinUID")); r._enableResponse = false; // 忽略後續頁面可見性變更。 r.visibilitychangeFn = function () {}; window[key] = { restore() { r.visibilitychangeFn = original.visibilitychangeFn; r._enableResponse = original.enableResponse; if (window.oUIManager?.CourseReader === r && r._settings) { r.visibilitychangeFn(null); } delete window[key]; console.info("已還原課程偵測。"); } }; console.info("已套用暫時修改;未改寫累積時數。"); })();

看到「已套用暫時修改;未改寫累積時數。」表示腳本已執行到最後。重複執行會顯示「已套用,無須重複執行。」,避免覆蓋用來還原的原始函式。

這段程式停用確認機制,沒有自動按確認,也沒有修改累積秒數或直接提交課程完成狀態。頁面不被主動暫停,仍不代表瀏覽器一定允許長時間背景執行,或伺服器一定認列全部時間。

還原與下次使用

在同一個主頁 Console 執行:

window.__courseBackgroundPatch?.restore();

還原會恢復原本的可見性處理函式與回應確認設定,再依當前頁面狀態同步計時。若原本啟用回應確認,先前已清除的倒數會重新開始,並非恢復舊倒數的剩餘秒數。

重新整理也會清除暫時修改,但可能中斷正在進行的閱讀,應先透過原介面正常回存。準備換教材時,先還原、正常離開,再於下一份符合條件的教材播放後重新套用;同頁換課時,舊的 patch 標記不一定會自動清除。

判斷是否成功記錄時數,仍要比對閱讀計時、回存請求及離開教材後重新查詢的紀錄。畫面上的時間持續增加,或 API 回傳 HTTP 200,都不足以證明課程已通過。

先分清楚三種時間與進度

資訊

代表意義

不能直接推論的結果

影片播放位置

播放器目前播到第幾秒

平台認列的閱讀秒數

閱讀視窗計時

前端依閱讀狀態計算的時間

已成功回存的時間

重新查詢的閱讀紀錄

伺服器回傳的累積結果

所有活動均已通過

課程完成狀態

時數、測驗、問卷等條件的綜合結果

單次回存請求的成功與否

觀察案例中,教材被歸類為一般網頁,內容嵌入外部影片。回存的播放位置欄位為空,沒有看到依外部播放器進度回報的資料。因此,不能將播放位置與平台計時視為同一件事;這也不表示所有影音教材都使用相同實作。

從 Network 建立證據

先被動觀察正常閱讀流程,再考慮變更。Chrome DevTools 的 Network 可保留跨頁請求,並檢視 Payload、Response 與 Initiator;Initiator 能協助追到發出請求的程式位置。 Chrome DevTools Network reference

  1. 記錄課程列表的既有時數、閱讀視窗計時與當前時間。

  2. 保留正常播放狀態,捕捉至少一次週期回存。

  3. 比對請求方法、端點、欄位名稱、狀態碼與業務結果。

  4. 在載入的 JavaScript 中追查計時啟停、週期回存及離開結算。

  5. 把「實際請求」、「程式碼分支」與「推論」分開記錄。

Resource Timing 可以補充頁面先前載入哪些 URL、何時開始請求,但不能取代 Request Body 與 Response Body。若開始觀察前就已開啟教材,起始回應可能不在新啟用的 Network 記錄裡,不應為了補抓而直接重整正在閱讀的頁面。

公開筆記只保留必要欄位與去識別化範例。HAR、Copy as cURL 或完整表單內容可能包含登入憑證、員工資訊與組織資料,不能把原始輸出直接放進文章。

API 的角色與回存內容

同一個系統通常有數種不同用途的請求。以下名稱僅用來表達此次觀察到的角色:

示意動作

用途

本次觀察程度

record-recent-material

記錄最近閱讀的教材

程式碼與資源時序

start-reading-session

建立或取得閱讀紀錄,載入既有時間

程式碼與資源時序;未取得起始回應

update-reading-session

週期回存、暫停/恢復、離開結算

實際 POST 與回應,以及程式碼

keep-session-alive

維持登入或線上狀態

程式碼與資源時序

登入保活不等於閱讀時數回存。更新動作名稱即使帶有 finish,也可能只是在結算一段閱讀工作階段,並非把整堂課標記完成。

實際週期請求可抽象為下列資料;欄位名稱與結構已改寫,原請求使用表單編碼:

{ "courseId": "example-course", "activityId": "example-activity", "readingSessionId": "example-session", "lastPosition": "", "finalize": false, "paused": false }

值得注意的是,觀察到的回存沒有直接包含前端累積秒數。這支持「伺服器可能結合工作階段、請求時間與暫停狀態計算時數」的推論,但後端原始碼不可見,無法確認精確算法、時間上限或防重播規則。

正常回存收到 HTTP 200;還原系統自訂的回應編碼後,業務結果表示成功且未進入閒置狀態。該回應沒有累積時數或課程通過旗標,所以仍須另外查詢紀錄。

前端計時與週期回存

案例中的顯示計時器每 100 毫秒執行一次,以啟動時的累積值加上當下時間差,更新閱讀視窗。回存則由同一個迴圈累計 3,000 次 callback 觸發,在前景且未受節流時約為 5 分鐘。

這裡有兩個不同的計算基礎:

  • 顯示時間使用日期時間差。

  • 回存頻率使用 callback 次數。

背景執行若受到計時器節流,兩者可能出現差異:畫面下一次更新時補上經過時間,但回存要等 callback 次數達標。瀏覽器確實可能限制背景計時器,具體行為也受版本、音訊播放與其他條件影響,不能把 5 分鐘視為嚴格保證。 MDN Page Visibility API

暫停、恢復及正常離開各有狀態處理;正常離開會使用結算旗標。程式也有離開頁面時使用 fetch 搭配 keepalive 的分支,但這只能說明它嘗試在離開時送出,不能取代實際送達與寫入驗證。

頁面可見性不是視線偵測

document.visibilityState 與 visibilitychange 描述頁面是否可見,不能判斷使用者眼睛看向哪裡。切換分頁、最小化與切換應用程式也不能一概視為相同事件,應讀取實際狀態; blur/ focus 與可見性是不同訊號。 MDN Page Visibility API

此次一般網頁教材啟用了可見性檢查。程式分支在 hidden 時停止閱讀及回應確認計時,並回存;回到 visible 後再啟動。這些是應用程式的行為,不是 Page Visibility API 自動替平台執行的功能。

以下診斷片段只讀取狀態,不修改偵測或課程資料:

console.table({ visibility: document.visibilityState, hidden: document.hidden, focused: document.hasFocus(), observedAt: new Date().toISOString() });

回應確認是什麼

另一套機制是定期要求使用者回應。案例當時的設定為每 20 分鐘出現一次確認視窗,限 30 秒回應;這些數值是當次執行參數,不是所有平台的固定規則。

階段

程式處理

到達確認時間

停止閱讀計時、回存並顯示確認視窗

使用者按確認

恢復計時,重新啟動下一輪確認計時

超過回應期限

顯示停止記錄提示,關閉閱讀視窗

已檢視的確認按鈕回呼主要是關閉視窗與恢復計時,沒有看到獨立的「本人已確認」API。不過,狀態恢復仍可能經由共用回存函式送出資料,因此不能解讀成「按確認完全不會影響後端」。

暫時修改與自動確認的差異

本次曾在教材正常閱讀、沒有等待確認的狀態下,進行可還原的前端暫時修改。流程是保存原函式與設定,先停止既有回應計時器,再停用後續確認,並以空函式取代頁面可見性處理。最後驗證函式已替換、確認計時器已停止,以及閱讀計時仍在增加。

先停止計時器的順序很重要:若停止函式本身會檢查啟用旗標,先關閉旗標可能使它提早返回,留下仍在執行的舊計時器。

做法

對流程的影響

能否證明伺服器認列

停用前端確認

不再依原週期彈出視窗

不能;僅確認本機狀態已修改

自動按下確認

理論上會經過原視窗及恢復回呼

不能;本次未實作或驗證

正常手動確認後重新查詢

保留原流程,再檢查持久化結果

可驗證該段紀錄,仍需另看整堂課條件

自動按確認比較接近原本前端流程,不等於比較容易被後端接受。尚未觀察到後端對兩種方式的差別,也沒有證據支持「有按確認就一定認列」。

此次修改沒有改寫累積秒數,也沒有直接提交課程完成狀態。只驗證到前端閱讀狀態維持、短時間內顯示計時繼續增加;沒有完成長時間背景播放或後端認列驗證。

還原時應恢復原處理函式與旗標,再依當前可見狀態同步計時。重新整理也會重新載入原程式,但可能中斷當前閱讀;先完成正常回存再重整。這類頁面物件修改不是跨重整、跨教材或跨登入的持久設定,不能假設換課後仍有相同行為。

如何驗證伺服器實際記錄

先以正常手動流程建立基準,並將以下步驟用於有權測試的環境。需要比較背景行為時,每次只改變一個條件,避免同時切換可見性、播放器狀態與回應機制。

  1. 記錄重新查詢得到的起始閱讀時間與課程狀態。

  2. 記錄實際經過時間,以及暫停、確認、恢復與回存的時間點。

  3. 檢查週期回存是否收到預期的業務成功結果。

  4. 透過原介面正常回傳並離開教材。

  5. 重新查詢課程列表,計算伺服器時間增量。

  6. 分別檢查教材門檻、其他教材、測驗與問卷,避免只看總進度。

可用下列公式整理觀察結果:

伺服器認列增量 = 結束後重新查詢的秒數 - 開始前重新查詢的秒數

若增量與前端顯示不一致,先排除列表未更新、回存失敗、暫停期間、時間取整及後端結算延遲,再判斷是否有額外規則。無論停用偵測或自動確認,都不能僅憑畫面計時斷言有效。

證據範圍與未完成事項

項目

本次結果

正常週期回存

捕捉到實際請求與業務成功回應

前端計時與偵測邏輯

已讀取當次載入的程式及設定

暫時修改

已套用,短時間前端狀態與計時驗證通過

自動確認

僅討論可行性,未實作、未執行

修改後的伺服器時數

尚未透過離開後重新查詢核對

直接呼叫 API 完成課程

未測試,不能宣稱可行

後端計時與防重播規則

無法由前端觀察完整確認

19 October 2026