feat(local-agent): camera 三平台 input device(ADR-020 WP-1/2)
camera 即時推論開不了根因:vendor decoder-only ffmpeg --disable-everything 沒 enable 任何 input device → macOS avfoundation 認不得。 - WP-1 macOS:ffmpeg rebuild 加 --enable-avfoundation + --enable-indev=avfoundation (--disable-autodetect 會靜默 disable、兩行要一起帶)。-list_devices 列出相機、 +22KB、LGPL-safe。sha 已核對。 - WP-2:buildCaptureArgs 補 Linux v4l2 分支(原誤落 avfoundation default 必壞)+ ListFFmpegDevices Linux glob /dev/video*。四路明確 case + 回歸鎖。 reviewer WP-1(0C/0M/0m) + WP-2(0C/0M/2m) 通過。Windows dshow / Linux v4l2 實機驗待機器。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4d27594d6
commit
17134e8eae
@ -178,6 +178,8 @@ vendor-ffmpeg-macos-build: ## macOS:從源碼 build LGPL v3 decoder-only ffmpe
|
|||||||
--disable-everything \
|
--disable-everything \
|
||||||
--enable-small \
|
--enable-small \
|
||||||
--enable-protocol=file,pipe \
|
--enable-protocol=file,pipe \
|
||||||
|
--enable-avfoundation \
|
||||||
|
--enable-indev=avfoundation \
|
||||||
--enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \
|
--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 \
|
||||||
--enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \
|
--enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import (
|
|||||||
const stderrTailBytes = 8 * 1024
|
const stderrTailBytes = 8 * 1024
|
||||||
|
|
||||||
// FFmpegCamera captures webcam frames using ffmpeg subprocess.
|
// 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
|
// ffmpeg outputs a continuous MJPEG stream to stdout which is parsed
|
||||||
// by scanning for JPEG SOI (0xFFD8) and EOI (0xFFD9) markers.
|
// by scanning for JPEG SOI (0xFFD8) and EOI (0xFFD9) markers.
|
||||||
type FFmpegCamera struct {
|
type FFmpegCamera struct {
|
||||||
@ -167,10 +167,33 @@ func (c *FFmpegCamera) StderrTail() string {
|
|||||||
|
|
||||||
// buildCaptureArgs returns the ffmpeg arguments for the current OS.
|
// buildCaptureArgs returns the ffmpeg arguments for the current OS.
|
||||||
func buildCaptureArgs(cameraIndex int, cameraName string, width, height, framerate int) []string {
|
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 "<index>:none")
|
||||||
|
// - Windows → dshow(-i video="<name>")
|
||||||
|
// - Linux → v4l2(-i /dev/video<N>)
|
||||||
|
//
|
||||||
|
// 三平台後段皆接 -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)
|
videoSize := fmt.Sprintf("%dx%d", width, height)
|
||||||
fps := fmt.Sprintf("%d", framerate)
|
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":
|
case "windows":
|
||||||
// DirectShow on Windows: -f dshow -i video="Camera Name"
|
// DirectShow on Windows: -f dshow -i video="Camera Name"
|
||||||
inputName := cameraName
|
inputName := cameraName
|
||||||
@ -183,30 +206,41 @@ func buildCaptureArgs(cameraIndex int, cameraName string, width, height, framera
|
|||||||
inputName = "Integrated Camera"
|
inputName = "Integrated Camera"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{
|
return captureTail([]string{
|
||||||
"-f", "dshow",
|
"-f", "dshow",
|
||||||
"-framerate", fps,
|
"-framerate", fps,
|
||||||
"-video_size", videoSize,
|
"-video_size", videoSize,
|
||||||
"-i", fmt.Sprintf("video=%s", inputName),
|
"-i", fmt.Sprintf("video=%s", inputName),
|
||||||
"-f", "image2pipe",
|
})
|
||||||
"-vcodec", "mjpeg",
|
case "linux":
|
||||||
"-q:v", "5",
|
// Video4Linux2 on Linux: -f v4l2 -i /dev/video<N>
|
||||||
"-an",
|
// v4l2 以裝置節點路徑(非 index)指定攝影機;cameraIndex 對應 /dev/video<N>,
|
||||||
"-",
|
// 這是 Linux 攝影機的慣例(/dev/video0 = 第一支)。cameraName 在 Linux 未使用。
|
||||||
}
|
return captureTail([]string{
|
||||||
default:
|
"-f", "v4l2",
|
||||||
|
"-framerate", fps,
|
||||||
|
"-video_size", videoSize,
|
||||||
|
"-i", fmt.Sprintf("/dev/video%d", cameraIndex),
|
||||||
|
})
|
||||||
|
case "darwin":
|
||||||
// AVFoundation on macOS: -f avfoundation -i "index:none"
|
// AVFoundation on macOS: -f avfoundation -i "index:none"
|
||||||
return []string{
|
return captureTail([]string{
|
||||||
"-f", "avfoundation",
|
"-f", "avfoundation",
|
||||||
"-framerate", fps,
|
"-framerate", fps,
|
||||||
"-video_size", videoSize,
|
"-video_size", videoSize,
|
||||||
"-i", fmt.Sprintf("%d:none", cameraIndex),
|
"-i", fmt.Sprintf("%d:none", cameraIndex),
|
||||||
"-f", "image2pipe",
|
})
|
||||||
"-vcodec", "mjpeg",
|
default:
|
||||||
"-q:v", "5",
|
// 未知平台:明確落到 macOS 的 avfoundation 是錯的(原本 default 就是這個 bug)。
|
||||||
"-an",
|
// 保留 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),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
192
local-agent/server/internal/camera/ffmpeg_capture_args_test.go
Normal file
192
local-agent/server/internal/camera/ffmpeg_capture_args_test.go
Normal file
@ -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 <indev>" 值
|
||||||
|
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<N> 節點路徑。
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -3,8 +3,10 @@ package camera
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -19,7 +21,14 @@ func DetectFFmpeg() bool {
|
|||||||
// Automatically selects the correct capture framework for the current OS:
|
// Automatically selects the correct capture framework for the current OS:
|
||||||
// - macOS: AVFoundation
|
// - macOS: AVFoundation
|
||||||
// - Windows: DirectShow (dshow)
|
// - Windows: DirectShow (dshow)
|
||||||
|
// - Linux: Video4Linux2 (v4l2, enumerated from /dev/video*)
|
||||||
func ListFFmpegDevices() []CameraInfo {
|
func ListFFmpegDevices() []CameraInfo {
|
||||||
|
// Linux 走 /dev/video* 列舉、不依賴 ffmpeg binary(-list_devices 在部分 v4l2 build
|
||||||
|
// 不穩定),故不要求 DetectFFmpeg 也能列出裝置節點;實際抓 frame 時才需要 ffmpeg。
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
return listV4L2Devices()
|
||||||
|
}
|
||||||
|
|
||||||
if !DetectFFmpeg() {
|
if !DetectFFmpeg() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -132,3 +141,66 @@ func parseDShowOutput(output string) []CameraInfo {
|
|||||||
|
|
||||||
return cameras
|
return cameras
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Linux (Video4Linux2) ---
|
||||||
|
|
||||||
|
// v4l2DeviceGlob 是列舉 v4l2 攝影機節點的 glob pattern。抽成變數讓測試可覆寫成
|
||||||
|
// 假的目錄,不依賴實機 /dev。
|
||||||
|
var v4l2DeviceGlob = "/dev/video*"
|
||||||
|
|
||||||
|
// v4l2DeviceIndexRe 從 /dev/video<N> 取出 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<N> 形式、略過
|
||||||
|
}
|
||||||
|
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> 取出 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
|
||||||
|
}
|
||||||
|
|||||||
88
local-agent/vendor/ffmpeg/macos/BUILD.md
vendored
88
local-agent/vendor/ffmpeg/macos/BUILD.md
vendored
@ -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 |
|
| Toolchain | Apple clang 16.0.0 (clang-1600.0.26.6), Command Line Tools |
|
||||||
| Assembler | nasm 3.01(Homebrew bottle,compiled 2025-10-11) |
|
| Assembler | nasm 3.01(Homebrew bottle,compiled 2025-10-11) |
|
||||||
| Homebrew | 5.1.6 |
|
| 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 一致) |
|
| 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
|
## Binary sha256
|
||||||
|
|
||||||
| 檔案 | sha256 |
|
| 檔案 | sha256 |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `ffmpeg` | `c3cb9f1dad66730267c12fca92c6344d2f8939ab227889caac33005f8947992c` |
|
| `ffmpeg` | `1afa56dabb4ba4fb45323e37510602309ffd544d9e7ecb2d8267697a7cc16626` |
|
||||||
| `ffprobe` | `bd388fb4372ed5f7e44ee331a51be6383d702fb2c067bf562cabbdfbdd8b0c5e` |
|
| `ffprobe` | `501ec3fbe450c44f4c28cd7534940c6a16a42f8584e852b4f315de565aba414e` |
|
||||||
| `COPYING.LGPLv3` | `da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768` |
|
| `COPYING.LGPLv3` | `da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768` |
|
||||||
|
|
||||||
|
> 舊值(2026-04-15 decoder-only、無 indev):ffmpeg `c3cb9f1d…992c` / ffprobe `bd388fb4…0c5e`。
|
||||||
|
|
||||||
計算指令:
|
計算指令:
|
||||||
```bash
|
```bash
|
||||||
shasum -a 256 vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe
|
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 | 人類可讀 |
|
| 檔案 | Bytes | 人類可讀 |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| `ffmpeg` | 6,007,520 | 5.7 MB |
|
| `ffmpeg` | 6,030,224 | 5.8 MB |
|
||||||
| `ffprobe` | 5,865,568 | 5.6 MB |
|
| `ffprobe` | 5,892,384 | 5.6 MB |
|
||||||
|
|
||||||
實測比 TDD 原估 10–15 MB 小一半,因為 `--disable-everything` + 白名單僅啟用必要 decoder/demuxer/filter,無 GPL 元件。
|
實測比 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 實測耗時
|
### Build 實測耗時
|
||||||
|
|
||||||
- **2 分 44 秒**(`make vendor-ffmpeg-macos-build` 的 `time` 量測)
|
- **3 分 57 秒**(2026-08-02 ADR-020 rebuild,`make vendor-ffmpeg-macos-build` 的 `time` 量測)
|
||||||
- user: 559.60s,system: 56.03s,wall-clock: 164.56s
|
- user: 870.32s,system: 92.27s,wall-clock: 236.62s
|
||||||
- CPU 使用率:~374%(macOS x86_64,8 核 Intel)
|
- CPU 使用率:~406%(macOS x86_64,8 核 Intel)
|
||||||
- 比 TDD 原估 10–20 分鐘快很多,因為 `--disable-everything` 大幅削減編譯單元數量
|
- (2026-04-15 首次 decoder-only build 為 2 分 44 秒;本次略增因多編 avfoundation indev)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@ -64,6 +75,13 @@ build 不 link 以下 GPL-only 元件:
|
|||||||
僅使用 libavcodec 內建的 LGPL native decoder(h264 / hevc / mpeg1video / mpeg2video /
|
僅使用 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_*)。
|
||||||
|
|
||||||
|
**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(完整複製)
|
## Configure flags(完整複製)
|
||||||
@ -82,6 +100,8 @@ mpeg4 / mjpeg / prores / vp8 / vp9 / aac / mp2 / mp3 / pcm_*)。
|
|||||||
--disable-everything \
|
--disable-everything \
|
||||||
--enable-small \
|
--enable-small \
|
||||||
--enable-protocol=file,pipe \
|
--enable-protocol=file,pipe \
|
||||||
|
--enable-avfoundation \
|
||||||
|
--enable-indev=avfoundation \
|
||||||
--enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \
|
--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 \
|
||||||
--enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \
|
--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 元件 |
|
| `--disable-everything` | 先關全部,白名單 enable,確保不額外 link 任何 GPL 元件 |
|
||||||
| `--enable-small` | 最佳化體積而非速度 |
|
| `--enable-small` | 最佳化體積而非速度 |
|
||||||
| `--enable-protocol=file,pipe` | 只開 file:// 和 pipe(ffmpeg 內部 stdin/stdout) |
|
| `--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 "<index>: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-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,...` | 涵蓋常見 codec:H.264 / H.265 / MPEG1/2/4 / mjpeg / prores / vp8/9 / AAC / MP2/3 / PCM |
|
||||||
| `--enable-parser=...` | 必要,否則某些 decoder 會在碼流切分階段 fail |
|
| `--enable-parser=...` | 必要,否則某些 decoder 會在碼流切分階段 fail |
|
||||||
@ -192,6 +214,13 @@ codesign -v vendor/ffmpeg/macos/ffprobe
|
|||||||
vendor/ffmpeg/macos/ffmpeg -hide_banner -i <some-sample>.mp4 -f image2pipe -vcodec mjpeg -frames:v 1 -q:v 5 /tmp/test.jpg
|
vendor/ffmpeg/macos/ffmpeg -hide_banner -i <some-sample>.mp4 -f image2pipe -vcodec mjpeg -frames:v 1 -q:v 5 /tmp/test.jpg
|
||||||
file /tmp/test.jpg
|
file /tmp/test.jpg
|
||||||
# 預期:JPEG image data
|
# 預期: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
|
- `mpegts` — MPEG Transport Stream
|
||||||
- `matroska,webm` — ok
|
- `matroska,webm` — ok
|
||||||
|
|
||||||
### 4. Dynamic dependencies (`otool -L`)
|
### 4. Dynamic dependencies (`otool -L`)(ADR-020 rebuild 後)
|
||||||
|
|
||||||
```
|
```
|
||||||
vendor/ffmpeg/macos/ffmpeg:
|
vendor/ffmpeg/macos/ffmpeg:
|
||||||
|
/System/Library/Frameworks/Foundation.framework/.../Foundation ← ADR-020 新增(avfoundation 依賴)
|
||||||
/usr/lib/libSystem.B.dylib
|
/usr/lib/libSystem.B.dylib
|
||||||
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
|
/System/Library/Frameworks/AVFoundation.framework/.../AVFoundation ← ADR-020 新增
|
||||||
/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo
|
/System/Library/Frameworks/CoreVideo.framework/.../CoreVideo
|
||||||
/System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia
|
/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:
|
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`... 都不存在)
|
- ✅ **無任何第三方 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
|
### 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
|
$ 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`)屬正常行為,非失敗。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
local-agent/vendor/ffmpeg/macos/ffmpeg
vendored
BIN
local-agent/vendor/ffmpeg/macos/ffmpeg
vendored
Binary file not shown.
BIN
local-agent/vendor/ffmpeg/macos/ffprobe
vendored
BIN
local-agent/vendor/ffmpeg/macos/ffprobe
vendored
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user