AI 工具 17 min read ★ Featured

錄教學影片時,游標為什麼錄不進去?一個 Claude Code 錄影 skill 的誕生

想讓 AI 操作瀏覽器順便錄成教學影片,結果撞到作業系統層級的限制:單一視窗的錄影裡,滑鼠游標不可能出現。macOS `screencapture -l <windowid>` 能在視窗被完全蓋住時照錄乾淨內容(讀 window server backing store),但同樣的機制決定了它永遠沒有游標——游標是系統畫在所有視窗「之上」的獨立圖層,不屬於任何視窗。這篇記錄我怎麼查清這條物理限制、然後換掉前提:用 Playwright 錄自己起的瀏覽器,在頁面注入一顆 DOM 假游標(CSS transform 平滑移動 + 點擊漣漪),一次解掉「要游標/要畫面乾淨/要能同時做別的事」三難。最花時間的不是畫游標,是驗證它真的有出現——抽影片畫格檢查害我對著幻覺改了三輪程式碼。已開源成 MIT 授權的 Claude Code skill。

錄教學影片時,游標為什麼錄不進去?一個 Claude Code 錄影 skill 的誕生
本文目錄 · 10

# 錄教學影片時,游標為什麼錄不進去?一個 Claude Code 錄影 skill 的誕生

我想讓 AI 幫我操作瀏覽器,順便把過程錄成教學影片。聽起來只是「開個螢幕錄影」的小事,實際做下去卻撞到一個作業系統層級的限制:在單一視窗的錄影裡,滑鼠游標不可能出現。

不是 macOS 忘了做這個功能,是結構上做不到。查清楚這件事之後,解法反而變得清楚——既然抓不到真的游標,那就在頁面裡畫一顆假的。

最後這套東西被我包成一個 Claude Code skill,已經開源:github.com/yanchen184/browser-record。這篇講的是中間那段推導,以及三個模式各自的取捨。

TL;DR:想錄乾淨的網頁教學影片會遇到三難:要游標、要畫面乾淨、要錄的時候能做別的事。macOS 全螢幕錄影有游標但吃掉整個桌面;單視窗錄影(screencapture -l )能在視窗被完全蓋住時照錄,但結構上不可能有游標——它讀的是視窗自己的 backing store,而游標是系統畫在所有視窗之上的獨立圖層。解法是換前提:用 Playwright 錄瀏覽器內容,在頁面裡注入一顆 DOM 假游標(平滑移動 + 點擊漣漪)。三者兼得。已開源成 Claude Code skill。

重點速覽

  • 問題:錄網頁教學影片,想同時要「看得出點了哪裡」「畫面乾淨」「錄的時候我能做別的事」,三者用內建工具無法兼得。
  • 關鍵發現:macOS screencapture -l 可以在視窗被其他視窗完全蓋住時照錄乾淨內容(讀 window server 的 backing store),但同樣的原理決定了它永遠不會有游標——游標不屬於任何視窗。
  • 解法:不要捕捉真游標。用 Playwright 錄自己起的瀏覽器,addInitScript 注入一顆固定定位的 DOM 圓點當游標,用 CSS transform transition 做平滑移動,點擊時放 @keyframes 漣漪。
  • 最難的坑不是畫游標,是驗證它有沒有出現:注入的節點會被框架 hydration 清掉,而「抽影片畫格來檢查」不可靠——隨便挑的時間點很可能落在游標移動的空檔,看不到不代表沒有。正解是在點擊那一瞬間存證截圖。
  • 成果:開源 skill,三種模式一支腳本,clone → npm install 就能用。
  • 三難:游標、乾淨、不占螢幕
  • 被蓋住也能錄:backing store 的意外收穫
  • 為什麼游標抓不到
  • 換個前提:畫一顆假的
  • 三種模式怎麼選
  • 最難的不是畫游標,是驗證它真的在
  • 踩過的坑
  • 安裝與使用
  • FAQ

三難:游標、乾淨、不占螢幕

