diff --git a/local-agent/Makefile b/local-agent/Makefile index 9f08fca..31c11ea 100644 --- a/local-agent/Makefile +++ b/local-agent/Makefile @@ -178,6 +178,8 @@ vendor-ffmpeg-macos-build: ## macOS:從源碼 build LGPL v3 decoder-only ffmpe --disable-everything \ --enable-small \ --enable-protocol=file,pipe \ + --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-parser=h264,hevc,mpeg4video,mpegaudio,aac \ diff --git a/local-agent/server/internal/camera/ffmpeg_camera.go b/local-agent/server/internal/camera/ffmpeg_camera.go index 45a7c4e..7691acb 100644 --- a/local-agent/server/internal/camera/ffmpeg_camera.go +++ b/local-agent/server/internal/camera/ffmpeg_camera.go @@ -17,7 +17,7 @@ import ( const stderrTailBytes = 8 * 1024 // FFmpegCamera captures webcam frames using ffmpeg subprocess. -// Supports macOS (AVFoundation) and Windows (DirectShow). +// Supports macOS (AVFoundation), Windows (DirectShow) and Linux (Video4Linux2). // ffmpeg outputs a continuous MJPEG stream to stdout which is parsed // by scanning for JPEG SOI (0xFFD8) and EOI (0xFFD9) markers. type FFmpegCamera struct { @@ -167,10 +167,33 @@ func (c *FFmpegCamera) StderrTail() string { // buildCaptureArgs returns the ffmpeg arguments for the current OS. func buildCaptureArgs(cameraIndex int, cameraName string, width, height, framerate int) []string { + return buildCaptureArgsForOS(runtime.GOOS, cameraIndex, cameraName, width, height, framerate) +} + +// buildCaptureArgsForOS 依指定 goos 組 ffmpeg capture args,把平台判斷抽成參數以便 +// table test 三平台輸出(不用 mock runtime.GOOS)。三平台各用不同 input device(indev): +// - macOS → avfoundation(-i ":none") +// - Windows → dshow(-i video="") +// - Linux → v4l2(-i /dev/video) +// +// 三平台後段皆接 -f image2pipe -vcodec mjpeg -q:v 5 -an -,MJPEG pipe 架構共用。 +// 對照見 ADR-020 §2.2 三平台 capture args 對照表。 +func buildCaptureArgsForOS(goos string, cameraIndex int, cameraName string, width, height, framerate int) []string { videoSize := fmt.Sprintf("%dx%d", width, height) fps := fmt.Sprintf("%d", framerate) - switch runtime.GOOS { + // captureTail 是三平台共用的輸出段(把 raw frame 轉成 stdout 上的 MJPEG stream)。 + captureTail := func(args []string) []string { + return append(args, + "-f", "image2pipe", + "-vcodec", "mjpeg", + "-q:v", "5", + "-an", + "-", + ) + } + + switch goos { case "windows": // DirectShow on Windows: -f dshow -i video="Camera Name" inputName := cameraName @@ -183,30 +206,41 @@ func buildCaptureArgs(cameraIndex int, cameraName string, width, height, framera inputName = "Integrated Camera" } } - return []string{ + return captureTail([]string{ "-f", "dshow", "-framerate", fps, "-video_size", videoSize, "-i", fmt.Sprintf("video=%s", inputName), - "-f", "image2pipe", - "-vcodec", "mjpeg", - "-q:v", "5", - "-an", - "-", - } - default: + }) + case "linux": + // Video4Linux2 on Linux: -f v4l2 -i /dev/video + // v4l2 以裝置節點路徑(非 index)指定攝影機;cameraIndex 對應 /dev/video, + // 這是 Linux 攝影機的慣例(/dev/video0 = 第一支)。cameraName 在 Linux 未使用。 + return captureTail([]string{ + "-f", "v4l2", + "-framerate", fps, + "-video_size", videoSize, + "-i", fmt.Sprintf("/dev/video%d", cameraIndex), + }) + case "darwin": // AVFoundation on macOS: -f avfoundation -i "index:none" - return []string{ + return captureTail([]string{ "-f", "avfoundation", "-framerate", fps, "-video_size", videoSize, "-i", fmt.Sprintf("%d:none", cameraIndex), - "-f", "image2pipe", - "-vcodec", "mjpeg", - "-q:v", "5", - "-an", - "-", - } + }) + default: + // 未知平台:明確落到 macOS 的 avfoundation 是錯的(原本 default 就是這個 bug)。 + // 保留 avfoundation 作為最後手段,但這只是為了讓非三大平台不至於 build 失敗; + // 實務上未知平台的 camera 抓取本就不支援,會在 ffmpeg 層以「Unknown input format」失敗。 + // 三大目標平台(darwin/windows/linux)都有明確 case、不會落到這裡。 + return captureTail([]string{ + "-f", "avfoundation", + "-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 new file mode 100644 index 0000000..79a3fd3 --- /dev/null +++ b/local-agent/server/internal/camera/ffmpeg_capture_args_test.go @@ -0,0 +1,192 @@ +package camera + +import ( + "strings" + "testing" +) + +// TestBuildCaptureArgsForOS 驗證三平台各自用正確的 ffmpeg input device(indev): +// macOS→avfoundation、Windows→dshow、Linux→v4l2。這是 ADR-020 的核心修復: +// 原本 Linux 會誤落 default 分支用 avfoundation → 必壞。 +func TestBuildCaptureArgsForOS(t *testing.T) { + const ( + width = 640 + height = 480 + fps = 30 + index = 0 + ) + + tests := []struct { + name string + goos string + cameraName string + // wantInputFlag 是預期的 "-f " 值 + wantIndev string + // wantInputArg 是 "-i" 後面的值 + wantInputArg string + }{ + { + name: "macOS uses avfoundation with index:none", + goos: "darwin", + wantIndev: "avfoundation", + wantInputArg: "0:none", + }, + { + name: "Windows uses dshow with video=name", + goos: "windows", + cameraName: "Integrated Camera", + wantIndev: "dshow", + wantInputArg: "video=Integrated Camera", + }, + { + name: "Linux uses v4l2 with /dev/video path", + goos: "linux", + wantIndev: "v4l2", + wantInputArg: "/dev/video0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := buildCaptureArgsForOS(tt.goos, index, tt.cameraName, width, height, fps) + + gotIndev := valueAfterFlag(args, "-f") // 第一個 -f 是 input format + if gotIndev != tt.wantIndev { + t.Errorf("indev = %q, want %q\nargs: %v", gotIndev, tt.wantIndev, args) + } + + gotInput := valueAfterFlag(args, "-i") + if gotInput != tt.wantInputArg { + t.Errorf("input = %q, want %q\nargs: %v", gotInput, tt.wantInputArg, args) + } + + // 三平台後段皆須為 image2pipe / mjpeg,MJPEG pipe 架構共用。 + if !containsSeq(args, "-f", "image2pipe") { + t.Errorf("missing image2pipe output, args: %v", args) + } + if !containsSeq(args, "-vcodec", "mjpeg") { + t.Errorf("missing mjpeg vcodec, args: %v", args) + } + if args[len(args)-1] != "-" { + t.Errorf("last arg should be stdout '-', got %q", args[len(args)-1]) + } + }) + } +} + +// TestBuildCaptureArgsForOS_LinuxIndexToDevicePath 驗證 Linux 的 cameraIndex 正確 +// 對應到 /dev/video 節點路徑。 +func TestBuildCaptureArgsForOS_LinuxIndexToDevicePath(t *testing.T) { + for _, idx := range []int{0, 1, 2, 10} { + args := buildCaptureArgsForOS("linux", idx, "", 640, 480, 30) + want := "/dev/video" + itoa(idx) + if got := valueAfterFlag(args, "-i"); got != want { + t.Errorf("index %d → input %q, want %q", idx, got, want) + } + } +} + +// TestBuildCaptureArgsForOS_LinuxNotAVFoundation 是 ADR-020 的回歸鎖:Linux 絕不能 +// 用 avfoundation(macOS 專用)。若未來有人把 Linux case 拿掉、讓它落回 default, +// 這個測試會抓到。 +func TestBuildCaptureArgsForOS_LinuxNotAVFoundation(t *testing.T) { + args := buildCaptureArgsForOS("linux", 0, "", 640, 480, 30) + joined := strings.Join(args, " ") + if strings.Contains(joined, "avfoundation") { + t.Fatalf("Linux must NOT use avfoundation (macOS-only), args: %v", args) + } + if !strings.Contains(joined, "v4l2") { + t.Fatalf("Linux must use v4l2, args: %v", args) + } +} + +// TestParseV4L2Devices 驗證 /dev/video* 路徑清單解析成 CameraInfo,且依 index 數值 +// 排序(video10 排在 video2 之後、而非字典序)。 +func TestParseV4L2Devices(t *testing.T) { + paths := []string{ + "/dev/video10", + "/dev/video2", + "/dev/video0", + "/dev/video-not-a-number", // 應被略過 + "/dev/videoX", // 應被略過 + } + + got := parseV4L2Devices(paths) + + wantIndexes := []int{0, 2, 10} + if len(got) != len(wantIndexes) { + t.Fatalf("got %d devices, want %d: %+v", len(got), len(wantIndexes), got) + } + for i, want := range wantIndexes { + if got[i].Index != want { + t.Errorf("device[%d].Index = %d, want %d", i, got[i].Index, want) + } + if got[i].Name != "/dev/video"+itoa(want) { + t.Errorf("device[%d].Name = %q, want /dev/video%d", i, got[i].Name, want) + } + } +} + +// TestParseV4L2Devices_Empty 驗證無裝置時回 nil/空。 +func TestParseV4L2Devices_Empty(t *testing.T) { + if got := parseV4L2Devices(nil); len(got) != 0 { + t.Errorf("expected no devices, got %+v", got) + } +} + +// TestV4L2Index 驗證從路徑取 index。 +func TestV4L2Index(t *testing.T) { + cases := map[string]int{ + "/dev/video0": 0, + "/dev/video12": 12, + "/dev/video": -1, + "/dev/videoab": -1, + "videoX": -1, + } + for path, want := range cases { + if got := v4l2Index(path); got != want { + t.Errorf("v4l2Index(%q) = %d, want %d", path, got, want) + } + } +} + +// --- helpers --- + +// valueAfterFlag 回傳 args 中第一個等於 flag 的元素的下一個值。 +func valueAfterFlag(args []string, flag string) string { + for i := 0; i < len(args)-1; i++ { + if args[i] == flag { + return args[i+1] + } + } + return "" +} + +// containsSeq 判斷 args 是否包含連續的 a b 兩個元素。 +func containsSeq(args []string, a, b string) bool { + for i := 0; i < len(args)-1; i++ { + if args[i] == a && args[i+1] == b { + return true + } + } + return false +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var buf []byte + for n > 0 { + buf = append([]byte{byte('0' + n%10)}, buf...) + n /= 10 + } + if neg { + buf = append([]byte{'-'}, buf...) + } + return string(buf) +} diff --git a/local-agent/server/internal/camera/ffmpeg_detect.go b/local-agent/server/internal/camera/ffmpeg_detect.go index ca587ba..79a9682 100644 --- a/local-agent/server/internal/camera/ffmpeg_detect.go +++ b/local-agent/server/internal/camera/ffmpeg_detect.go @@ -3,8 +3,10 @@ package camera import ( "fmt" "os/exec" + "path/filepath" "regexp" "runtime" + "sort" "strconv" "strings" ) @@ -17,9 +19,16 @@ func DetectFFmpeg() bool { // ListFFmpegDevices detects available video devices using ffmpeg. // Automatically selects the correct capture framework for the current OS: -// - macOS: AVFoundation +// - macOS: AVFoundation // - Windows: DirectShow (dshow) +// - Linux: Video4Linux2 (v4l2, enumerated from /dev/video*) func ListFFmpegDevices() []CameraInfo { + // Linux 走 /dev/video* 列舉、不依賴 ffmpeg binary(-list_devices 在部分 v4l2 build + // 不穩定),故不要求 DetectFFmpeg 也能列出裝置節點;實際抓 frame 時才需要 ffmpeg。 + if runtime.GOOS == "linux" { + return listV4L2Devices() + } + if !DetectFFmpeg() { return nil } @@ -132,3 +141,66 @@ func parseDShowOutput(output string) []CameraInfo { return cameras } + +// --- Linux (Video4Linux2) --- + +// v4l2DeviceGlob 是列舉 v4l2 攝影機節點的 glob pattern。抽成變數讓測試可覆寫成 +// 假的目錄,不依賴實機 /dev。 +var v4l2DeviceGlob = "/dev/video*" + +// v4l2DeviceIndexRe 從 /dev/video 取出 index N。 +var v4l2DeviceIndexRe = regexp.MustCompile(`video(\d+)$`) + +// listV4L2Devices 列舉 Linux 上的 v4l2 攝影機節點(/dev/video*)。 +// +// 為何用 glob 而非 ffmpeg -list_devices:v4l2 indev 的 -list_devices 支援度依 ffmpeg +// 版本而異、部分 build 不輸出可解析清單;直接列舉 /dev/video* 節點是最穩定的跨版本做法。 +// +// 注意:/dev/video* 也包含非攝影機的 V4L2 節點(如 metadata / output device),這裡先 +// 全數列出、由使用者選擇;抓 frame 失敗會在 ffmpeg 層以明確錯誤浮現(camera 修 bug 後不再吞錯)。 +func listV4L2Devices() []CameraInfo { + matches, err := filepath.Glob(v4l2DeviceGlob) + if err != nil { + return nil + } + return parseV4L2Devices(matches) +} + +// parseV4L2Devices 把 /dev/video* 路徑清單轉成 CameraInfo(依 index 排序)。 +// 抽出來讓測試可直接餵路徑清單、不碰檔案系統。 +func parseV4L2Devices(paths []string) []CameraInfo { + // 依裝置 index 數值排序(Glob 回傳為字典序,video10 會排在 video2 前,需正規化)。 + sorted := append([]string(nil), paths...) + sort.Slice(sorted, func(i, j int) bool { + return v4l2Index(sorted[i]) < v4l2Index(sorted[j]) + }) + + var cameras []CameraInfo + for _, path := range sorted { + idx := v4l2Index(path) + if idx < 0 { + continue // 不是 /dev/video 形式、略過 + } + cameras = append(cameras, CameraInfo{ + ID: fmt.Sprintf("cam-%d", idx), + Name: path, // Linux 以裝置節點路徑作為名稱(buildCaptureArgs 用 index 組回路徑) + Index: idx, + Width: 640, + Height: 480, + }) + } + return cameras +} + +// v4l2Index 從 /dev/video 取出 N;非該形式回 -1。 +func v4l2Index(path string) int { + m := v4l2DeviceIndexRe.FindStringSubmatch(path) + if len(m) != 2 { + return -1 + } + n, err := strconv.Atoi(m[1]) + if err != nil { + return -1 + } + return n +} diff --git a/local-agent/vendor/ffmpeg/macos/BUILD.md b/local-agent/vendor/ffmpeg/macos/BUILD.md index e7b0047..2498334 100644 --- a/local-agent/vendor/ffmpeg/macos/BUILD.md +++ b/local-agent/vendor/ffmpeg/macos/BUILD.md @@ -18,17 +18,24 @@ 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-04-15 | +| Build date | 2026-08-02(ADR-020 rebuild:加回 avfoundation indev) | | Build flags | 見下方 Configure flags 區塊(與 `Makefile` 的 `vendor-ffmpeg-macos-build` target 一致) | +> **ADR-020 變更(2026-08-02)**:configure 加 `--enable-avfoundation` + `--enable-indev=avfoundation`, +> 讓 camera 即時推論可用 avfoundation 抓實體攝影機。詳見 +> `docs/autoflow/04-architecture/adr/adr-020-ffmpeg-camera-indev.md`。 +> 之前的 decoder-only build(2026-04-15)沒編任何 indev,導致 `Unknown input format: 'avfoundation'`、camera 開不了。 + ## Binary sha256 | 檔案 | sha256 | |------|--------| -| `ffmpeg` | `c3cb9f1dad66730267c12fca92c6344d2f8939ab227889caac33005f8947992c` | -| `ffprobe` | `bd388fb4372ed5f7e44ee331a51be6383d702fb2c067bf562cabbdfbdd8b0c5e` | +| `ffmpeg` | `1afa56dabb4ba4fb45323e37510602309ffd544d9e7ecb2d8267697a7cc16626` | +| `ffprobe` | `501ec3fbe450c44f4c28cd7534940c6a16a42f8584e852b4f315de565aba414e` | | `COPYING.LGPLv3` | `da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768` | +> 舊值(2026-04-15 decoder-only、無 indev):ffmpeg `c3cb9f1d…992c` / ffprobe `bd388fb4…0c5e`。 + 計算指令: ```bash shasum -a 256 vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe @@ -38,17 +45,21 @@ shasum -a 256 vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe | 檔案 | Bytes | 人類可讀 | |------|-------|---------| -| `ffmpeg` | 6,007,520 | 5.7 MB | -| `ffprobe` | 5,865,568 | 5.6 MB | +| `ffmpeg` | 6,030,224 | 5.8 MB | +| `ffprobe` | 5,892,384 | 5.6 MB | 實測比 TDD 原估 10–15 MB 小一半,因為 `--disable-everything` + 白名單僅啟用必要 decoder/demuxer/filter,無 GPL 元件。 +> **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 實測耗時 -- **2 分 44 秒**(`make vendor-ffmpeg-macos-build` 的 `time` 量測) -- user: 559.60s,system: 56.03s,wall-clock: 164.56s -- CPU 使用率:~374%(macOS x86_64,8 核 Intel) -- 比 TDD 原估 10–20 分鐘快很多,因為 `--disable-everything` 大幅削減編譯單元數量 +- **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) ## License @@ -64,6 +75,13 @@ build 不 link 以下 GPL-only 元件: 僅使用 libavcodec 內建的 LGPL native decoder(h264 / hevc / mpeg1video / mpeg2video / mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_*)。 +**avfoundation indev(ADR-020)為 LGPL-safe,不引入任何 GPL 元件**:avfoundation input device +只是薄封裝、透過 macOS 系統的 AVFoundation / CoreMedia / CoreVideo framework 抓實體攝影機 frame, +不含任何第三方 / GPL codec。加 `--enable-avfoundation` + `--enable-indev=avfoundation` 後, +`ffmpeg -version` 的 configuration line 仍**不含** `--enable-gpl` / `libx264` / `libx265`(已實測驗證), +`--enable-version3`(LGPL v3)合規未破。新增 link 的皆為 Apple 系統 framework(AVFoundation / +Foundation / CoreGraphics / libobjc),非第三方 dylib。 + --- ## Configure flags(完整複製) @@ -82,6 +100,8 @@ mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_*)。 --disable-everything \ --enable-small \ --enable-protocol=file,pipe \ + --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-parser=h264,hevc,mpeg4video,mpegaudio,aac \ @@ -109,6 +129,8 @@ mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_*)。 | `--disable-everything` | 先關全部,白名單 enable,確保不額外 link 任何 GPL 元件 | | `--enable-small` | 最佳化體積而非速度 | | `--enable-protocol=file,pipe` | 只開 file:// 和 pipe(ffmpeg 內部 stdin/stdout) | +| `--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-parser=...` | 必要,否則某些 decoder 會在碼流切分階段 fail | @@ -192,6 +214,13 @@ codesign -v vendor/ffmpeg/macos/ffprobe vendor/ffmpeg/macos/ffmpeg -hide_banner -i .mp4 -f image2pipe -vcodec mjpeg -frames:v 1 -q:v 5 /tmp/test.jpg file /tmp/test.jpg # 預期:JPEG image data + +# 7.(ADR-020)確認 avfoundation indev 有編進去、可列出攝影機 +vendor/ffmpeg/macos/ffmpeg -hide_banner -devices 2>&1 | grep avfoundation +# 預期:D avfoundation +vendor/ffmpeg/macos/ffmpeg -hide_banner -f avfoundation -list_devices true -i "" 2>&1 +# 預期:列出 AVFoundation video/audio devices(不再 Unknown input format: 'avfoundation') +# 註:-list_devices true 列完裝置後會以非 0 退出(Error opening input),屬正常,非失敗。 ``` --- @@ -254,22 +283,28 @@ $ vendor/ffmpeg/macos/ffmpeg -hide_banner -formats 2>&1 \ - `mpegts` — MPEG Transport Stream - `matroska,webm` — ok -### 4. Dynamic dependencies (`otool -L`) +### 4. Dynamic dependencies (`otool -L`)(ADR-020 rebuild 後) ``` vendor/ffmpeg/macos/ffmpeg: + /System/Library/Frameworks/Foundation.framework/.../Foundation ← ADR-020 新增(avfoundation 依賴) /usr/lib/libSystem.B.dylib - /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation - /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo - /System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia + /System/Library/Frameworks/AVFoundation.framework/.../AVFoundation ← ADR-020 新增 + /System/Library/Frameworks/CoreVideo.framework/.../CoreVideo + /System/Library/Frameworks/CoreMedia.framework/.../CoreMedia + /System/Library/Frameworks/CoreGraphics.framework/.../CoreGraphics ← ADR-020 新增(avfoundation suggest) + /System/Library/Frameworks/CoreFoundation.framework/.../CoreFoundation + /usr/lib/libobjc.A.dylib ← ADR-020 新增(Objective-C runtime) vendor/ffmpeg/macos/ffprobe: - (同上四個 macOS system framework) + (同上一組 macOS system framework) ``` -- ✅ 只依賴 macOS 系統內建 framework(`libSystem`, `CoreFoundation`, `CoreVideo`, `CoreMedia`) +- ✅ 只依賴 macOS 系統內建 framework / dylib(AVFoundation / Foundation / CoreVideo / CoreMedia / + CoreGraphics / CoreFoundation / libSystem / libobjc) - ✅ **無任何第三方 dylib**(`libx264`, `libx265`, `libvpx`, `libopus`... 都不存在) -- ✅ 等同於 self-contained binary,搬到任一台 macOS 10.15+ x86_64 都能跑 +- ✅ 新增的皆為 Apple 第一方系統 framework,**LGPL 合規未破**,仍是 self-contained binary,搬到任一台 + macOS 10.15+ x86_64 都能跑 ### 5. Code signing @@ -278,7 +313,26 @@ $ codesign -v vendor/ffmpeg/macos/ffmpeg # exit 0, no output $ codesign -v vendor/ffmpeg/macos/ffprobe # exit 0, no output ``` -ad-hoc simbol signing ok,Gatekeeper 可過。 +ad-hoc symbol signing ok,Gatekeeper 可過。 + +### 6. avfoundation indev(ADR-020,本次 rebuild 新增) + +``` +$ vendor/ffmpeg/macos/ffmpeg -hide_banner -devices 2>&1 | grep avfoundation + D avfoundation + +$ vendor/ffmpeg/macos/ffmpeg -hide_banner -f avfoundation -list_devices true -i "" +[AVFoundation indev @ ...] AVFoundation video devices: +[AVFoundation indev @ ...] [0] FaceTime HD相機(內建) +[AVFoundation indev @ ...] [1] Capture screen 0 +[AVFoundation indev @ ...] AVFoundation audio devices: +[AVFoundation indev @ ...] [0] MacBook Pro的麥克風 +``` + +- ✅ `-devices` 列出 `avfoundation`(demuxing supported) +- ✅ `-list_devices true` 成功列出實體攝影機(FaceTime HD 相機)+ 音訊裝置 +- ✅ 不再出現 `Unknown input format: 'avfoundation'`(camera 開不了的根因已解) +- 註:`-list_devices true` 列完裝置後以非 0 退出(`Error opening input`)屬正常行為,非失敗。 --- diff --git a/local-agent/vendor/ffmpeg/macos/ffmpeg b/local-agent/vendor/ffmpeg/macos/ffmpeg index d9c5782..387c950 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 8746c55..10f3668 100755 Binary files a/local-agent/vendor/ffmpeg/macos/ffprobe and b/local-agent/vendor/ffmpeg/macos/ffprobe differ