diff --git a/local-agent/Makefile b/local-agent/Makefile index 31c11ea..0c51462 100644 --- a/local-agent/Makefile +++ b/local-agent/Makefile @@ -181,7 +181,7 @@ vendor-ffmpeg-macos-build: ## macOS:從源碼 build LGPL v3 decoder-only ffmpe --enable-avfoundation \ --enable-indev=avfoundation \ --enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \ - --enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be \ + --enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be,rawvideo \ --enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \ --enable-filter=scale,format,fps,null,anull \ --enable-muxer=image2pipe,image2,null \ diff --git a/local-agent/server/internal/camera/ffmpeg_camera.go b/local-agent/server/internal/camera/ffmpeg_camera.go index 7691acb..272ae81 100644 --- a/local-agent/server/internal/camera/ffmpeg_camera.go +++ b/local-agent/server/internal/camera/ffmpeg_camera.go @@ -223,9 +223,18 @@ func buildCaptureArgsForOS(goos string, cameraIndex int, cameraName string, widt "-i", fmt.Sprintf("/dev/video%d", cameraIndex), }) case "darwin": - // AVFoundation on macOS: -f avfoundation -i "index:none" + // AVFoundation on macOS: -f avfoundation -pixel_format uyvy422 -i "index:none" + // + // 必須明確指定攝影機支援的 input pixel format。攝影機只支援 + // uyvy422/yuyv422/nv12/0rgb/bgr0;不指定時 avfoundation 會嘗試 yuv420p + // → 協商失敗(Input/output error)。指定 uyvy422(攝影機原生格式)→ 攝影機成功打開。 + // + // 位置關鍵:-pixel_format 是 input 選項,必須放在 -i 之前(跟 -framerate/ + // -video_size 同段)。放到 -i 之後會被當成 output 轉碼目標、不解決 input 協商。 + // 對照見根因文件 .autoflow/05-implementation/camera-pixel-format-rootcause.md。 return captureTail([]string{ "-f", "avfoundation", + "-pixel_format", "uyvy422", "-framerate", fps, "-video_size", videoSize, "-i", fmt.Sprintf("%d:none", cameraIndex), diff --git a/local-agent/server/internal/camera/ffmpeg_capture_args_test.go b/local-agent/server/internal/camera/ffmpeg_capture_args_test.go index 79a3fd3..0abfcb4 100644 --- a/local-agent/server/internal/camera/ffmpeg_capture_args_test.go +++ b/local-agent/server/internal/camera/ffmpeg_capture_args_test.go @@ -24,12 +24,15 @@ func TestBuildCaptureArgsForOS(t *testing.T) { wantIndev string // wantInputArg 是 "-i" 後面的值 wantInputArg string + // wantPixelFormat 若非空,斷言 "-pixel_format <值>" 存在且位於 -i 之前(input 選項)。 + wantPixelFormat string }{ { - name: "macOS uses avfoundation with index:none", - goos: "darwin", - wantIndev: "avfoundation", - wantInputArg: "0:none", + name: "macOS uses avfoundation with index:none and uyvy422 input pixel format", + goos: "darwin", + wantIndev: "avfoundation", + wantInputArg: "0:none", + wantPixelFormat: "uyvy422", }, { name: "Windows uses dshow with video=name", @@ -60,6 +63,20 @@ func TestBuildCaptureArgsForOS(t *testing.T) { t.Errorf("input = %q, want %q\nargs: %v", gotInput, tt.wantInputArg, args) } + // macOS avfoundation 必須帶 -pixel_format uyvy422,且它是 input 選項、 + // 必須位於 -i 之前(放到 -i 之後會被當 output 轉碼目標、不解決 input 協商)。 + if tt.wantPixelFormat != "" { + gotPixFmt := valueAfterFlag(args, "-pixel_format") + if gotPixFmt != tt.wantPixelFormat { + t.Errorf("pixel_format = %q, want %q\nargs: %v", gotPixFmt, tt.wantPixelFormat, args) + } + pixIdx := indexOfFlag(args, "-pixel_format") + iIdx := indexOfFlag(args, "-i") + if pixIdx < 0 || iIdx < 0 || pixIdx > iIdx { + t.Errorf("-pixel_format (idx %d) must appear before -i (idx %d)\nargs: %v", pixIdx, iIdx, args) + } + } + // 三平台後段皆須為 image2pipe / mjpeg,MJPEG pipe 架構共用。 if !containsSeq(args, "-f", "image2pipe") { t.Errorf("missing image2pipe output, args: %v", args) @@ -162,6 +179,16 @@ func valueAfterFlag(args []string, flag string) string { return "" } +// indexOfFlag 回傳 args 中第一個等於 flag 的元素的索引;不存在回 -1。 +func indexOfFlag(args []string, flag string) int { + for i, a := range args { + if a == flag { + return i + } + } + return -1 +} + // containsSeq 判斷 args 是否包含連續的 a b 兩個元素。 func containsSeq(args []string, a, b string) bool { for i := 0; i < len(args)-1; i++ { diff --git a/local-agent/vendor/ffmpeg/macos/BUILD.md b/local-agent/vendor/ffmpeg/macos/BUILD.md index 2498334..ea339fd 100644 --- a/local-agent/vendor/ffmpeg/macos/BUILD.md +++ b/local-agent/vendor/ffmpeg/macos/BUILD.md @@ -18,9 +18,20 @@ LGPL static build 來源,採「自 build decoder-only」策略,binary 直接 | Toolchain | Apple clang 16.0.0 (clang-1600.0.26.6), Command Line Tools | | Assembler | nasm 3.01(Homebrew bottle,compiled 2025-10-11) | | Homebrew | 5.1.6 | -| Build date | 2026-08-02(ADR-020 rebuild:加回 avfoundation indev) | +| Build date | 2026-08-03(camera raw capture fix:decoder 白名單加 `rawvideo`) | | Build flags | 見下方 Configure flags 區塊(與 `Makefile` 的 `vendor-ffmpeg-macos-build` target 一致) | +> **camera raw capture fix(2026-08-03)**:decoder 白名單加 `rawvideo`。 +> avfoundation 攝影機吐 raw `uyvy422`(codec = `rawvideo`),要轉成 MJPEG pipe 必須先「解碼 rawvideo」, +> 但先前 decoder-only 白名單沒有 `rawvideo` → `Decoding requested, but no decoder found for: rawvideo` → EOF。 +> 這是 ADR-020(avfoundation indev)之後 camera 白名單的**第二個盲點**:ADR-020 讓攝影機「能被開啟」, +> 但沒處理「攝影機 raw 輸出的解碼」。 +> **未來 rebuild 別漏**:camera raw capture 需 `rawvideo` decoder + `avfoundation` indev **兩者**齊全。 +> `rawvideo` 是 libavcodec 內建 native decoder、LGPL-safe(僅 byte 重排 / format 標記、無演算法),不引入任何 GPL 元件; +> swscale 已 enable(`rawvideo(uyvy422) decode → swscale 轉 → mjpeg encode` 轉碼鏈齊)。 +> 只加 macOS(Windows/Linux 用 BtbN full build 內建 rawvideo,不需改)。 +> 詳見 `.autoflow/05-implementation/camera-pixel-format-rootcause.md`。 + > **ADR-020 變更(2026-08-02)**:configure 加 `--enable-avfoundation` + `--enable-indev=avfoundation`, > 讓 camera 即時推論可用 avfoundation 抓實體攝影機。詳見 > `docs/autoflow/04-architecture/adr/adr-020-ffmpeg-camera-indev.md`。 @@ -30,11 +41,12 @@ LGPL static build 來源,採「自 build decoder-only」策略,binary 直接 | 檔案 | sha256 | |------|--------| -| `ffmpeg` | `1afa56dabb4ba4fb45323e37510602309ffd544d9e7ecb2d8267697a7cc16626` | -| `ffprobe` | `501ec3fbe450c44f4c28cd7534940c6a16a42f8584e852b4f315de565aba414e` | +| `ffmpeg` | `cb42312fa89d2dc8443ae0dab810d42e18d3efefd9b62cf0a2e18762f7a8cdb7` | +| `ffprobe` | `e7eb9e61a6bd77549cd6e2da9fec25beb67e71c3c3c4b1356ee395c591c0b3e9` | | `COPYING.LGPLv3` | `da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768` | -> 舊值(2026-04-15 decoder-only、無 indev):ffmpeg `c3cb9f1d…992c` / ffprobe `bd388fb4…0c5e`。 +> 舊值(2026-08-02 ADR-020、加 avfoundation indev、無 rawvideo):ffmpeg `1afa56da…6626` / ffprobe `501ec3fb…414e`。 +> 更舊(2026-04-15 decoder-only、無 indev):ffmpeg `c3cb9f1d…992c` / ffprobe `bd388fb4…0c5e`。 計算指令: ```bash @@ -45,21 +57,24 @@ shasum -a 256 vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe | 檔案 | Bytes | 人類可讀 | |------|-------|---------| -| `ffmpeg` | 6,030,224 | 5.8 MB | -| `ffprobe` | 5,892,384 | 5.6 MB | +| `ffmpeg` | 6,034,432 | 5.8 MB | +| `ffprobe` | 5,896,624 | 5.6 MB | 實測比 TDD 原估 10–15 MB 小一半,因為 `--disable-everything` + 白名單僅啟用必要 decoder/demuxer/filter,無 GPL 元件。 +> **rawvideo decoder 體積增量(2026-08-03)**:ffmpeg 6,030,224 → 6,034,432 bytes,僅 +> **+4,208 bytes(< 0.005 MB)**。rawvideo decoder 只做 byte 重排 / format 標記、無演算法,體積增量極小。 + > **avfoundation indev 體積增量(ADR-020)**:ffmpeg 6,007,520 → 6,030,224 bytes,僅 > **+22,704 bytes(+0.02 MB)**,遠低於 ADR-020 估的 < 0.5 MB。因 avfoundation indev 是薄封裝, > 呼叫系統 AVFoundation / CoreMedia / CoreVideo framework,不自帶任何 codec。 ### Build 實測耗時 -- **3 分 57 秒**(2026-08-02 ADR-020 rebuild,`make vendor-ffmpeg-macos-build` 的 `time` 量測) -- user: 870.32s,system: 92.27s,wall-clock: 236.62s -- CPU 使用率:~406%(macOS x86_64,8 核 Intel) -- (2026-04-15 首次 decoder-only build 為 2 分 44 秒;本次略增因多編 avfoundation indev) +- **2 分 17 秒**(2026-08-03 加 rawvideo decoder rebuild,`make vendor-ffmpeg-macos-build` 的 `time` 量測) +- user: 548.43s,system: 52.22s,wall-clock: 137.43s +- CPU 使用率:~437%(macOS x86_64,8 核 Intel) +- (2026-08-02 ADR-020 rebuild 為 3 分 57 秒;2026-04-15 首次 decoder-only build 為 2 分 44 秒。本次較快為 build cache / 系統負載差異,rawvideo decoder 本身編譯成本極低) ## License @@ -73,7 +88,11 @@ build 不 link 以下 GPL-only 元件: - 無 `libfaac`(non-free) 僅使用 libavcodec 內建的 LGPL native decoder(h264 / hevc / mpeg1video / mpeg2video / -mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_*)。 +mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_* / rawvideo)。 + +**`rawvideo` decoder(2026-08-03)為 LGPL-safe**:libavcodec 內建 native decoder,僅做 byte 重排 / +pixel format 標記、無壓縮演算法,不引入任何 GPL / 第三方元件。加 `rawvideo` 後 `ffmpeg -version` 的 +configuration line 仍**不含** `--enable-gpl` / `libx264` / `libx265`(已實測驗證),LGPL v3 合規未破。 **avfoundation indev(ADR-020)為 LGPL-safe,不引入任何 GPL 元件**:avfoundation input device 只是薄封裝、透過 macOS 系統的 AVFoundation / CoreMedia / CoreVideo framework 抓實體攝影機 frame, @@ -103,7 +122,7 @@ Foundation / CoreGraphics / libobjc),非第三方 dylib。 --enable-avfoundation \ --enable-indev=avfoundation \ --enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \ - --enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be \ + --enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be,rawvideo \ --enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \ --enable-filter=scale,format,fps,null,anull \ --enable-muxer=image2pipe,image2,null \ @@ -132,7 +151,7 @@ Foundation / CoreGraphics / libobjc),非第三方 dylib。 | `--enable-avfoundation` | **(ADR-020)** camera 抓實體攝影機需 AVFoundation framework。因本 build 用 `--disable-autodetect`(連 AVFoundation 框架都不自動偵測),必須顯式 `--enable-avfoundation` 才能讓下面的 `avfoundation` indev 的依賴(`avfoundation corevideo coremedia pthreads`)被滿足。**少了這行、`--enable-indev=avfoundation` 會被 configure 靜默 disable(`WARNING: Disabled avfoundation_indev because not all dependencies are satisfied`)** | | `--enable-indev=avfoundation` | **(ADR-020)** camera 即時推論的 macOS input device。ffmpeg `-f avfoundation -i ":none"` 從實體攝影機抓 raw frame → MJPEG pipe。少了它會 `Unknown input format: 'avfoundation'`。LGPL-safe 薄封裝、體積增量 < 0.03MB | | `--enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2` | 對齊 PRD v2 支援的上傳格式 `.mp4 / .avi / .mov / .mpeg / .mpg` | -| `--enable-decoder=h264,hevc,...` | 涵蓋常見 codec:H.264 / H.265 / MPEG1/2/4 / mjpeg / prores / vp8/9 / AAC / MP2/3 / PCM | +| `--enable-decoder=h264,hevc,...,rawvideo` | 涵蓋常見 codec:H.264 / H.265 / MPEG1/2/4 / mjpeg / prores / vp8/9 / AAC / MP2/3 / PCM。**`rawvideo`(2026-08-03)**:camera 經 avfoundation 抓出的 frame 是 raw `uyvy422`(codec = `rawvideo`),轉 MJPEG pipe 前必須先解碼;少了它 → `no decoder found for: rawvideo` → EOF。LGPL-safe native decoder | | `--enable-parser=...` | 必要,否則某些 decoder 會在碼流切分階段 fail | | `--enable-muxer=image2pipe,image2,null` | 輸出單張 JPEG 或 NULL(測試用) | | `--enable-encoder=mjpeg` | `-f image2pipe -vcodec mjpeg` 需要 mjpeg encoder(LGPL-safe) | @@ -334,6 +353,34 @@ $ vendor/ffmpeg/macos/ffmpeg -hide_banner -f avfoundation -list_devices true -i - ✅ 不再出現 `Unknown input format: 'avfoundation'`(camera 開不了的根因已解) - 註:`-list_devices true` 列完裝置後以非 0 退出(`Error opening input`)屬正常行為,非失敗。 +### 7. rawvideo decoder(2026-08-03,本次 rebuild 新增) + +``` +$ vendor/ffmpeg/macos/ffmpeg -hide_banner -decoders 2>&1 | grep -i rawvideo + V..... rawvideo +``` + +- ✅ `rawvideo` decoder 已編入白名單。 + +### 8. Camera raw capture 端到端(2026-08-03,決定性驗證) + +``` +$ vendor/ffmpeg/macos/ffmpeg -f avfoundation -pixel_format uyvy422 \ + -framerate 30 -video_size 640x480 -i "0:none" \ + -f image2pipe -vcodec mjpeg -q:v 5 -an -frames:v 1 -y /tmp/cam_ok.jpg +Input #0, avfoundation, from '0:none': + Stream #0:0: Video: rawvideo (UYVY / 0x59565955), uyvy422, 640x480, 30 tbr +Stream mapping: + Stream #0:0 -> #0:0 (rawvideo (native) -> mjpeg (native)) +frame= 1 ... Lsize= 15KiB + +$ file /tmp/cam_ok.jpg +/tmp/cam_ok.jpg: JPEG image data, baseline, precision 8, 640x480, components 3 +``` + +- ✅ 攝影機成功打開(`-pixel_format uyvy422`)+ `rawvideo (native) -> mjpeg (native)` 轉碼鏈全通 +- ✅ 真的從實體攝影機出一張 640x480 JPEG(15 KiB),兩層根因(args pixel_format + rawvideo decoder)都修好。 + --- ## Commit 清單(只允許這四個檔進 git) diff --git a/local-agent/vendor/ffmpeg/macos/ffmpeg b/local-agent/vendor/ffmpeg/macos/ffmpeg index 387c950..3c2671d 100755 Binary files a/local-agent/vendor/ffmpeg/macos/ffmpeg and b/local-agent/vendor/ffmpeg/macos/ffmpeg differ diff --git a/local-agent/vendor/ffmpeg/macos/ffprobe b/local-agent/vendor/ffmpeg/macos/ffprobe index 10f3668..17de7bf 100755 Binary files a/local-agent/vendor/ffmpeg/macos/ffprobe and b/local-agent/vendor/ffmpeg/macos/ffprobe differ