// camera.go — /api/camera/* 與 /api/media/* 的 route 宣告。 // // 這兩組 endpoint 全部走 tunnel proxy:實際的攝影機 / 圖片 / 影片 / 批次推論 // 都執行在 local agent(USB 插著攝影機、跑著 local-tool 的那台機器)。api-server // 只負責「面向瀏覽器 + auth」,把請求原樣中繼給 local agent(見 proxy.go)。 // // 為什麼從 stubs.go 的 501 搬出來獨立成檔: // - 對齊 devices.go 的慣例(每個 domain 一個 register 檔),stubs.go 只留真正 // 還沒 handler 的 endpoint。 // - camera/media 與 devices 走的是同一套 newProxyHandler,pattern 已在 B5 生產跑通 // (/api/devices/scan 等)。這裡只是「宣告路徑 + 決定 streaming flag」。 // // streaming flag 的判斷: // - GET /camera/stream → streaming(MJPEG multipart/x-mixed-replace 長連線) // - GET /media/batch-images/:i → **非 streaming**:回單張 jpeg(一次讀完),走 // request-response 即可。 // - 其餘(list / start / stop / upload / seek)→ 非 streaming request-response。 // upload 類的 request body 由 http.NewRequestWithContext 以 streaming 方式送出 // (proxy.go 直接把 c.Request.Body 當 upstream body),大 multipart 也不會一次 // 載入記憶體;streaming flag 只控制「response 是否逐 chunk flush」,與 request // body 上傳無關,所以 upload 用 proxyOptions{} 即可。 // // 對齊 .autoflow/04-architecture/camera-e2e-effort-estimate.md §2.1 / §3.1 與 // local-tool/server/internal/api/router.go:108-118(路徑與 local agent 完全一致, // 無需 rewritePath)。 // // POC 對照:edge-ai-platform relay/server.go:213-228(flusher 逐 chunk 送 MJPEG) // 對應到 visionA 的 proxy.go writeProxyResponse streaming 分支 —— 那段已在 api-server // 實作好,camera/media 只需宣告路徑即可享用。 package api import ( "github.com/gin-gonic/gin" ) // registerCameraRoutes 註冊 /api/camera/* 與 /api/media/* 的 tunnel proxy routes。 // // 掛在 apiGroup(已套 AuthMiddleware)底下,所以每條路徑都受 OIDC session 保護。 // MJPEG 帶不了 Authorization header 的認證問題(R-M3/R-C4)由前端 + api-server // 的 query-token / cookie 機制處理,不在本檔範圍(本檔只負責把已認證請求中繼出去)。 func registerCameraRoutes(g *gin.RouterGroup, deps Deps) { // request-response 類:body / response 一次讀完即可。 proxy := newProxyHandler(deps, proxyOptions{}) // streaming 類:response 是 MJPEG 長連線,需逐 chunk flush。 streamProxy := newProxyHandler(deps, proxyOptions{streaming: true}) // --- Camera(即時攝影機) --- g.GET("/camera/list", proxy) // 列出可用攝影機 g.POST("/camera/start", proxy) // 開 camera + 起推論,回 streamUrl g.POST("/camera/stop", proxy) // 停止 pipeline g.GET("/camera/stream", streamProxy) // MJPEG multipart/x-mixed-replace 長連線 // --- Media(圖片 / 影片 / 批次推論) --- // 注意:media 的「結果畫面」也走上面的 /camera/stream(pipeline.go 共用同一個 // MJPEGStreamer),所以 media 端點本身都是 request-response(回 streamUrl + metadata)。 g.POST("/media/upload/image", proxy) // multipart 圖片上傳 g.POST("/media/upload/video", proxy) // multipart 影片上傳(大檔,body streaming 送出) g.POST("/media/upload/batch-images", proxy) // 多檔(最多 50 張) g.GET("/media/batch-images/:index", proxy) // 回單張 jpeg(非 streaming) g.POST("/media/seek", proxy) // 影片 seek }