錄一段「怎麼操作這個網站」的教學影片,我要的東西其實只有三個:

  • 看得出點了哪裡——沒有視覺指示,學員只會看到畫面突然跳掉

  • 畫面乾淨——不要錄進我的桌布、Dock、其他視窗、跳出來的通知

  • 錄的時候我可以做別的事——錄影是背景工作,不該把我的電腦綁架十分鐘
  • 用內建工具,這三個湊不齊。全螢幕錄影(Cmd+Shift+5)滿足第 1 點,但要求瀏覽器攤在桌面最上層,於是第 2、3 點全破。我開始翻 screencapture 的參數,想看看有沒有辦法只錄一個視窗。

    被蓋住也能錄:backing store 的意外收穫

    screencapture 有個 -l 參數可以指定 window id,配上 -v(錄影):

    screencapture -x -v -V 15 -l <windowid> out.mov

    我實測的時候順手把 iTerm 整個蓋在 Chrome 上面,想說錄出來大概是一片黑或錄到 iTerm。結果打開影片——乾乾淨淨的 Chrome 內容,完全沒有 iTerm 的痕跡。

    原理是 macOS 的 window server 給每個視窗各自維護一份 backing store(自己的繪圖快取)。畫面上你看到誰蓋住誰,是合成階段的結果;而 -l 直接去讀那個視窗自己的快取,繞過了合成。所以遮擋對它沒有意義。

    這一條直接解掉了第 2、3 點:畫面乾淨,而且錄製期間我可以把視窗丟到後面繼續做別的事。

    有個例外要記住:縮到 Dock 就錄不到。最小化會讓 backing store 停止更新,蓋住可以,縮小不行。另外 window id 每次開視窗都會變,一定要動態抓,不能寫死。

    為什麼游標抓不到

    高興沒多久,我發現錄出來的影片裡沒有滑鼠游標。翻遍 screencapture 的 man page 也沒有相關開關。

    一開始以為是漏了參數,後來才想通:這跟上面那個「被蓋住也能錄」是同一件事的兩面

    -l 讀的是「某個視窗自己的 backing store」。而滑鼠游標不屬於任何視窗——它是系統合成在所有視窗之上的一個獨立圖層。所以:

    • 你讀單一視窗的 backing store → 遮擋物不會進來(優點)
    • 同樣地,游標也不會進來(代價)
    這不是缺功能,是這個機制的必然結果。 想要真游標,就只能回去用全螢幕錄影(ffmpeg 的 avfoundation 有 -capture_cursor 1),連同它的所有缺點一起接受。

    想通這點之後,我不再試圖「把游標弄進來」——那條路是封死的。

    換個前提:畫一顆假的

    既然抓不到系統游標,那就換掉「錄使用者的螢幕」這個前提:用 Playwright 起一個自己的瀏覽器,錄它的內容,然後在頁面裡注入一顆自己畫的游標。

    這個轉向一次解掉全部三難:

    • Playwright 的 recordVideo 錄的是瀏覽器內容,跟桌面完全無關 → 畫面乾淨、不占螢幕
    • 游標是我自己畫的 DOM 節點 → 想讓它多明顯就多明顯,還能加點擊動畫
    游標本身就是一顆固定定位的圓點,靠 CSS transition 移動:
    #__demo_cursor {
      position: fixed; top: 0; left: 0;
      width: 22px; height: 22px;
      border-radius: 50%;
      background: rgba(255,64,96,0.85);
      border: 2px solid #fff;
      z-index: 2147483647; pointer-events: none;
      transition: transform var(--cur-dur, 900ms) cubic-bezier(.25,.8,.3,1);
    }

    pointer-events: none 是必要的,否則這顆點會擋住底下元素的點擊。移動就是改 transform,讓 CSS 自己補間:

    window.__cursor = {
      move(x, y, dur) {
        dot.style.setProperty('--cur-dur', dur + 'ms');
        dot.style.transform = translate(${x}px, ${y}px);
      },
      click(x, y) {
        ripple.style.animation = 'none';
        void ripple.offsetWidth;          // 強制 reflow,動畫才能重播
        ripple.style.animation = '__demo_ping 600ms ease-out';
      },
    };

    點擊的漣漪是另一顆同心圓,@keyframesscale(1) 放到 scale(3.4) 同時淡出。那句 void ripple.offsetWidth 是重點——不強制 reflow,連續兩次點擊的第二次動畫不會重播。

    實際操作時,「點一個元素」被拆成:算出元素中心座標 → 游標平滑滑過去 → 放漣漪 → 才真的觸發 click()。慢下來反而是特色,教學影片本來就需要讓觀眾跟得上。

    browser-record 實際錄出來的效果,紅色假游標移動到目標並放出點擊漣漪

    三種模式怎麼選

    最後三種做法我都留著了,因為它們各有沒辦法互相取代的場景:

    A 全螢幕B 單視窗C Playwright + 假游標
    真實滑鼠游標❌(有視覺指示器)
    視窗被蓋住也能錄✅(根本不占螢幕)
    畫面乾淨
    看得出點了哪裡✅ 漣漪動畫
    錄製時能做別的事
    平台macOSmacOS跨平台
    預設用 C。 只有兩種情況該回去用 A:必須呈現真實滑鼠軌跡(例如你要示範的就是滑鼠手勢本身),或者要錄的東西不在瀏覽器裡(IDE、桌面軟體)。

    B 的定位比較窄,但在「要錄非 Playwright 控制的那顆瀏覽器」時很有用——比如你要錄的是自己手動操作、有登入狀態的那個視窗。

    最難的不是畫游標,是驗證它真的在

    這是整件事我花最多時間的地方,也是唯一值得單獨拿出來講的教訓。

    假游標是注入的 DOM 節點,而現代網站的框架在 hydration 階段會接管並重建 DOM——注入的節點可能就這樣被掃掉。錄影腳本跑完、印出 OK duration=27.5s,不代表影片裡真的有游標。

    我第一次驗證的方法是:從影片抽幾張畫格出來看。結果連續三次都「看不到游標」,我以為是注入失敗,改了三輪程式(加重試、加 MutationObserver、加延遲),問題依舊。

    後來寫了一個最小重現腳本,直接在頁面裡問「#__demo_cursor 這個節點現在存在嗎」,答案是存在,而且位置正確。游標一直都在,是我的驗證方法錯了:

  • 我隨手挑的時間點,剛好落在游標兩次移動之間的空檔

  • 更蠢的是,游標初始 transformtranslate(-50px,-50px)(畫面外),而我那份劇本的第一個動作是捲動、不是點擊——所以開頭十秒游標本來就在畫面外
  • 「抽畫格看不到」推不出「游標沒出現」。 這跟看 log 有 success 就當作修好了是同一類錯誤:用一個不對的觀測點,得到一個假結論,然後對著幻覺改了三輪程式碼。

    正確做法是讓驗證發生在確定該有東西的那一刻。所以 --verify 參數會在每次點擊的瞬間存一張截圖:

    const clickAt = async (selector) => {
      const { x, y, el } = await moveTo(selector);
      await page.evaluate(([x, y]) => window.__cursor?.click(x, y), [x, y]);
      await sleep(180);
      if (has('verify')) {
        await page.screenshot({ path: join(WORK_DIR, verify_click_${++shotSeq}.png) });
      }
      await el.click();
    };

    交付前打開其中一張,用眼睛確認紅點真的壓在目標元素上。這才叫驗過。

    順帶一提,初始位置在畫面外那個設計也順手修了:goto() 之後會自動把游標帶到畫面中央,不管劇本第一個動作是什麼。

    踩過的坑

    除了上面那個,還有幾個值得記:

    • waitUntil: 'networkidle' 會 timeout——多數站有長連線(留言系統、分析腳本),永遠不會 idle。用 domcontentloaded + waitForLoadState('load')。我在自己的部落格上卡了 60 秒才想通。
    • 注入節點要掛 document.body,不要掛 documentElement——掛在 documentElement 上比較容易被框架的 DOM 重建掃掉。
    • context.close() 才會 flush 影片——直接 browser.close() 會拿不到檔案,這是 Playwright 的行為,不是 bug。
    • ffmpeg -list_devices 一定回非零 exit code——它把 "" 當成輸入檔所以報錯。在 set -euo pipefail 的腳本裡直接 pipe,整個 script 會靜默死掉(exit 251,沒有任何錯誤訊息)。必須用 || true 隔離。這個坑我 debug 了很久,因為它什麼都不印。
    • avfoundation 的裝置清單有兩組方括號——格式是 [AVFoundation indev @ 0x...] [3] Capture screen 0,用 awk -F'[][]' '{print $2}' 會抓到前面的 log prefix 而不是裝置編號。要用 sed -nE 's/.*\[([0-9]+)\] Capture screen 0.*/\1/p'。而且這個編號每台機器不一樣,不能寫死。

    安裝與使用

    skill 已經開源,MIT 授權:

    git clone https://github.com/yanchen184/browser-record.git ~/.claude/skills/browser-record
    cd ~/.claude/skills/browser-record
    npm install
    npx playwright install chromium

    裝在 ~/.claude/skills/ 底下 Claude Code 會自動載入,之後直接說「幫我錄一段這個網站的操作影片」就會觸發。依賴裝在 skill 自己的目錄裡,所以從任何專案跑都行,不會污染你的專案。

    手動跑的話,先寫一份劇本:

    export const pace = { cursorMove: 900, afterClick: 1400, readPause: 2200 };
    

    export default async ({ goto, clickAt, scrollBy, type, pause }) => {
    await goto('https://example.com');
    await scrollBy(500);
    await clickAt('a[href="/docs/"]');
    await type('input[name=q]', '搜尋關鍵字');
    await pause();
    };

    然後:

    node ~/.claude/skills/browser-record/scripts/record.mjs ./my-demo.mjs \
      --out ./demo.mp4 --verify

    pace 那組數字是給教學影片用的節奏,覺得太慢可以自己調。

    FAQ

    Q:假游標看起來會不會很假?

    會,它就是一顆紅點,不是 macOS 的箭頭。但教學影片要的是「觀眾知道現在點了哪裡」,一顆有漣漪動畫的紅點在這件事上比真實箭頭更清楚——真箭頭在 1280 寬的影片裡其實很難看見。

    Q:可以錄需要登入的頁面嗎?

    可以,劇本裡的 page 是原生 Playwright page,要帶 cookie、走登入流程都行。但如果你要錄的是已經登入好的那顆手動瀏覽器,那應該用模式 B 而不是 C。

    Q:Windows / Linux 能用嗎?

    模式 C 可以,它只依賴 Playwright 和 ffmpeg。模式 A、B 是 macOS 專屬(screencapture 和 avfoundation)。

    Q:為什麼不用現成的螢幕錄影軟體?

    如果你是手動操作、要錄自己的畫面,現成軟體更好。這個 skill 的場景是讓 AI 執行一段可重複的操作流程並錄下來——改版之後重跑一次劇本就得到新影片,不用重錄。

    Q:影片可以直接發嗎?

    輸出是 mp4(H.264 / yuv420p),可以直接上傳。README 裡那張 GIF 就是從輸出的 mp4 轉的。

    author
    陳彥彤

    AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

    support

    覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

    related

    相關文章

    [工作流] · 10min
    AI 逼問法 vs 需求訪談:grilling skill 三代拆解
    mattpocock/skills(183k star)裡的 grilling 系列 skill,把「AI 動手前先逼問使用者」拆成三代演化:一次一題、一次一輪、邊問邊產文件。拆開三份 SKILL.md 原始碼,對照人類需求訪談「一次一題 vs 一次一輪」的老權衡,附今天這篇文章寫作過程本身當真實案例。
    [工作流] · 16min
    166k Star Skill 庫拆解:4 個 AI Agent 老問題,各自的解法
    GitHub 166k star 個人 Skill 庫實際拆解:6 個核心 skill 對應 4 個 AI agent 常見失控問題,Matt Pocock 怎麼用 vertical slice 逼 agent 先問清楚再動手。
    [工程實作] · 19min
    我怎麼把別人的 skill 吸進自己產線,做成更強的 skill:一次語意標註實作覆盤
    看到好用的 skill,大部分人裝來用;如果你有自己的產線,更值錢的做法是拆它的零件焊進你的流程。我原本有一套截圖審查+手冊產線(/screenshot-review),但手冊圖是「乾圖」沒有圖上標註——這正是 app-screenshots 的 annotate.js 補得上的洞。本文覆盤:我怎麼把它拆進產線,過程撞到一個架構級的坑(標註截不進圖=OVERLAY GONE:agent-browser 每個 CLI 指令是獨立 CDP 呼叫,eval 注入的 overlay 下一指令就蒸發),查根因、用 batch 把 open→eval→screenshot 綁進同一 session 解掉、封裝成 annotate-shot.sh、寫回自己的 skill。全程真的跑真的驗,附 batch 解法的真實成果圖與踩坑總表。