指南)
esp-iot-solution USB Hub 雙攝像頭示例前端 Web 顯示組件的編譯與二次開發(fā)指南【免費(fèi)下載鏈接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution本篇技術(shù)指南聚焦于 esp-iot-solution 倉庫中examples/usb/host/usb_hub_dual_camera示例的前端組件frontend_source完整講解從 Node.js 環(huán)境準(zhǔn)備、pnpm 依賴安裝、Vite 構(gòu)建到 gzip 壓縮產(chǎn)物并發(fā)布到 SPIFFS 的整條鏈路并結(jié)合 Vue 3 Vuetify 3 源碼剖析頁面與 ESP32 HTTP 后端之間的 API 交互機(jī)制。讀完本文你將掌握如何在本機(jī)獨(dú)立編譯該 MJPEG 多攝像頭預(yù)覽頁面、理解其相機(jī)列表 / 激活 / 截圖 / 關(guān)閉的完整工作流程并具備基于現(xiàn)有源碼進(jìn)行界面定制與二次開發(fā)的能力。組件定位一個(gè)跑在 ESP32 上的攝像頭管理 Web 前端examples/usb/host/usb_hub_dual_camera是一個(gè)演示如何通過 ESP-IDF 的usb_host_uvc組件連接 USB 攝像頭、并通過 HTTP 服務(wù)器在瀏覽器中預(yù)覽 MJPEG 圖像的示例。它支持 USB Hub 擴(kuò)展多個(gè)攝像頭但同時(shí)打開的攝像頭數(shù)量受 USB 主機(jī)硬件資源限制該限制由設(shè)備端動(dòng)態(tài)上報(bào)前端會(huì)據(jù)此控制Add Camera按鈕的可用性見下文cameraNumberLimit說明。整個(gè)示例目錄結(jié)構(gòu)如下frontend_source本指南的主角Vue 3 TypeScript Vuetify 3 構(gòu)建的瀏覽器端頁面源碼mainESP32 固件源碼包含app_https.cHTTP/HTTPS 服務(wù)、app_uvc.cUVC 相機(jī)管理、app_wifi.cSoftAP 連接等模塊scripts/gzip_webserver_files.sh將前端構(gòu)建產(chǎn)物 gzip 壓縮并搬運(yùn)到spiffs目錄的腳本spiffs已打包好的 SPIFFS 資源index.html.gz、assets/index-*.js.gz、assets/index-*.css.gz、favicon.ico.gz。前端構(gòu)建產(chǎn)物以 gzip 形式隨固件燒錄到 SPIFFS 分區(qū)ESP32 在運(yùn)行時(shí)解壓并提供給瀏覽器訪問因此前端開發(fā)流程的核心就是構(gòu)建 → 壓縮 → 覆蓋spiffs目錄 → 重新編譯固件。環(huán)境要求與工具鏈根據(jù) frontend_source/README.md構(gòu)建前端組件需要滿足以下條件項(xiàng)目要求說明Node.jsnode 20較低的 Node 版本可能無法正常運(yùn)行 Vite 5 與相關(guān)依賴包管理器推薦pnpm項(xiàng)目提供pnpm-lock.yaml鎖定文件使用 pnpm 可保證依賴版本一致源碼目錄examples/usb/host/usb_hub_dual_camera/frontend_source所有前端操作均在該目錄下進(jìn)行進(jìn)入源碼目錄后執(zhí)行編譯三部曲pnpm install # install dependencies # 部分依賴需要運(yùn)行 postinstall 腳本可能需要執(zhí)行 pnpm approve-builds pnpm build # build # Gzip compress the build artifacts and move them to the spiffs directory. ./../scripts/gzip_webserver_files.sh其中pnpm approve-builds是 pnpm 10 之后的安全機(jī)制pnpm 默認(rèn)不再自動(dòng)執(zhí)行依賴包中的 postinstall 腳本需要顯式批準(zhǔn)。本項(xiàng)目在 package.json 中通過pnpm.onlyBuiltDependencies字段預(yù)先聲明了允許執(zhí)行構(gòu)建腳本的依賴白名單core-js、esbuild、vue-demipnpm: { onlyBuiltDependencies: [ core-js, esbuild, vue-demi ] }如果你使用的是較新的 pnpm 版本pnpm install時(shí)若提示某些依賴需要 approve按 README 提示執(zhí)行pnpm approve-builds即可。若因網(wǎng)絡(luò)或環(huán)境原因無法使用 pnpm也可以在frontend_source目錄下使用npm install代替但依賴解析結(jié)果可能與鎖文件存在差異建議優(yōu)先 pnpm。構(gòu)建腳本與產(chǎn)物npm scripts 全解析package.json 中定義了完整的腳本體系scripts: { dev: vite, build: run-p type-check \build-only {}\ --, preview: vite preview, build-only: vite build, type-check: vue-tsc --build --force, lint: eslint . --fix }各腳本含義如下pnpm dev啟動(dòng) Vite 開發(fā)服務(wù)器默認(rèn)端口 3000見 vite.config.mts 中的server.port。開發(fā)調(diào)試時(shí)可直接在瀏覽器中預(yù)覽頁面布局pnpm build正式構(gòu)建它通過npm-run-all2的run-p并行執(zhí)行類型檢查vue-tsc --build --force與純構(gòu)建vite build保證類型安全的同時(shí)加快構(gòu)建速度pnpm preview本地預(yù)覽構(gòu)建產(chǎn)物pnpm type-check單獨(dú)運(yùn)行 Vue 類型檢查pnpm lint運(yùn)行 ESLint 并自動(dòng)修復(fù)。pnpm build成功后產(chǎn)物輸出到frontend_source/dist目錄Vite 默認(rèn)輸出目錄。此時(shí)還不能直接交給固件需要下一步的 gzip 壓縮。依賴棧一覽從 package.json 可以看出前端組件采用了一套標(biāo)準(zhǔn)的 Vue 3 現(xiàn)代工程棧運(yùn)行時(shí)依賴vue^3.4、vuetify^3.6Material Design 組件庫、mdi/font與mdi/jsMaterial Design Icons 字體與 JS 圖標(biāo)、roboto-fontface、core-js構(gòu)建與工程化依賴vite^5.4、typescript~5.6、vue-tsc、eslint^9、sass/sass-embedded、pinia^2.1狀態(tài)管理、vue-router^4.4Vite 插件全家桶unplugin-vue-router文件路由、unplugin-auto-import自動(dòng)導(dǎo)入、unplugin-vue-components組件自動(dòng)注冊、vite-plugin-vuetifyVuetify 按需加載、vite-plugin-mock-server本地 mock 服務(wù)、vite-plugin-vue-layouts布局系統(tǒng)。Vite 配置要點(diǎn)vite.config.mts 中值得關(guān)注的配置項(xiàng)路徑別名alias: { : fileURLToPath(new URL(./src, import.meta.url)) }源碼中/store/mainstore、/utils等導(dǎo)入即映射到src目錄unplugin-vue-router自動(dòng)掃描src/pages下的.vue文件生成路由并將類型聲明輸出到src/typed-router.d.tsAutoImport自動(dòng)導(dǎo)入 Vue API 并生成src/auto-imports.d.ts這就是頁面代碼中可以直接使用onBeforeMount、watch、ref而無需手動(dòng) import 的原因Components自動(dòng)注冊src/components下的組件并生成src/components.d.tsVuetify({ autoImport: true })按需引入 Vuetify 組件減小產(chǎn)物體積mockServer()開發(fā)模式下提供本地 mock 接口方便脫離硬件調(diào)試前端頁面。把構(gòu)建產(chǎn)物發(fā)布到 SPIFFSgzip 腳本原理構(gòu)建完成后執(zhí)行./../scripts/gzip_webserver_files.sh該腳本gzip_webserver_files.sh的核心邏輯如下SRC_DIR$(realpath $(dirname $0)/../frontend_source/dist) DEST_DIR$(realpath $(dirname $0)/../spiffs) rm -fr $DEST_DIR mkdir -p $DEST_DIR find $SRC_DIR -type f | while read -r FILE; do REL_PATH${FILE#$SRC_DIR/} DEST_PATH$DEST_DIR/$REL_PATH.gz mkdir -p $(dirname $DEST_PATH) gzip -c $FILE $DEST_PATH done其工作流程為定位frontend_source/dist為源目錄spiffs為目標(biāo)目錄清空并重建spiffs目錄遍歷dist下所有文件保持相對目錄結(jié)構(gòu)不變逐個(gè)gzip -c壓縮并輸出為xxx.gz。執(zhí)行后spiffs目錄中會(huì)出現(xiàn)類似倉庫當(dāng)前已包含的產(chǎn)物spiffs/ ├── assets/ │ ├── index-DkJESlaa.js.gz │ └── index-DximumU_.css.gz ├── favicon.ico.gz └── index.html.gz之后重新編譯固件即可將最新的頁面打包進(jìn) SPIFFS 分區(qū)。相關(guān)分區(qū)劃分見 partitions.csv不同芯片的編譯配置見sdkconfig.defaults及sdkconfig.defaults.esp32p4、sdkconfig.defaults.esp32s2、sdkconfig.defaults.esp32s3、sdkconfig.defaults.esp32s31。前端源碼結(jié)構(gòu)頁面是如何組織起來的前端源碼frontend_source/src采用典型的 Vue 3 組合式 Pinia 狀態(tài)管理結(jié)構(gòu)文件職責(zé)main.ts應(yīng)用入口createApp(App)后注冊全部插件并掛載#appApp.vue根組件v-approuter-view /pages/index.vue主頁相機(jī)卡片列表 Add Camera 選擇區(qū)components/CameraCard.vue單個(gè)相機(jī)的預(yù)覽卡片負(fù)責(zé)拉流、截圖、關(guān)閉store/mainstore.tsPinia store集中管理相機(jī)列表與激活狀態(tài)輪詢后端狀態(tài)plugins/index.ts 與 plugins/vuetify.ts插件注冊與 Vuetify 初始化router/index.ts路由配置utils.ts類型定義Camera、Resolution、CameraActivedutils.ts中定義的三個(gè)核心類型貫穿前后端通信export interface Resolution { format: string; // 如 MJPEG width: number; height: number; index: number; // 分辨率在相機(jī)能力列表中的索引 } export interface Camera { id: string | number; resolutions: Resolution[]; } export interface CameraActived { id: string | number; resolution: Resolution; }前端與 ESP32 后端的 API 契約頁面不是純展示而是通過一系列 HTTP API 與設(shè)備端 app_https.c 交互源碼中可見GET /api/cameras、GET /api/stream/*等路由的注冊位置。綜合前端源碼可以還原出完整的 API 契約1. 輪詢狀態(tài)GET /api/camerasmainstore.ts 中的fetchServerSideStatus()每 2 秒輪詢一次該接口pages/index.vue 中setTimeout(updateServerSideStatus, 2000)const response await fetch(/api/cameras); const resjson await response.json();返回的 JSON 包含三個(gè)關(guān)鍵字段limitnumber設(shè)備端允許同時(shí)激活的相機(jī)數(shù)量上限寫入cameraNumberLimit。主頁據(jù)此判斷是否仍顯示 Add Camera 卡片camerasCamera[]當(dāng)前連接的全部相機(jī)及其支持的分辨率列表activatedCameraActived[]設(shè)備端當(dāng)前已激活的相機(jī)。store 隨后合并客戶端本地激活狀態(tài)與服務(wù)器端狀態(tài)計(jì)算可選的相機(jī)列表clientCameraOptions已被本頁面激活的相機(jī)、以及已被其他客戶端在服務(wù)端激活的相機(jī)都會(huì)被過濾或降級(jí)處理避免重復(fù)搶占同一路相機(jī)。2. 激活相機(jī)POST /api/activeCameraCard.vue 在組件掛載前向后端發(fā)送激活請求const activeEndPoint new URL(/api/active, location.href) fetch(activeEndPoint, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id: props.camId, resolution: { format: props.resolution.format ?? undefined, width: props.resolution.width, height: props.resolution.height, index: props.resolution.index, } }) })請求體攜帶相機(jī) id 與目標(biāo)分辨率格式、寬高、能力索引。激活成功后頁面狀態(tài)置為pending并開始加載視頻流若失敗如response.ok為 false則顯示對應(yīng)的錯(cuò)誤提示。3. 拉取視頻流GET /api/stream/{camId}激活成功后CameraCard.vue構(gòu)造帶時(shí)間戳的流地址作為img的srcconst cameraImageSrcUrl new URL(/api/stream/${props.camId}, location.href) cameraImageSrcUrl.searchParams.set(ts, String(Date.now())) cameraImageSrc.value cameraImageSrcUrl.hrefESP32 后端會(huì)持續(xù)推送 MJPEG 幀數(shù)據(jù)GET /api/stream/*路由見 app_https.c瀏覽器通過img直接渲染該 multipart MJPEG 流。這也是 README 中僅支持預(yù)覽 MJPEG 格式圖像以及不推薦 Safari 瀏覽器的深層原因——Safari 對 MJPEG multipart 流的支持存在限制。4. 關(guān)閉相機(jī)POST /api/deactivate點(diǎn)擊卡片右上角的關(guān)閉按鈕時(shí)quit()方法先讓圖片指向/api/404停止渲染再向后端發(fā)送反激活請求const deactivateEndpoint new URL(/api/deactivate, location.href) await fetch(deactivateEndpoint, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id: props.camId }) })組件卸載onBeforeUnmount時(shí)也會(huì)調(diào)用quit()確保切頁或刷新后不會(huì)在設(shè)備端留下長期占用的激活通道。頁面交互細(xì)節(jié)加載狀態(tài)、錯(cuò)誤處理與幀截圖CameraCard.vue通過cameraStatus狀態(tài)機(jī)pending | normal | err管理相機(jī)卡片的三種呈現(xiàn)形態(tài)pending激活請求已發(fā)出、首幀未到達(dá)顯示圓形加載動(dòng)畫v-progress-circularnormal首幀成功加載onImageLoad正常展示實(shí)時(shí)畫面err加載失敗或超時(shí)覆蓋層顯示錯(cuò)誤文案。兩個(gè)值得注意的健壯性設(shè)計(jì)首幀超時(shí)保護(hù)激活后若 12 秒內(nèi)沒有渲染出任何幀setPendingTimeout自動(dòng)切換為錯(cuò)誤態(tài)并提示 Stream started but no frame was rendered避免頁面無限轉(zhuǎn)圈錯(cuò)誤文案細(xì)分圖片加載失敗提示 Failed to render MJPEG stream激活 HTTP 失敗則透傳響應(yīng)狀態(tài)碼Failed to activate camera (${response.status})。幀截圖功能README 提到的網(wǎng)頁可保存當(dāng)前幀由captureFrame()實(shí)現(xiàn)其原理是 Canvas 快照而非后端截圖canvas.value.width camImageElement.width canvas.value.height camImageElement.height ctx.drawImage(camImageElement, 0, 0, canvas.value.width, canvas.value.height) capturedImage.value canvas.value.toDataURL(image/jpeg) const link document.createElement(a) link.href capturedImage.value link.download capture_${props.camId}_${Date.now()}.jpg link.click()流程為把當(dāng)前img元素繪制到隱藏canvas上 →toDataURL(image/jpeg)導(dǎo)出 JPEG → 創(chuàng)建臨時(shí)a標(biāo)簽觸發(fā)瀏覽器下載文件名形如capture_0_1720000000000.jpg。本地開發(fā)調(diào)試建議結(jié)合 vite.config.mts 中的vite-plugin-mock-server與pnpm dev可以脫離硬件先行開發(fā)界面在frontend_source目錄執(zhí)行pnpm devVite 開發(fā)服務(wù)器默認(rèn)監(jiān)聽http://localhost:3000借助 mock 插件模擬/api/cameras等接口數(shù)據(jù)插件配置在vite.config.mts中通過mockServer()啟用驗(yàn)證頁面布局與交互邏輯界面確認(rèn)無誤后執(zhí)行pnpm build與./../scripts/gzip_webserver_files.sh回到固件工程重新編譯燒錄即可在設(shè)備上體驗(yàn)完整功能。如需對照中文說明倉庫內(nèi)還提供了 README_zh.md內(nèi)容與本指南核心編譯步驟一致。總結(jié)USB Hub 雙攝像頭示例的前端組件frontend_source是一套完整的 Vue 3 Vuetify 3 Pinia Vite 工程其開發(fā)閉環(huán)清晰可控pnpm install → pnpm build → gzip_webserver_files.sh → 固件編譯燒錄。頁面通過GET /api/cameras輪詢相機(jī)清單、POST /api/active激活指定分辨率的相機(jī)、GET /api/stream/{id}渲染 MJPEG 流、POST /api/deactivate釋放資源配合 Canvas 實(shí)現(xiàn)前端幀截圖構(gòu)成了一個(gè)可在瀏覽器中管理多路 USB 相機(jī)的完整 Web 應(yīng)用。掌握上述構(gòu)建流程與 API 契約后無論是修改界面樣式、增加新的控制按鈕還是對接后端新增能力都可以在現(xiàn)有源碼基礎(chǔ)上快速展開?!久赓M(fèi)下載鏈接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考