diff --git a/Makefile b/Makefile index cf05d82..43ff486 100644 --- a/Makefile +++ b/Makefile @@ -16,16 +16,20 @@ SRCS = \ src/views/VUMeterView.cpp \ src/views/WebcamControlsView.cpp \ src/views/LEDView.cpp \ + src/views/DeskbarReplicant.cpp \ src/webcam/WebcamRoster.cpp \ src/webcam/WebcamDevice.cpp \ src/webcam/VideoConsumer.cpp \ src/webcam/AudioConsumer.cpp \ src/webcam/USBVideoParser.cpp \ + src/webcam/VirtualProducer.cpp \ src/mcp/MCPServer.cpp \ src/utils/ExportUtils.cpp \ src/utils/IconUtils.cpp \ src/utils/StreamServer.cpp \ - src/utils/VideoRecorder.cpp + src/utils/VideoRecorder.cpp \ + src/utils/NotificationUtils.cpp \ + src/utils/VideoFilter.cpp RDEFS = resources/BubiCam.rdef @@ -39,7 +43,7 @@ LOCAL_INCLUDE_PATHS = src src/views src/webcam src/mcp src/utils OPTIMIZE := FULL -LOCALES = +LOCALES = en it de zh ja DEFINES = diff --git a/docs/libbubicapture/README.it.md b/docs/libbubicapture/README.it.md new file mode 100644 index 0000000..781c00b --- /dev/null +++ b/docs/libbubicapture/README.it.md @@ -0,0 +1,255 @@ +# libbubicapture + +Una piccola libreria di cattura per Haiku: elenca le webcam USB tramite il Media +Kit e ti consegna i frame video già decodificati (e i livelli audio), senza il +boilerplate del Media Kit. + +È il componente `src/webcam/` di BubiCam, impacchettato perché altre +applicazioni possano riusarlo. BubiCam ne è un consumatore; la tua app può +essere un altro. + +> Stato: questo documento descrive il contratto pubblico del componente di +> cattura. Il codice è in `src/webcam/`. Una volta estratto come libreria a sé, +> mantiene le stesse classi e lo stesso contratto descritto qui. + +--- + +## Cosa fa per te + +- Trova le webcam registrate nel Media Kit (`WebcamRoster`). +- Si connette a un device e avvia la pipeline di cattura (`WebcamDevice`). +- Converte ogni formato in ingresso (MJPEG, YUYV, I420, NV12, NV21, UYVY, RGB) in + un unico formato pixel pronto all'uso e posta ogni frame al tuo `BLooper`. +- Riporta le statistiche di cattura (FPS, frame catturati, frame persi). +- Riporta i livelli di picco audio (sinistro/destro) per i VU meter. + +Cosa **non** fa: encoding, registrazione, streaming di rete o UI. Quelli sono +compito dell'applicazione. La libreria ti dà solo frame puliti. + +--- + +## Dipendenze + +Solo librerie di sistema Haiku — nessun pacchetto di terze parti: + +``` +be media tracker translation device shared jpeg +``` + +`media` esegue la cattura, `jpeg` (libjpeg-turbo) decodifica le webcam MJPEG. + +--- + +## Il modello mentale (leggi prima questo) + +La libreria è **push-based** e costruita sul modello looper/messaggi di Haiku. +Non interroghi tu i frame: dai al device un `BLooper` e la libreria **gli posta +un `BMessage` per ogni frame**, da un thread del Media Kit. + +``` + webcam (producer del Media Kit) + │ buffer grezzi + ▼ + WebcamDevice ──► VideoConsumer interno ──► conversione di formato + │ + BMessage('frcv') + puntatore "bitmap" + ▼ + IL TUO BLooper::MessageReceived() +``` + +Tre fatti che ne derivano, e che devi rispettare: + +1. **Il tuo handler gira sul thread del looper, non su un thread di cattura.** + Valgono le regole standard di Haiku: blocca le finestre prima di toccare le + view, tieni l'handler breve. +2. **Il `BBitmap` del frame è di proprietà della libreria ed è valido solo dentro + quel messaggio.** Se ti servono i pixel dopo il ritorno, **copiali**. Non fare + `delete` del bitmap e non conservare il puntatore. +3. **I frame vengono scartati, non accodati, se rimani indietro.** La pipeline usa + 3 buffer; un handler lento significa frame persi (vedi `FramesDropped()`), mai + crescita illimitata della memoria. + +--- + +## Formato dei frame + +Ogni frame consegnato è un `BBitmap` in **`B_RGB32`** (ordine byte BGRA in +memoria, 4 byte per pixel). Un solo formato, sempre — non devi mai ramificare sul +codec in ingresso. Le dimensioni le prendi da `bitmap->Bounds()`, i pixel da +`bitmap->Bits()` con `bitmap->BytesPerRow()`. + +--- + +## Avvio rapido + +Un capturer minimale senza interfaccia: enumera, avvia il primo device, conta i +frame. + +```cpp +#include +#include +#include +#include "WebcamRoster.h" +#include "WebcamDevice.h" + +// La costante del messaggio frame che la libreria posta (vedi "Contratto del +// messaggio frame"). +static const uint32 kFrameReceived = 'frcv'; + +class CaptureLooper : public BLooper { +public: + CaptureLooper() : BLooper("capture") {} + + void MessageReceived(BMessage* msg) override { + if (msg->what == kFrameReceived) { + BBitmap* frame = NULL; + if (msg->FindPointer("bitmap", (void**)&frame) == B_OK && frame) { + // 'frame' è valido SOLO qui. Copialo se ti serve dopo. + fCount++; + } + return; + } + BLooper::MessageReceived(msg); + } + + int32 fCount = 0; +}; + +int main() { + BApplication app("application/x-vnd.example-capture"); + + WebcamRoster roster; + roster.EnumerateDevices(); + if (roster.CountDevices() == 0) + return 1; + + WebcamDevice* dev = roster.DeviceAt(0); + + CaptureLooper* looper = new CaptureLooper(); + looper->Run(); + + dev->StartCapture(looper); // i frame ora scorrono verso il looper + app.Run(); // ... fai il tuo lavoro ... + dev->StopCapture(); + return 0; +} +``` + +Questa è tutta la superficie di integrazione: enumera, `StartCapture(looper)`, +leggi i frame in `MessageReceived`, `StopCapture()`. + +--- + +## Riferimento API + +### `WebcamRoster` — trovare i device + +`WebcamRoster` è un `BHandler`. L'enumerazione funziona da sola; le notifiche di +hot-plug richiedono di aggiungerlo a un `BLooper` in esecuzione. + +| Metodo | Scopo | +|---|---| +| `status_t EnumerateDevices()` | Scansiona il Media Kit per le webcam. Chiamalo all'avvio, o dopo un evento di hot-plug. | +| `int32 CountDevices()` | Numero di webcam trovate. | +| `WebcamDevice* DeviceAt(int32 i)` | Device per indice. Di proprietà del roster — non fare delete. | +| `WebcamDevice* DeviceByName(const char* name)` | Device per nome, oppure `NULL`. | +| `status_t StartWatching()` / `void StopWatching()` | Abilita/disabilita le notifiche di hot-plug. | + +Quando i device cambiano, il roster posta `MSG_DEVICES_CHANGED` (`'dvch'`) al +looper a cui è collegato. Ri-esegui `EnumerateDevices()` e aggiorna la tua lista. + +### `WebcamDevice` — catturare da una webcam + +Controllo della cattura: + +| Metodo | Scopo | +|---|---| +| `status_t StartCapture(BLooper* target, uint32 frameMessage = MSG_WEBCAM_FRAME, uint32 audioLevelMessage = MSG_WEBCAM_AUDIO_LEVEL)` | Connette la pipeline e inizia a postare i frame a `target`. Passa i tuoi `what` per integrarti con un protocollo di messaggi tuo; i default restano `'frcv'`/`'audl'`. | +| `void StopCapture()` | Ferma e disconnette. Sicuro da chiamare da qualsiasi thread. | +| `bool IsCapturing()` | Se la cattura è in corso. | + +Per catturare i campioni audio (non solo i livelli) — per registrazione o +encoding — passa un `AudioSink` al device: `device->SetAudioSink(mioSink)`. Il +sink riceve il PCM grezzo direttamente dal thread audio (vedi `AudioSink.h`). + +Selezione del formato (chiamare **prima** di `StartCapture`): + +| Metodo | Scopo | +|---|---| +| `const BObjectList& SupportedFormats()` | Risoluzioni/frame rate dichiarati dal device. | +| `void SetRequestedFormat(const VideoFormat&)` | Richiede una risoluzione/rate specifica. Il driver può negoziare qualcosa di vicino. | +| `VideoFormat CurrentFormat()` | Il formato negoziato. Verificalo dopo i primi frame. | + +Sorgente audio (chiamare **prima** di `StartCapture`): + +| Metodo | Scopo | +|---|---| +| `bool SupportsAudio()` | Se la webcam espone un microfono. | +| `void SetAudioNodeID(int32 id)` | `-1` = auto, `0` = nessun audio, `>0` = un node id specifico del Media Kit. | + +Statistiche (interrogabili in qualsiasi momento durante la cattura): + +| Metodo | Scopo | +|---|---| +| `uint32 FramesCaptured()` | Frame consegnati dall'avvio. | +| `uint32 FramesDropped()` | Frame persi perché il consumatore è rimasto indietro. | +| `float CurrentFPS()` | Frame rate corrente (media mobile). | + +Identificazione (USB / driver), utile per UI e diagnostica: +`Name()`, `VendorID()`, `ProductID()`, `ProductName()`, `SerialNumber()`, +`DriverName()`, `DriverVersion()`, e `GetUSBInfo()` / `GetDriverInfo()` per le +struct complete. + +### Contratto del messaggio frame + +`StartCapture` posta, per ogni frame, al tuo looper: + +- `what`: `'frcv'` (`MSG_FRAME_RECEIVED`) +- campo `"bitmap"`: un `BBitmap*` (`FindPointer`), `B_RGB32`, **in prestito** — + valido solo durante il messaggio, di proprietà della libreria. + +Messaggi di livello audio (quando l'audio è attivo): + +- `what`: `'audl'` (`MSG_WEBCAM_AUDIO_LEVEL`) +- campi `"left"` / `"right"`: livelli di picco `float` in `0.0 .. 1.0`. + +> I default sono `MSG_WEBCAM_FRAME` (`'frcv'`) e `MSG_WEBCAM_AUDIO_LEVEL` +> (`'audl'`), definiti in `WebcamDevice.h` e di proprietà della libreria. +> Sovrascrivili per-cattura con i parametri di `StartCapture` qui sopra — senza +> toccare i sorgenti della libreria. + +--- + +## Threading e ownership — le regole che fanno male + +- **I bitmap sono in prestito.** Validi solo dentro il messaggio `'frcv'`. Copia + per conservarli. Mai `delete`. +- **I device sono di proprietà del roster.** Non fare delete di `WebcamDevice*`; + tieni vivo il `WebcamRoster` finché usi i suoi device. +- **`StopCapture()` è sincrono e thread-safe.** Dopo il ritorno, nessun altro + messaggio frame verrà postato per quel device. +- **Un looper, un thread di handler.** Il tuo handler dei frame non deve + bloccare. Sposta encoding/IO su un thread tuo; la libreria continua a catturare + e semplicemente scarta i frame che non riesci a smaltire. + +--- + +## Compilazione + +Le classi sono C++ semplice con il Media Kit di Haiku. Per usarle oggi, aggiungi +i sorgenti al tuo build e linka le librerie di sistema: + +``` +SRCS += WebcamRoster.cpp WebcamDevice.cpp VideoConsumer.cpp \ + AudioConsumer.cpp USBVideoParser.cpp +LIBS += be media tracker translation device shared jpeg +``` + +(Quando sarà impacchettata come libreria shared/static, linka `libbubicapture` e +aggiungi i suoi header all'include path. Il contratto qui sopra non cambia.) + +--- + +## Licenza + +MIT, come BubiCam. Vedi `LICENSE`. diff --git a/docs/libbubicapture/README.md b/docs/libbubicapture/README.md new file mode 100644 index 0000000..e1997a0 --- /dev/null +++ b/docs/libbubicapture/README.md @@ -0,0 +1,252 @@ +# libbubicapture + +A small Haiku capture library: it enumerates USB webcams through the Media Kit +and hands you decoded video frames (and audio levels) with no Media Kit +boilerplate. + +It is the `src/webcam/` component of BubiCam, packaged so other applications can +reuse it. BubiCam is one consumer; your app can be another. + +> Status: this document describes the public contract of the capture component. +> The code lives in `src/webcam/`. When extracted as a standalone library it +> keeps the same classes and the same contract described here. + +--- + +## What it does for you + +- Finds webcams registered with the Media Kit (`WebcamRoster`). +- Connects to a device and runs the capture pipeline (`WebcamDevice`). +- Converts every input format (MJPEG, YUYV, I420, NV12, NV21, UYVY, RGB) to a + single, ready-to-use pixel format and posts each frame to your `BLooper`. +- Reports capture statistics (FPS, frames captured, frames dropped). +- Reports audio peak levels (left/right) for VU meters. + +What it does **not** do: encoding, recording, network streaming, or UI. Those +are the application's job. The library only gives you clean frames. + +--- + +## Dependencies + +Haiku system libraries only — no third-party packages: + +``` +be media tracker translation device shared jpeg +``` + +`media` does the capture, `jpeg` (libjpeg-turbo) decodes MJPEG webcams. + +--- + +## The mental model (read this first) + +The library is **push-based** and built on Haiku's looper/messaging model. You +do not poll for frames. You give the device a `BLooper`, and the library +**posts a `BMessage` to it for every frame**, from a Media Kit thread. + +``` + webcam (Media Kit producer) + │ raw buffers + ▼ + WebcamDevice ──► internal VideoConsumer ──► format conversion + │ + BMessage('frcv') + "bitmap" pointer + ▼ + YOUR BLooper::MessageReceived() +``` + +Three facts that follow from this, and that you must respect: + +1. **Your handler runs on the looper thread, not a capture thread.** Standard + Haiku rules apply — lock windows before touching views, keep the handler + short. +2. **The frame `BBitmap` is owned by the library and is only valid inside that + message.** If you need the pixels after returning, **copy them**. Do not + `delete` the bitmap and do not store the pointer. +3. **Frames are dropped, not queued, when you fall behind.** The pipeline uses + 3 buffers; a slow handler means dropped frames (see `FramesDropped()`), never + unbounded memory growth. + +--- + +## Frame format + +Every delivered frame is a `BBitmap` in **`B_RGB32`** (BGRA byte order in +memory, 4 bytes per pixel). One format, always — you never branch on the input +codec. Get dimensions from `bitmap->Bounds()` and pixels from `bitmap->Bits()` +with `bitmap->BytesPerRow()`. + +--- + +## Quick start + +A minimal headless capturer: enumerate, start the first device, count frames. + +```cpp +#include +#include +#include +#include "WebcamRoster.h" +#include "WebcamDevice.h" + +// The frame message constant the library posts (see "Frame message contract"). +static const uint32 kFrameReceived = 'frcv'; + +class CaptureLooper : public BLooper { +public: + CaptureLooper() : BLooper("capture") {} + + void MessageReceived(BMessage* msg) override { + if (msg->what == kFrameReceived) { + BBitmap* frame = NULL; + if (msg->FindPointer("bitmap", (void**)&frame) == B_OK && frame) { + // 'frame' is valid ONLY here. Copy if you need to keep it. + fCount++; + } + return; + } + BLooper::MessageReceived(msg); + } + + int32 fCount = 0; +}; + +int main() { + BApplication app("application/x-vnd.example-capture"); + + WebcamRoster roster; + roster.EnumerateDevices(); + if (roster.CountDevices() == 0) + return 1; + + WebcamDevice* dev = roster.DeviceAt(0); + + CaptureLooper* looper = new CaptureLooper(); + looper->Run(); + + dev->StartCapture(looper); // frames now flow to looper + app.Run(); // ... do work ... + dev->StopCapture(); + return 0; +} +``` + +That is the whole integration surface: enumerate, `StartCapture(looper)`, read +frames in `MessageReceived`, `StopCapture()`. + +--- + +## API reference + +### `WebcamRoster` — find devices + +`WebcamRoster` is a `BHandler`. Enumeration works standalone; live hot-plug +notifications require adding it to a running `BLooper`. + +| Method | Purpose | +|---|---| +| `status_t EnumerateDevices()` | Scan the Media Kit for webcams. Call once at startup, or after a hot-plug event. | +| `int32 CountDevices()` | Number of webcams found. | +| `WebcamDevice* DeviceAt(int32 i)` | Device by index. Owned by the roster — do not delete. | +| `WebcamDevice* DeviceByName(const char* name)` | Device by name, or `NULL`. | +| `status_t StartWatching()` / `void StopWatching()` | Enable/disable hot-plug notifications. | + +When devices change, the roster posts `MSG_DEVICES_CHANGED` (`'dvch'`) to the +looper it is attached to. Re-run `EnumerateDevices()` and refresh your list. + +### `WebcamDevice` — capture from one webcam + +Capture control: + +| Method | Purpose | +|---|---| +| `status_t StartCapture(BLooper* target, uint32 frameMessage = MSG_WEBCAM_FRAME, uint32 audioLevelMessage = MSG_WEBCAM_AUDIO_LEVEL)` | Connect the pipeline and start posting frames to `target`. Pass your own `what` codes to integrate with a custom message protocol; the defaults keep `'frcv'`/`'audl'`. | +| `void StopCapture()` | Stop and disconnect. Safe to call from any thread. | +| `bool IsCapturing()` | Whether capture is running. | + +To capture audio samples (not just levels) — for recording or encoding — give +the device an `AudioSink`: `device->SetAudioSink(mySink)`. The sink receives raw +PCM directly from the audio thread (see `AudioSink.h`). + +Format selection (call **before** `StartCapture`): + +| Method | Purpose | +|---|---| +| `const BObjectList& SupportedFormats()` | Resolutions/frame rates the device advertises. | +| `void SetRequestedFormat(const VideoFormat&)` | Ask for a specific resolution/rate. The driver may negotiate something close. | +| `VideoFormat CurrentFormat()` | The negotiated format. Verify after the first frames arrive. | + +Audio source (call **before** `StartCapture`): + +| Method | Purpose | +|---|---| +| `bool SupportsAudio()` | Whether the webcam exposes a microphone. | +| `void SetAudioNodeID(int32 id)` | `-1` = auto, `0` = no audio, `>0` = a specific Media Kit node id. | + +Statistics (poll any time during capture): + +| Method | Purpose | +|---|---| +| `uint32 FramesCaptured()` | Frames delivered since start. | +| `uint32 FramesDropped()` | Frames dropped because the consumer fell behind. | +| `float CurrentFPS()` | Rolling frame rate. | + +Identification (USB / driver), useful for UI and diagnostics: +`Name()`, `VendorID()`, `ProductID()`, `ProductName()`, `SerialNumber()`, +`DriverName()`, `DriverVersion()`, and `GetUSBInfo()` / `GetDriverInfo()` for the +full structs. + +### Frame message contract + +`StartCapture` posts, for every frame, to your looper: + +- `what`: `'frcv'` (`MSG_FRAME_RECEIVED`) +- field `"bitmap"`: a `BBitmap*` (`FindPointer`), `B_RGB32`, **borrowed** — valid + only during the message, owned by the library. + +Audio level messages (when audio is active): + +- `what`: `'audl'` (`MSG_WEBCAM_AUDIO_LEVEL`) +- fields `"left"` / `"right"`: `float` peak levels in `0.0 .. 1.0`. + +> The defaults are `MSG_WEBCAM_FRAME` (`'frcv'`) and `MSG_WEBCAM_AUDIO_LEVEL` +> (`'audl'`), defined in `WebcamDevice.h` and owned by the library. Override +> them per-capture via the `StartCapture` parameters above — no need to touch +> the library source. + +--- + +## Threading and ownership — the rules that bite + +- **Bitmaps are borrowed.** Valid only inside the `'frcv'` message. Copy to keep. + Never `delete`. +- **Devices are owned by the roster.** Do not delete `WebcamDevice*`; keep the + `WebcamRoster` alive for as long as you use its devices. +- **`StopCapture()` is synchronous and thread-safe.** After it returns, no more + frame messages will be posted for that device. +- **One looper, one handler thread.** Your frame handler must not block. Offload + encoding/IO to your own thread; the library will keep capturing and simply + drop frames you can't keep up with. + +--- + +## Building + +The classes are plain C++ with the Haiku Media Kit. To use them today, add the +sources to your build and link the system libraries: + +``` +SRCS += WebcamRoster.cpp WebcamDevice.cpp VideoConsumer.cpp \ + AudioConsumer.cpp USBVideoParser.cpp +LIBS += be media tracker translation device shared jpeg +``` + +(When packaged as a shared/static library, link `libbubicapture` instead and add +its headers to your include path. The contract above is unchanged.) + +--- + +## License + +MIT, same as BubiCam. See `LICENSE`. diff --git a/locales/de.catkeys b/locales/de.catkeys new file mode 100644 index 0000000..bf497c6 --- /dev/null +++ b/locales/de.catkeys @@ -0,0 +1,25 @@ +1 German application/x-vnd.BubiCam 1 +File MainWindow Datei +About BubiCam… MainWindow Über BubiCam… +Take Screenshot MainWindow Bildschirmfoto aufnehmen +Start Recording MainWindow Aufnahme starten +Stop Recording MainWindow Aufnahme stoppen +Export Info as Text… MainWindow Info als Text exportieren… +Export Info as JSON… MainWindow Info als JSON exportieren… +Quit MainWindow Beenden +Webcam MainWindow Webcam +Refresh Devices MainWindow Geräte aktualisieren +Format MainWindow Format +Control MainWindow Steuerung +Start Preview MainWindow Vorschau starten +Stop Preview MainWindow Vorschau stoppen +Force Stop (Driver Frozen) MainWindow Erzwungener Stopp (Treiber hängt) +Reconnect MainWindow Neu verbinden +Show Controls Panel MainWindow Steuerungspanel anzeigen +Restore Factory Defaults MainWindow Werkseinstellungen wiederherstellen +Auto-start Preview on Launch MainWindow Vorschau beim Start automatisch starten +Audio MainWindow Audio +Tools MainWindow Werkzeuge +No webcams found MainWindow Keine Webcams gefunden +No webcams detected MainWindow Keine Webcams erkannt +Select a webcam from the menu MainWindow Webcam aus dem Menü auswählen diff --git a/locales/en.catkeys b/locales/en.catkeys new file mode 100644 index 0000000..e1a04da --- /dev/null +++ b/locales/en.catkeys @@ -0,0 +1,25 @@ +1 English application/x-vnd.BubiCam 1 +File MainWindow File +About BubiCam… MainWindow About BubiCam… +Take Screenshot MainWindow Take Screenshot +Start Recording MainWindow Start Recording +Stop Recording MainWindow Stop Recording +Export Info as Text… MainWindow Export Info as Text… +Export Info as JSON… MainWindow Export Info as JSON… +Quit MainWindow Quit +Webcam MainWindow Webcam +Refresh Devices MainWindow Refresh Devices +Format MainWindow Format +Control MainWindow Control +Start Preview MainWindow Start Preview +Stop Preview MainWindow Stop Preview +Force Stop (Driver Frozen) MainWindow Force Stop (Driver Frozen) +Reconnect MainWindow Reconnect +Show Controls Panel MainWindow Show Controls Panel +Restore Factory Defaults MainWindow Restore Factory Defaults +Auto-start Preview on Launch MainWindow Auto-start Preview on Launch +Audio MainWindow Audio +Tools MainWindow Tools +No webcams found MainWindow No webcams found +No webcams detected MainWindow No webcams detected +Select a webcam from the menu MainWindow Select a webcam from the menu diff --git a/locales/it.catkeys b/locales/it.catkeys new file mode 100644 index 0000000..637f7c5 --- /dev/null +++ b/locales/it.catkeys @@ -0,0 +1,25 @@ +1 Italian application/x-vnd.BubiCam 1 +File MainWindow File +About BubiCam… MainWindow Informazioni su BubiCam… +Take Screenshot MainWindow Cattura schermata +Start Recording MainWindow Avvia registrazione +Stop Recording MainWindow Ferma registrazione +Export Info as Text… MainWindow Esporta info come testo… +Export Info as JSON… MainWindow Esporta info come JSON… +Quit MainWindow Esci +Webcam MainWindow Webcam +Refresh Devices MainWindow Aggiorna dispositivi +Format MainWindow Formato +Control MainWindow Controllo +Start Preview MainWindow Avvia anteprima +Stop Preview MainWindow Ferma anteprima +Force Stop (Driver Frozen) MainWindow Arresto forzato (driver bloccato) +Reconnect MainWindow Riconnetti +Show Controls Panel MainWindow Mostra pannello controlli +Restore Factory Defaults MainWindow Ripristina impostazioni predefinite +Auto-start Preview on Launch MainWindow Avvio automatico anteprima +Audio MainWindow Audio +Tools MainWindow Strumenti +No webcams found MainWindow Nessuna webcam trovata +No webcams detected MainWindow Nessuna webcam rilevata +Select a webcam from the menu MainWindow Seleziona una webcam dal menu diff --git a/locales/ja.catkeys b/locales/ja.catkeys new file mode 100644 index 0000000..b1fb470 --- /dev/null +++ b/locales/ja.catkeys @@ -0,0 +1,25 @@ +1 Japanese application/x-vnd.BubiCam 1 +File MainWindow ファイル +About BubiCam… MainWindow BubiCam について… +Take Screenshot MainWindow スクリーンショット撮影 +Start Recording MainWindow 録画開始 +Stop Recording MainWindow 録画停止 +Export Info as Text… MainWindow 情報をテキストとしてエクスポート… +Export Info as JSON… MainWindow 情報を JSON としてエクスポート… +Quit MainWindow 終了 +Webcam MainWindow ウェブカメラ +Refresh Devices MainWindow デバイス更新 +Format MainWindow フォーマット +Control MainWindow 操作 +Start Preview MainWindow プレビュー開始 +Stop Preview MainWindow プレビュー停止 +Force Stop (Driver Frozen) MainWindow 強制停止(ドライバーフリーズ) +Reconnect MainWindow 再接続 +Show Controls Panel MainWindow コントロールパネル表示 +Restore Factory Defaults MainWindow 初期設定に戻す +Auto-start Preview on Launch MainWindow 起動時にプレビューを自動開始 +Audio MainWindow オーディオ +Tools MainWindow ツール +No webcams found MainWindow ウェブカメラが見つかりません +No webcams detected MainWindow ウェブカメラが検出されません +Select a webcam from the menu MainWindow メニューからウェブカメラを選択 diff --git a/locales/zh.catkeys b/locales/zh.catkeys new file mode 100644 index 0000000..a0c5861 --- /dev/null +++ b/locales/zh.catkeys @@ -0,0 +1,25 @@ +1 Chinese application/x-vnd.BubiCam 1 +File MainWindow 文件 +About BubiCam… MainWindow 关于 BubiCam… +Take Screenshot MainWindow 截取屏幕 +Start Recording MainWindow 开始录制 +Stop Recording MainWindow 停止录制 +Export Info as Text… MainWindow 导出信息为文本… +Export Info as JSON… MainWindow 导出信息为 JSON… +Quit MainWindow 退出 +Webcam MainWindow 摄像头 +Refresh Devices MainWindow 刷新设备 +Format MainWindow 格式 +Control MainWindow 控制 +Start Preview MainWindow 开始预览 +Stop Preview MainWindow 停止预览 +Force Stop (Driver Frozen) MainWindow 强制停止(驱动冻结) +Reconnect MainWindow 重新连接 +Show Controls Panel MainWindow 显示控制面板 +Restore Factory Defaults MainWindow 恢复出厂设置 +Auto-start Preview on Launch MainWindow 启动时自动开始预览 +Audio MainWindow 音频 +Tools MainWindow 工具 +No webcams found MainWindow 未找到摄像头 +No webcams detected MainWindow 未检测到摄像头 +Select a webcam from the menu MainWindow 从菜单中选择摄像头 diff --git a/src/BubiCamApp.cpp b/src/BubiCamApp.cpp index 5a82895..9ec4f31 100644 --- a/src/BubiCamApp.cpp +++ b/src/BubiCamApp.cpp @@ -6,9 +6,18 @@ #include "BubiCamApp.h" #include "MainWindow.h" +#include "WebcamRoster.h" +#include "WebcamDevice.h" #include #include +#include +#include +#include + +#include +#include +#include #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "BubiCamApp" @@ -19,7 +28,9 @@ const char* kApplicationSignature = "application/x-vnd.BubiCam"; BubiCamApp::BubiCamApp() : BApplication(kApplicationSignature), - fMainWindow(NULL) + fMainWindow(NULL), + fHeadless(false), + fHeadlessDuration(0) { } @@ -29,9 +40,64 @@ BubiCamApp::~BubiCamApp() } +static void +_RegisterMIMEType(const char* mimeType, const char* shortDesc, + const char* longDesc, const char* extension, const char* preferredApp) +{ + BMimeType mime(mimeType); + if (mime.IsInstalled()) + return; + + mime.Install(); + mime.SetShortDescription(shortDesc); + mime.SetLongDescription(longDesc); + mime.SetPreferredApp(preferredApp); + + if (extension != NULL) { + BMessage extensions; + extensions.AddString("extensions", extension); + mime.SetFileExtensions(&extensions); + } +} + + void BubiCamApp::ReadyToRun() { + // Register custom MIME types for BubiCam file formats + _RegisterMIMEType( + "application/x-vnd.BubiCam-preset", + "BubiCam Preset", + "BubiCam webcam control preset file", + "bcpreset", + kApplicationSignature); + + _RegisterMIMEType( + "application/x-vnd.BubiCam-report", + "BubiCam Report", + "BubiCam diagnostic report file", + "bcreport", + kApplicationSignature); + + _RegisterMIMEType( + "text/x-vnd.BubiCam-testresults-csv", + "BubiCam Test CSV", + "BubiCam test results in CSV format", + "csv", + NULL); // Don't claim ownership of .csv + + _RegisterMIMEType( + "application/x-vnd.BubiCam-testresults-json", + "BubiCam Test JSON", + "BubiCam test results in JSON format", + NULL, + NULL); + + if (fHeadless) { + _RunHeadless(); + return; + } + fMainWindow = new MainWindow(); fMainWindow->Show(); } @@ -75,6 +141,97 @@ BubiCamApp::AboutRequested() } +void +BubiCamApp::ArgvReceived(int32 argc, char** argv) +{ + for (int32 i = 1; i < argc; i++) { + if (strcmp(argv[i], "--headless") == 0) { + fHeadless = true; + } else if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc) { + fHeadlessDuration = atoi(argv[++i]); + } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + _PrintUsage(); + PostMessage(B_QUIT_REQUESTED); + return; + } + } +} + + +void +BubiCamApp::_PrintUsage() +{ + fprintf(stderr, + "BubiCam - Webcam Driver Tester for Haiku OS\n" + "\n" + "Usage: BubiCam [options]\n" + "\n" + "Options:\n" + " --headless Run without GUI (streaming server only)\n" + " --duration Headless mode duration (0 = run until killed)\n" + " --help, -h Show this help message\n" + "\n" + "Headless mode starts the MJPEG streaming server on port 8080\n" + "and the MCP server on port 9847. Access the live stream at:\n" + " http://localhost:8080/stream\n" + " http://localhost:8080/snapshot\n" + "\n" + "Scripting (with GUI):\n" + " hey BubiCam get Status\n" + " hey BubiCam get FPS\n" + " hey BubiCam set Streaming to true\n" + " hey BubiCam do Screenshot\n" + ); +} + + +void +BubiCamApp::_RunHeadless() +{ + fprintf(stderr, "BubiCam: headless mode\n"); + + // Enumerate webcams + WebcamRoster roster; + roster.EnumerateDevices(); + + if (roster.CountDevices() == 0) { + fprintf(stderr, "BubiCam: no webcams found, exiting\n"); + PostMessage(B_QUIT_REQUESTED); + return; + } + + WebcamDevice* device = roster.DeviceAt(0); + fprintf(stderr, "BubiCam: using '%s'\n", device->Name()); + + // Create a simple looper to receive frames (frame counting only) + BLooper* looper = new BLooper("headless_capture"); + looper->Run(); + + status_t err = device->StartCapture(looper); + if (err != B_OK) { + fprintf(stderr, "BubiCam: failed to start capture: %s\n", strerror(err)); + looper->Lock(); + looper->Quit(); + PostMessage(B_QUIT_REQUESTED); + return; + } + + fprintf(stderr, "BubiCam: capture started, streaming on port 8080\n"); + + if (fHeadlessDuration > 0) { + // Run for specified duration + snooze((bigtime_t)fHeadlessDuration * 1000000); + fprintf(stderr, "BubiCam: duration expired (%d sec), stopping\n", + (int)fHeadlessDuration); + device->StopCapture(); + looper->Lock(); + looper->Quit(); + PostMessage(B_QUIT_REQUESTED); + } + // If duration == 0, run indefinitely until quit signal +} + + int main() { diff --git a/src/BubiCamApp.h b/src/BubiCamApp.h index 213541f..f6f67f4 100644 --- a/src/BubiCamApp.h +++ b/src/BubiCamApp.h @@ -17,12 +17,18 @@ class BubiCamApp : public BApplication { virtual ~BubiCamApp(); virtual void ReadyToRun(); + virtual void ArgvReceived(int32 argc, char** argv); virtual void MessageReceived(BMessage* message); virtual bool QuitRequested(); virtual void AboutRequested(); private: + void _PrintUsage(); + void _RunHeadless(); + MainWindow* fMainWindow; + bool fHeadless; + int32 fHeadlessDuration; // seconds, 0 = until quit }; #endif // BUBICAM_APP_H diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 34bc659..fdad472 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -18,6 +18,9 @@ #include "WebcamDevice.h" #include "MCPServer.h" #include "StreamServer.h" +#include "DeskbarReplicant.h" +#include "NotificationUtils.h" +#include "VideoFilter.h" #include "ExportUtils.h" #include "IconUtils.h" #include "VideoRecorder.h" @@ -47,6 +50,7 @@ #include #include +#include #include #include @@ -133,6 +137,7 @@ MainWindow::MainWindow() fStreamServer(NULL), fStreamMenuItem(NULL), fRecorder(NULL), + fFilterChain(NULL), fTimelapseRunner(NULL), fTimelapseCount(0), fTimelapseInterval(5000000), @@ -160,6 +165,13 @@ MainWindow::MainWindow() // Create stream server fStreamServer = new StreamServer(BMessenger(this)); + // Create video filter chain with built-in filters + fFilterChain = new VideoFilterChain(); + fFilterChain->AddFilter(new GrayscaleFilter()); + fFilterChain->AddFilter(new InvertFilter()); + fFilterChain->AddFilter(new MirrorFilter()); + fFilterChain->AddFilter(new SepiaFilter()); + _BuildMenu(); _BuildLayout(); _PopulateWebcamMenu(); @@ -213,6 +225,7 @@ MainWindow::~MainWindow() delete fSavePanel; delete fLastFrame; delete fRecorder; + delete fFilterChain; // Stop stream server if (fStreamServer != NULL) { @@ -239,68 +252,98 @@ MainWindow::_BuildMenu() fMenuBar = new BMenuBar("menubar"); // File menu - BMenu* fileMenu = new BMenu("File"); - fileMenu->AddItem(new BMenuItem("About BubiCam" B_UTF8_ELLIPSIS, + BMenu* fileMenu = new BMenu(B_TRANSLATE("File")); + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("About BubiCam" B_UTF8_ELLIPSIS), new BMessage(MSG_ABOUT))); fileMenu->AddSeparatorItem(); - fileMenu->AddItem(new BMenuItem("Take Screenshot", + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Take Screenshot"), new BMessage(MSG_SCREENSHOT), 'P')); - fileMenu->AddItem(new BMenuItem("Start Recording", + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Start Recording"), new BMessage(MSG_RECORD_START), 'G')); - fileMenu->AddItem(new BMenuItem("Stop Recording", + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Stop Recording"), new BMessage(MSG_RECORD_STOP), 'G', B_SHIFT_KEY)); - fileMenu->AddItem(new BMenuItem("Export Info as Text" B_UTF8_ELLIPSIS, + { + BMenu* codecMenu = new BMenu(B_TRANSLATE("Recording Codec")); + BMenuItem* mjpegItem = new BMenuItem("Motion JPEG", + new BMessage(MSG_CODEC_MJPEG)); + mjpegItem->SetMarked(true); + codecMenu->AddItem(mjpegItem); + codecMenu->AddItem(new BMenuItem("Uncompressed RGB32", + new BMessage(MSG_CODEC_RAW))); + codecMenu->SetRadioMode(true); + fileMenu->AddItem(codecMenu); + } + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Export Info as Text" B_UTF8_ELLIPSIS), new BMessage(MSG_EXPORT_INFO), 'E')); - fileMenu->AddItem(new BMenuItem("Export Info as JSON" B_UTF8_ELLIPSIS, + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Export Info as JSON" B_UTF8_ELLIPSIS), new BMessage(MSG_EXPORT_INFO_JSON), 'E', B_SHIFT_KEY)); fileMenu->AddSeparatorItem(); - fileMenu->AddItem(new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), + fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Quit"), new BMessage(B_QUIT_REQUESTED), 'Q')); fMenuBar->AddItem(fileMenu); // Webcam menu - fWebcamMenu = new BMenu("Webcam"); - fWebcamMenu->AddItem(new BMenuItem("Refresh Devices", + fWebcamMenu = new BMenu(B_TRANSLATE("Webcam")); + fWebcamMenu->AddItem(new BMenuItem(B_TRANSLATE("Refresh Devices"), new BMessage(MSG_REFRESH_DEVICES), 'R')); fWebcamMenu->AddSeparatorItem(); fMenuBar->AddItem(fWebcamMenu); // Format menu - fFormatMenu = new BMenu("Format"); + fFormatMenu = new BMenu(B_TRANSLATE("Format")); fFormatMenu->SetEnabled(false); fMenuBar->AddItem(fFormatMenu); // Control menu - fControlMenu = new BMenu("Control"); - fControlMenu->AddItem(new BMenuItem("Start Preview", + fControlMenu = new BMenu(B_TRANSLATE("Control")); + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Start Preview"), new BMessage(MSG_WEBCAM_START), 'S')); - fControlMenu->AddItem(new BMenuItem("Stop Preview", + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Stop Preview"), new BMessage(MSG_WEBCAM_STOP), 'T')); - fControlMenu->AddItem(new BMenuItem("Force Stop (Driver Frozen)", + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Force Stop (Driver Frozen)"), new BMessage(MSG_FORCE_STOP))); - fControlMenu->AddItem(new BMenuItem("Reconnect", + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Reconnect"), new BMessage(MSG_RESTART_PREVIEW), 'R')); fControlMenu->AddSeparatorItem(); - fControlMenu->AddItem(new BMenuItem("Show Controls Panel", + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Show Controls Panel"), new BMessage(MSG_TOGGLE_CONTROLS), 'K')); - fControlMenu->AddItem(new BMenuItem("Restore Factory Defaults", + fControlMenu->AddItem(new BMenuItem(B_TRANSLATE("Restore Factory Defaults"), new BMessage(MSG_FACTORY_RESET))); fControlMenu->AddSeparatorItem(); { - BMenuItem* autoPreviewItem = new BMenuItem("Auto-start Preview on Launch", + BMenuItem* autoPreviewItem = new BMenuItem(B_TRANSLATE("Auto-start Preview on Launch"), new BMessage(MSG_TOGGLE_AUTO_PREVIEW)); autoPreviewItem->SetMarked(fAutoStartPreview); fControlMenu->AddItem(autoPreviewItem); } + fControlMenu->AddSeparatorItem(); + fControlMenu->AddItem(new BMenuItem("Toggle Syslog Panel", + new BMessage(MSG_TOGGLE_SYSLOG))); + fControlMenu->AddItem(new BMenuItem("Toggle VU Meter", + new BMessage(MSG_TOGGLE_VUBAR))); + fControlMenu->AddItem(new BMenuItem("Reset Layout", + new BMessage(MSG_RESET_LAYOUT))); fMenuBar->AddItem(fControlMenu); - // Tools menu // Audio menu - fAudioMenu = new BMenu("Audio"); + fAudioMenu = new BMenu(B_TRANSLATE("Audio")); _PopulateAudioMenu(); fMenuBar->AddItem(fAudioMenu); - fToolsMenu = new BMenu("Tools"); + // Filters menu + BMenu* filterMenu = new BMenu("Filters"); + for (int32 i = 0; i < fFilterChain->CountFilters(); i++) { + VideoFilter* filter = fFilterChain->FilterAt(i); + if (filter != NULL) { + BMessage* msg = new BMessage(MSG_FILTER_TOGGLE); + msg->AddInt32("index", i); + BMenuItem* item = new BMenuItem(filter->Name(), msg); + filterMenu->AddItem(item); + } + } + fMenuBar->AddItem(filterMenu); + + fToolsMenu = new BMenu(B_TRANSLATE("Tools")); fToolsMenu->AddItem(new BMenuItem("Driver Tests" B_UTF8_ELLIPSIS, new BMessage(MSG_SHOW_DRIVER_TESTS), 'D')); fToolsMenu->AddItem(new BMenuItem("USB Descriptors" B_UTF8_ELLIPSIS, @@ -361,6 +404,13 @@ MainWindow::_BuildMenu() fStreamMenuItem = new BMenuItem("Start MJPEG Stream (Port 8080)", new BMessage(MSG_STREAM_TOGGLE)); fToolsMenu->AddItem(fStreamMenuItem); + fToolsMenu->AddSeparatorItem(); + fToolsMenu->AddItem(new BMenuItem("Show in Deskbar", + new BMessage(MSG_TOGGLE_DESKBAR))); + fToolsMenu->AddItem(new BMenuItem("Pixel Inspector", + new BMessage(MSG_TOGGLE_INSPECTOR), 'I')); + fToolsMenu->AddItem(new BMenuItem("Export Debug State" B_UTF8_ELLIPSIS, + new BMessage(MSG_EXPORT_DEBUG))); fMenuBar->AddItem(fToolsMenu); } @@ -456,7 +506,8 @@ MainWindow::_BuildLayout() .View()); // Left side: video + VU meter (toolbar is inside video box) - BSplitView* leftSplit = new BSplitView(B_VERTICAL); + fLeftSplit = new BSplitView(B_VERTICAL); + BSplitView* leftSplit = fLeftSplit; leftSplit->AddChild(videoBox); leftSplit->AddChild(vuBox); leftSplit->SetItemWeight(0, 0.78f, true); @@ -476,14 +527,16 @@ MainWindow::_BuildLayout() .View()); // Right side: Tab view on top, Syslog on bottom - BSplitView* rightSplit = new BSplitView(B_VERTICAL); + fRightSplit = new BSplitView(B_VERTICAL); + BSplitView* rightSplit = fRightSplit; rightSplit->AddChild(fRightTabView); rightSplit->AddChild(syslogBox); rightSplit->SetItemWeight(0, 0.55f, true); rightSplit->SetItemWeight(1, 0.45f, true); // Main horizontal split - BSplitView* mainSplit = new BSplitView(B_HORIZONTAL); + fMainSplit = new BSplitView(B_HORIZONTAL); + BSplitView* mainSplit = fMainSplit; mainSplit->AddChild(leftSplit); mainSplit->AddChild(rightSplit); mainSplit->SetItemWeight(0, 0.25f, true); @@ -523,10 +576,10 @@ MainWindow::_PopulateWebcamMenu() int32 count = fWebcamRoster->CountDevices(); if (count == 0) { - BMenuItem* noDevice = new BMenuItem("No webcams found", NULL); + BMenuItem* noDevice = new BMenuItem(B_TRANSLATE("No webcams found"), NULL); noDevice->SetEnabled(false); fWebcamMenu->AddItem(noDevice); - fStatusBar->SetText("No webcams detected"); + fStatusBar->SetText(B_TRANSLATE("No webcams detected")); } else { for (int32 i = 0; i < count; i++) { WebcamDevice* device = fWebcamRoster->DeviceAt(i); @@ -537,7 +590,7 @@ MainWindow::_PopulateWebcamMenu() fWebcamMenu->AddItem(item); } } - fStatusBar->SetText("Select a webcam from the menu"); + fStatusBar->SetText(B_TRANSLATE("Select a webcam from the menu")); } } @@ -897,6 +950,7 @@ MainWindow::_StartPreview() strerror(status), status); BAlert* alert = new BAlert("Error", error.String(), "OK"); alert->Go(); + NotificationUtils::Error("Capture Failed", error.String()); return; } @@ -1140,6 +1194,7 @@ MainWindow::_SaveScreenshot(const char* path) BString msg; msg.SetToFormat("Screenshot saved:\n%s", path); fStatusBar->SetText("Screenshot saved"); + NotificationUtils::Info("Screenshot", path); } else { BString error; error.SetToFormat("Failed to save screenshot:\n%s", strerror(status)); @@ -1350,10 +1405,8 @@ MainWindow::MessageReceived(BMessage* message) if (message->FindData("audio_data", B_RAW_TYPE, &data, &dataSize) == B_OK) { static int32 sAudioLogCount = 0; - if (++sAudioLogCount <= 3) { - fprintf(stderr, "Recording: AddAudioBuffer %zd bytes\n", - (size_t)dataSize); - } + if (++sAudioLogCount <= 3) + LOG_DEBUG("Recording: AddAudioBuffer %zd bytes", (size_t)dataSize); fRecorder->AddAudioBuffer(data, (size_t)dataSize); } } @@ -1583,6 +1636,97 @@ MainWindow::MessageReceived(BMessage* message) fStatusBar->SetText("Reference frame cleared"); break; + case MSG_FILTER_TOGGLE: + { + int32 index; + if (message->FindInt32("index", &index) == B_OK) { + VideoFilter* filter = fFilterChain->FilterAt(index); + if (filter != NULL) { + filter->SetEnabled(!filter->IsEnabled()); + BString msg; + msg.SetToFormat("Filter '%s': %s", filter->Name(), + filter->IsEnabled() ? "enabled" : "disabled"); + fStatusBar->SetText(msg.String()); + } + } + break; + } + + case MSG_EXPORT_DEBUG: + _ExportDebugState(); + break; + + case MSG_TOGGLE_INSPECTOR: + { + bool inspect = !fVideoPreview->InspectorMode(); + fVideoPreview->SetInspectorMode(inspect); + BMenuItem* item = fToolsMenu->FindItem(MSG_TOGGLE_INSPECTOR); + if (item != NULL) + item->SetMarked(inspect); + fStatusBar->SetText(inspect + ? "Pixel Inspector: click on preview to inspect" + : "Pixel Inspector disabled"); + break; + } + + case MSG_TOGGLE_SYSLOG: + { + // Toggle syslog panel visibility via collapsing split + bool visible = fRightSplit->IsItemCollapsed(1); + fRightSplit->SetItemCollapsed(1, !visible); + break; + } + + case MSG_TOGGLE_VUBAR: + { + bool visible = fLeftSplit->IsItemCollapsed(1); + fLeftSplit->SetItemCollapsed(1, !visible); + break; + } + + case MSG_RESET_LAYOUT: + fMainSplit->SetItemWeight(0, 0.25f, true); + fMainSplit->SetItemWeight(1, 0.75f, true); + fLeftSplit->SetItemWeight(0, 0.78f, true); + fLeftSplit->SetItemWeight(1, 0.22f, true); + fLeftSplit->SetItemCollapsed(1, false); + fRightSplit->SetItemWeight(0, 0.55f, true); + fRightSplit->SetItemWeight(1, 0.45f, true); + fRightSplit->SetItemCollapsed(1, false); + fStatusBar->SetText("Layout reset to defaults"); + break; + + case MSG_CODEC_MJPEG: + if (fRecorder != NULL) + fRecorder->SetCodec(VIDEO_CODEC_MJPEG); + fStatusBar->SetText("Recording codec: Motion JPEG"); + break; + + case MSG_CODEC_RAW: + if (fRecorder != NULL) + fRecorder->SetCodec(VIDEO_CODEC_RAW); + fStatusBar->SetText("Recording codec: Uncompressed RGB32"); + break; + + case MSG_TOGGLE_DESKBAR: + { + if (DeskbarReplicant::IsInstalledInDeskbar()) { + DeskbarReplicant::RemoveFromDeskbar(); + fStatusBar->SetText("Removed from Deskbar"); + } else { + status_t err = DeskbarReplicant::InstallInDeskbar(); + if (err == B_OK) + fStatusBar->SetText("Installed in Deskbar"); + else + fStatusBar->SetText("Failed to install in Deskbar"); + } + // Update menu checkmark + BMenuItem* item = fToolsMenu->FindItem(MSG_TOGGLE_DESKBAR); + if (item != NULL) + item->SetMarked(DeskbarReplicant::IsInstalledInDeskbar()); + break; + } + case MSG_RECORD_START: _StartRecording(); break; @@ -1753,6 +1897,110 @@ MainWindow::MessageReceived(BMessage* message) break; } + // Scripting (hey BubiCam get/set Property) + case B_GET_PROPERTY: + case B_SET_PROPERTY: + case B_EXECUTE_PROPERTY: + { + BMessage specifier; + int32 index; + int32 what; + const char* property; + if (message->GetCurrentSpecifier(&index, &specifier, &what, + &property) != B_OK) { + BWindow::MessageReceived(message); + break; + } + + BMessage reply(B_REPLY); + + if (strcmp(property, "Status") == 0 && + message->what == B_GET_PROPERTY) { + const char* status = "idle"; + if (fRecorder != NULL && fRecorder->IsRecording()) + status = "recording"; + else if (fIsPreviewActive) + status = "streaming"; + reply.AddString("result", status); + } else if (strcmp(property, "FPS") == 0 && + message->what == B_GET_PROPERTY) { + WebcamDevice* webcam = NULL; + { + BAutolock lock(fWebcamLock); + webcam = fCurrentWebcam; + } + reply.AddFloat("result", + webcam != NULL ? webcam->CurrentFPS() : 0.0f); + } else if (strcmp(property, "Device") == 0 && + message->what == B_GET_PROPERTY) { + WebcamDevice* webcam = NULL; + { + BAutolock lock(fWebcamLock); + webcam = fCurrentWebcam; + } + reply.AddString("result", + webcam != NULL ? webcam->Name() : "none"); + } else if (strcmp(property, "Streaming") == 0) { + if (message->what == B_GET_PROPERTY) { + reply.AddBool("result", fIsPreviewActive); + } else { + bool value; + if (specifier.FindBool("data", &value) == B_OK || + message->FindBool("data", &value) == B_OK) { + if (value && !fIsPreviewActive && fCurrentWebcam != NULL) + _StartPreview(); + else if (!value && fIsPreviewActive) + _StopPreview(); + reply.AddInt32("error", B_OK); + } + } + } else if (strcmp(property, "Recording") == 0) { + if (message->what == B_GET_PROPERTY) { + reply.AddBool("result", + fRecorder != NULL && fRecorder->IsRecording()); + } else { + bool value; + if (specifier.FindBool("data", &value) == B_OK || + message->FindBool("data", &value) == B_OK) { + if (value) + _StartRecording(); + else + _StopRecording(); + reply.AddInt32("error", B_OK); + } + } + } else if (strcmp(property, "Screenshot") == 0 && + message->what == B_EXECUTE_PROPERTY) { + _TakeScreenshot(); + reply.AddInt32("error", B_OK); + } else if (strcmp(property, "FramesCaptured") == 0 && + message->what == B_GET_PROPERTY) { + WebcamDevice* webcam = NULL; + { + BAutolock lock(fWebcamLock); + webcam = fCurrentWebcam; + } + reply.AddInt32("result", + webcam != NULL ? (int32)webcam->FramesCaptured() : 0); + } else if (strcmp(property, "FramesDropped") == 0 && + message->what == B_GET_PROPERTY) { + WebcamDevice* webcam = NULL; + { + BAutolock lock(fWebcamLock); + webcam = fCurrentWebcam; + } + reply.AddInt32("result", + webcam != NULL ? (int32)webcam->FramesDropped() : 0); + } else { + BWindow::MessageReceived(message); + break; + } + + reply.AddInt32("error", B_OK); + message->SendReply(&reply); + break; + } + default: BWindow::MessageReceived(message); break; @@ -1866,6 +2114,9 @@ MainWindow::_HandleFrameReceived(BMessage* message) if (w <= 0 || h <= 0 || w > 4096 || h > 4096) return; + // Apply video filters before display + fFilterChain->ApplyAll(bitmap); + fVideoPreview->SetFrame(bitmap); // Forward frame to fullscreen preview if active @@ -2367,6 +2618,8 @@ MainWindow::_ForceStop() _UpdateToolbarState(); fStatusBar->SetText("Preview force-stopped (driver was frozen)"); + NotificationUtils::Error("Driver Frozen", + "The webcam driver stopped responding. Preview was force-stopped."); fStatusBar->SetHighUIColor(B_PANEL_TEXT_COLOR); } @@ -2477,14 +2730,14 @@ MainWindow::_StartRecording() webcam = fCurrentWebcam; } if (webcam != NULL && webcam->SupportsAudio()) { - fprintf(stderr, "Recording: StartWithAudio %.0f Hz, %d ch, %d bit\n", + LOG_INFO("Recording: StartWithAudio %.0f Hz, %d ch, %d bit", webcam->AudioSampleRate(), (int)webcam->AudioChannels(), (int)webcam->AudioBitsPerSample()); status = fRecorder->StartWithAudio(filePath.Path(), width, height, fps, webcam->AudioSampleRate(), webcam->AudioChannels(), webcam->AudioBitsPerSample()); } else { - fprintf(stderr, "Recording: Start (no audio, supportsAudio=%d, webcam=%p)\n", + LOG_INFO("Recording: Start (no audio, supportsAudio=%d, webcam=%p)", webcam ? webcam->SupportsAudio() : -1, webcam); status = fRecorder->Start(filePath.Path(), width, height, fps); } @@ -2500,13 +2753,8 @@ MainWindow::_StartRecording() // Connect recorder directly to audio consumer for zero-loss recording bool hasAudio = fRecorder->HasAudio(); if (hasAudio && webcam != NULL) { - AudioConsumer* audioConsumer = webcam->GetAudioConsumer(); - fprintf(stderr, "Recording: hasAudio=%d, webcam=%p, audioConsumer=%p\n", - hasAudio, webcam, audioConsumer); - if (audioConsumer != NULL) { - audioConsumer->SetRecorder(fRecorder); - fprintf(stderr, "Recording: SetRecorder(%p) on AudioConsumer\n", fRecorder); - } + webcam->SetAudioSink(fRecorder); + LOG_DEBUG("Recording: routed audio to recorder sink %p", fRecorder); } BString statusMsg; @@ -2531,11 +2779,8 @@ MainWindow::_StopRecording() BAutolock lock(fWebcamLock); webcam = fCurrentWebcam; } - if (webcam != NULL) { - AudioConsumer* audioConsumer = webcam->GetAudioConsumer(); - if (audioConsumer != NULL) - audioConsumer->ClearRecorder(); - } + if (webcam != NULL) + webcam->ClearAudioSink(); uint32 frames = fRecorder->FramesRecorded(); bigtime_t duration = fRecorder->Duration(); @@ -2546,6 +2791,7 @@ MainWindow::_StopRecording() statusMsg.SetToFormat("Recording saved: %u frames, %.1f seconds", (unsigned)frames, duration / 1000000.0); fStatusBar->SetText(statusMsg.String()); + NotificationUtils::Info("Recording Saved", statusMsg.String()); _UpdateToolbarState(); } @@ -2838,6 +3084,100 @@ _ShutdownWatchdogThread(void* data) } +// ============================================================================ +// Scripting support: hey BubiCam get/set Property +// ============================================================================ + +static const char* kScriptingSuite = "suite/x-vnd.BubiCam"; + +static property_info sScriptingProperties[] = { + { + "Status", + { B_GET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Current status (idle, streaming, recording)", + 0, { B_STRING_TYPE } + }, + { + "FPS", + { B_GET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Current frames per second", + 0, { B_FLOAT_TYPE } + }, + { + "Device", + { B_GET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Current webcam device name", + 0, { B_STRING_TYPE } + }, + { + "Streaming", + { B_GET_PROPERTY, B_SET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Start/stop video preview (bool)", + 0, { B_BOOL_TYPE } + }, + { + "Recording", + { B_GET_PROPERTY, B_SET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Start/stop video recording (bool)", + 0, { B_BOOL_TYPE } + }, + { + "Screenshot", + { B_EXECUTE_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Take a screenshot", + 0, {} + }, + { + "FramesCaptured", + { B_GET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Total frames captured", + 0, { B_INT32_TYPE } + }, + { + "FramesDropped", + { B_GET_PROPERTY, 0 }, + { B_DIRECT_SPECIFIER, 0 }, + "Total frames dropped", + 0, { B_INT32_TYPE } + }, + { 0 } +}; + + +status_t +MainWindow::GetSupportedSuites(BMessage* data) +{ + if (data == NULL) + return B_BAD_VALUE; + + data->AddString("suites", kScriptingSuite); + + BPropertyInfo propertyInfo(sScriptingProperties); + data->AddFlat("messages", &propertyInfo); + + return BWindow::GetSupportedSuites(data); +} + + +BHandler* +MainWindow::ResolveSpecifier(BMessage* message, int32 index, + BMessage* specifier, int32 what, const char* property) +{ + BPropertyInfo propertyInfo(sScriptingProperties); + if (propertyInfo.FindMatch(message, index, specifier, what, property) >= 0) + return this; + + return BWindow::ResolveSpecifier(message, index, specifier, what, property); +} + + bool MainWindow::QuitRequested() { @@ -3015,6 +3355,86 @@ MainWindow::_LoadSettings() } +void +MainWindow::_ExportDebugState() +{ + BPath path; + find_directory(B_USER_DIRECTORY, &path); + BString filename("BubiCam_DebugState_"); + filename << ExportUtils::GetTimestamp() << ".txt"; + path.Append(filename.String()); + + BString report; + report << "=== BubiCam Debug State Export ===\n"; + report << "Timestamp: " << ExportUtils::GetTimestamp() << "\n\n"; + + // Application state + report << "--- Application State ---\n"; + report << "Preview active: " << (fIsPreviewActive ? "yes" : "no") << "\n"; + report << "Recording: " << (fRecorder != NULL && fRecorder->IsRecording() ? "yes" : "no") << "\n"; + report << "Driver crashed: " << (fDriverCrashed ? "yes" : "no") << "\n"; + report << "Fullscreen: " << (fIsFullscreen ? "yes" : "no") << "\n\n"; + + // Device state + WebcamDevice* webcam = NULL; + { + BAutolock lock(fWebcamLock); + webcam = fCurrentWebcam; + } + report << "--- Webcam Device ---\n"; + if (webcam != NULL) { + report << "Name: " << webcam->Name() << "\n"; + report << "Driver: " << webcam->DriverName() << "\n"; + report << "VID:PID: " << BString().SetToFormat("0x%04X:0x%04X", + webcam->VendorID(), webcam->ProductID()) << "\n"; + report << "Capturing: " << (webcam->IsCapturing() ? "yes" : "no") << "\n"; + report << "Frames captured: " << webcam->FramesCaptured() << "\n"; + report << "Frames dropped: " << webcam->FramesDropped() << "\n"; + report << "FPS: " << BString().SetToFormat("%.1f", webcam->CurrentFPS()) << "\n"; + VideoFormat fmt = webcam->CurrentFormat(); + report << "Format: " << BString().SetToFormat("%dx%d @ %.1f fps (%s)", + fmt.width, fmt.height, fmt.frameRate, fmt.colorSpace) << "\n"; + report << "Audio: " << (webcam->SupportsAudio() ? "yes" : "no") << "\n"; + if (webcam->HasDriverWarnings()) { + report << "Warnings: " << webcam->GetDriverWarnings() << "\n"; + } + } else { + report << "No device selected\n"; + } + report << "\n"; + + // Server state + report << "--- Server State ---\n"; + if (fMCPServer != NULL) + report << "MCP server: " << (fMCPServer->IsRunning() ? "running" : "stopped") << "\n"; + if (fStreamServer != NULL) + report << "Stream server: " << (fStreamServer->IsRunning() ? "running" : "stopped") << "\n"; + report << "\n"; + + // System info + report << "--- System Info ---\n"; + system_info sysInfo; + if (get_system_info(&sysInfo) == B_OK) { + report << "CPU count: " << sysInfo.cpu_count << "\n"; + report << "Max pages: " << sysInfo.max_pages << "\n"; + report << "Used pages: " << sysInfo.used_pages << "\n"; + } + report << "\n=== End Debug State ===\n"; + + // Write to file + BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE); + if (file.InitCheck() == B_OK) { + file.Write(report.String(), report.Length()); + BString msg; + msg.SetToFormat("Debug state exported to:\n%s", path.Path()); + fStatusBar->SetText("Debug state exported"); + NotificationUtils::Info("Debug Export", path.Path()); + } else { + fStatusBar->SetText("Failed to export debug state"); + } +} + + void MainWindow::_ExportRawFrame() { diff --git a/src/MainWindow.h b/src/MainWindow.h index c1b12e8..7e46dfc 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -21,6 +21,7 @@ #include #include #include +#include #include class LEDView; @@ -36,6 +37,7 @@ class WebcamDevice; class MCPServer; class StreamServer; class VideoRecorder; +class VideoFilterChain; // Message constants enum { @@ -75,6 +77,15 @@ enum { MSG_CAPTURE_REFERENCE = 'cprf', MSG_TOGGLE_COMPARE = 'tgcm', MSG_CLEAR_REFERENCE = 'clrf', + MSG_TOGGLE_DESKBAR = 'tgdb', + MSG_CODEC_MJPEG = 'cdmj', + MSG_CODEC_RAW = 'cdrw', + MSG_TOGGLE_SYSLOG = 'tgsy', + MSG_TOGGLE_VUBAR = 'tgvu', + MSG_RESET_LAYOUT = 'rlyt', + MSG_TOGGLE_INSPECTOR = 'tgin', + MSG_EXPORT_DEBUG = 'exdb', + MSG_FILTER_TOGGLE = 'fltg', MSG_TIMELAPSE_START = 'tlst', MSG_TIMELAPSE_STOP = 'tlsp', MSG_TIMELAPSE_TICK = 'tltk', @@ -101,6 +112,12 @@ class MainWindow : public BWindow { virtual void MessageReceived(BMessage* message); virtual bool QuitRequested(); + // Scripting support (hey BubiCam ...) + virtual status_t GetSupportedSuites(BMessage* data); + virtual BHandler* ResolveSpecifier(BMessage* message, int32 index, + BMessage* specifier, int32 what, + const char* property); + private: void _BuildMenu(); void _BuildLayout(); @@ -142,6 +159,7 @@ class MainWindow : public BWindow { void _ConnectAudioSource(int32 nodeID); void _DisconnectAudio(); + void _ExportDebugState(); void _EnterVideoFullscreen(); void _ExitVideoFullscreen(); void _StartDeviceWatching(); @@ -172,6 +190,11 @@ class MainWindow : public BWindow { BStringView* fStatusBar; BTabView* fRightTabView; + // Split views (for layout persistence) + BSplitView* fMainSplit; + BSplitView* fLeftSplit; + BSplitView* fRightSplit; + // Toolbar BToolBar* fToolbar; @@ -206,6 +229,7 @@ class MainWindow : public BWindow { // Video recording VideoRecorder* fRecorder; + VideoFilterChain* fFilterChain; // Time-lapse BMessageRunner* fTimelapseRunner; diff --git a/src/utils/NotificationUtils.cpp b/src/utils/NotificationUtils.cpp new file mode 100644 index 0000000..4a0baf2 --- /dev/null +++ b/src/utils/NotificationUtils.cpp @@ -0,0 +1,58 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + */ + +#include "NotificationUtils.h" + +#include + + +static const char* kNotificationID = "application/x-vnd.BubiCam"; + + +void +NotificationUtils::Info(const char* title, const char* message) +{ + _Send(B_INFORMATION_NOTIFICATION, title, message); +} + + +void +NotificationUtils::Warning(const char* title, const char* message) +{ + _Send(B_IMPORTANT_NOTIFICATION, title, message); +} + + +void +NotificationUtils::Error(const char* title, const char* message) +{ + _Send(B_ERROR_NOTIFICATION, title, message); +} + + +void +NotificationUtils::Progress(const char* title, const char* message, + float progress) +{ + _Send(B_PROGRESS_NOTIFICATION, title, message, progress); +} + + +void +NotificationUtils::_Send(notification_type type, const char* title, + const char* message, float progress) +{ + BNotification notification(type); + notification.SetGroup("BubiCam"); + notification.SetTitle(title); + notification.SetContent(message); + notification.SetMessageID(kNotificationID); + + if (type == B_PROGRESS_NOTIFICATION && progress >= 0.0f) + notification.SetProgress(progress); + + notification.Send(); +} diff --git a/src/utils/NotificationUtils.h b/src/utils/NotificationUtils.h new file mode 100644 index 0000000..60799b5 --- /dev/null +++ b/src/utils/NotificationUtils.h @@ -0,0 +1,28 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + * + * NotificationUtils - System notification helpers. + */ + +#ifndef NOTIFICATION_UTILS_H +#define NOTIFICATION_UTILS_H + +#include +#include + +class NotificationUtils { +public: + static void Info(const char* title, const char* message); + static void Warning(const char* title, const char* message); + static void Error(const char* title, const char* message); + static void Progress(const char* title, const char* message, + float progress); + +private: + static void _Send(notification_type type, const char* title, + const char* message, float progress = -1.0f); +}; + +#endif // NOTIFICATION_UTILS_H diff --git a/src/utils/VideoFilter.cpp b/src/utils/VideoFilter.cpp new file mode 100644 index 0000000..77909d6 --- /dev/null +++ b/src/utils/VideoFilter.cpp @@ -0,0 +1,156 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + */ + +#include "VideoFilter.h" + +#include +#include + + +// ============================================================================ +// Grayscale +// ============================================================================ + +void +GrayscaleFilter::Apply(BBitmap* bitmap) +{ + if (bitmap == NULL || !fEnabled) + return; + + uint8* bits = (uint8*)bitmap->Bits(); + int32 length = bitmap->BitsLength(); + + for (int32 i = 0; i < length; i += 4) { + uint8 gray = (uint8)((bits[i + 2] * 77 + bits[i + 1] * 150 + bits[i] * 29) >> 8); + bits[i] = gray; + bits[i + 1] = gray; + bits[i + 2] = gray; + // alpha unchanged + } +} + + +// ============================================================================ +// Invert +// ============================================================================ + +void +InvertFilter::Apply(BBitmap* bitmap) +{ + if (bitmap == NULL || !fEnabled) + return; + + uint8* bits = (uint8*)bitmap->Bits(); + int32 length = bitmap->BitsLength(); + + for (int32 i = 0; i < length; i += 4) { + bits[i] = 255 - bits[i]; + bits[i + 1] = 255 - bits[i + 1]; + bits[i + 2] = 255 - bits[i + 2]; + } +} + + +// ============================================================================ +// Mirror +// ============================================================================ + +void +MirrorFilter::Apply(BBitmap* bitmap) +{ + if (bitmap == NULL || !fEnabled) + return; + + int32 width = (int32)(bitmap->Bounds().Width() + 1); + int32 height = (int32)(bitmap->Bounds().Height() + 1); + int32 bpr = bitmap->BytesPerRow(); + uint8* bits = (uint8*)bitmap->Bits(); + + for (int32 y = 0; y < height; y++) { + uint32* row = (uint32*)(bits + y * bpr); + for (int32 x = 0; x < width / 2; x++) { + uint32 tmp = row[x]; + row[x] = row[width - 1 - x]; + row[width - 1 - x] = tmp; + } + } +} + + +// ============================================================================ +// Sepia +// ============================================================================ + +void +SepiaFilter::Apply(BBitmap* bitmap) +{ + if (bitmap == NULL || !fEnabled) + return; + + uint8* bits = (uint8*)bitmap->Bits(); + int32 length = bitmap->BitsLength(); + + for (int32 i = 0; i < length; i += 4) { + uint8 b = bits[i], g = bits[i + 1], r = bits[i + 2]; + int32 gray = (r * 77 + g * 150 + b * 29) >> 8; + + int32 sr = gray + 40; + int32 sg = gray + 20; + int32 sb = gray - 10; + + bits[i] = (uint8)(sb < 0 ? 0 : (sb > 255 ? 255 : sb)); + bits[i + 1] = (uint8)(sg > 255 ? 255 : sg); + bits[i + 2] = (uint8)(sr > 255 ? 255 : sr); + } +} + + +// ============================================================================ +// Filter Chain +// ============================================================================ + +VideoFilterChain::VideoFilterChain() + : + fFilters(10) +{ +} + + +VideoFilterChain::~VideoFilterChain() +{ +} + + +void +VideoFilterChain::AddFilter(VideoFilter* filter) +{ + fFilters.AddItem(filter); +} + + +VideoFilter* +VideoFilterChain::FilterAt(int32 index) const +{ + return fFilters.ItemAt(index); +} + + +int32 +VideoFilterChain::CountFilters() const +{ + return fFilters.CountItems(); +} + + +void +VideoFilterChain::ApplyAll(BBitmap* bitmap) +{ + for (int32 i = 0; i < fFilters.CountItems(); i++) { + VideoFilter* filter = fFilters.ItemAt(i); + if (filter != NULL && filter->IsEnabled()) + filter->Apply(bitmap); + } +} diff --git a/src/utils/VideoFilter.h b/src/utils/VideoFilter.h new file mode 100644 index 0000000..5c7e5d9 --- /dev/null +++ b/src/utils/VideoFilter.h @@ -0,0 +1,79 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + * + * VideoFilter - Abstract interface for video frame filters. + * + * Filters process B_RGB32 bitmaps in-place. They are applied in the + * video preview pipeline after format conversion and before display. + * + * Built-in filters: grayscale, invert, mirror, sepia. + * The interface is designed so external add-ons can provide additional + * filters in the future. + */ + +#ifndef VIDEO_FILTER_H +#define VIDEO_FILTER_H + +#include +#include +#include + +class VideoFilter { +public: + virtual ~VideoFilter() {} + + virtual const char* Name() const = 0; + virtual void Apply(BBitmap* bitmap) = 0; + virtual bool IsEnabled() const { return fEnabled; } + virtual void SetEnabled(bool enabled) { fEnabled = enabled; } + +protected: + bool fEnabled = false; +}; + + +// Built-in filters + +class GrayscaleFilter : public VideoFilter { +public: + const char* Name() const { return "Grayscale"; } + void Apply(BBitmap* bitmap); +}; + +class InvertFilter : public VideoFilter { +public: + const char* Name() const { return "Invert Colors"; } + void Apply(BBitmap* bitmap); +}; + +class MirrorFilter : public VideoFilter { +public: + const char* Name() const { return "Mirror Horizontal"; } + void Apply(BBitmap* bitmap); +}; + +class SepiaFilter : public VideoFilter { +public: + const char* Name() const { return "Sepia Tone"; } + void Apply(BBitmap* bitmap); +}; + + +// Filter chain manager +class VideoFilterChain { +public: + VideoFilterChain(); + ~VideoFilterChain(); + + void AddFilter(VideoFilter* filter); + VideoFilter* FilterAt(int32 index) const; + int32 CountFilters() const; + void ApplyAll(BBitmap* bitmap); + +private: + BObjectList fFilters; +}; + +#endif // VIDEO_FILTER_H diff --git a/src/utils/VideoRecorder.cpp b/src/utils/VideoRecorder.cpp index 7735e43..f827898 100644 --- a/src/utils/VideoRecorder.cpp +++ b/src/utils/VideoRecorder.cpp @@ -36,6 +36,7 @@ VideoRecorder::VideoRecorder() fHeight(0), fFPS(30.0f), fJPEGQuality(85), + fCodec(VIDEO_CODEC_MJPEG), fFrameCount(0), fStartTime(0), fHasAudio(false), @@ -44,6 +45,8 @@ VideoRecorder::VideoRecorder() fBitsPerSample(0), fAudioChunkCount(0), fTotalAudioBytes(0), + fAudioScratch(NULL), + fAudioScratchSamples(0), fMoviListStart(0), fMoviDataStart(0), fVideoIndex(20), @@ -56,6 +59,8 @@ VideoRecorder::~VideoRecorder() { if (fRecording) Stop(); + + delete[] fAudioScratch; } @@ -168,26 +173,34 @@ VideoRecorder::AddFrame(BBitmap* bitmap) if (!fRecording || bitmap == NULL) return B_NOT_ALLOWED; - // Compress bitmap to JPEG + const uint8* frameData = NULL; + uint32 frameSize = 0; uint8* jpegData = NULL; - unsigned long jpegSize = 0; - - status_t status = _CompressFrameToJPEG(bitmap, &jpegData, &jpegSize); - if (status != B_OK || jpegData == NULL) - return status; - // Write AVI chunk: '00dc' + size + data (+ padding) - off_t chunkStart = 0; - chunkStart = fFile.Position(); + if (fCodec == VIDEO_CODEC_RAW) { + // Raw uncompressed RGB32 - write bitmap bits directly + frameData = (const uint8*)bitmap->Bits(); + frameSize = (uint32)bitmap->BitsLength(); + } else { + // MJPEG - compress bitmap to JPEG + unsigned long jpegSize = 0; + status_t status = _CompressFrameToJPEG(bitmap, &jpegData, &jpegSize); + if (status != B_OK || jpegData == NULL) + return status; + frameData = jpegData; + frameSize = (uint32)jpegSize; + } + // Write AVI chunk: '00dc' (compressed) or '00db' (raw) + size + data + off_t chunkStart = fFile.Position(); off_t frameOffset = chunkStart - fMoviDataStart; - _WriteFourCC("00dc"); - _WriteUInt32((uint32)jpegSize); - fFile.Write(jpegData, jpegSize); + _WriteFourCC(fCodec == VIDEO_CODEC_RAW ? "00db" : "00dc"); + _WriteUInt32(frameSize); + fFile.Write(frameData, frameSize); // Pad to 2-byte boundary - if (jpegSize & 1) { + if (frameSize & 1) { uint8 pad = 0; fFile.Write(&pad, 1); } @@ -195,7 +208,7 @@ VideoRecorder::AddFrame(BBitmap* bitmap) // Record index entry AVIIndexEntry* entry = new AVIIndexEntry(); entry->offset = frameOffset; - entry->size = (uint32)jpegSize; + entry->size = frameSize; fVideoIndex.AddItem(entry); fFrameCount++; @@ -239,6 +252,39 @@ VideoRecorder::AddAudioBuffer(const void* data, size_t size) } +void +VideoRecorder::WriteAudio(const void* data, size_t size, + const media_raw_audio_format& format) +{ + if (data == NULL || size == 0) + return; + + if (format.format == media_raw_audio_format::B_AUDIO_FLOAT) { + // Convert 32-bit float to 16-bit PCM for AVI compatibility, reusing a + // scratch buffer to avoid allocating on the real-time audio thread. + const float* floatData = static_cast(data); + size_t sampleCount = size / sizeof(float); + + if (sampleCount > fAudioScratchSamples) { + delete[] fAudioScratch; + fAudioScratch = new int16[sampleCount]; + fAudioScratchSamples = sampleCount; + } + + for (size_t i = 0; i < sampleCount; i++) { + float sample = floatData[i]; + if (sample > 1.0f) sample = 1.0f; + if (sample < -1.0f) sample = -1.0f; + fAudioScratch[i] = (int16)(sample * 32767.0f); + } + + AddAudioBuffer(fAudioScratch, sampleCount * sizeof(int16)); + } else { + AddAudioBuffer(data, size); + } +} + + status_t VideoRecorder::Stop() { @@ -334,7 +380,7 @@ VideoRecorder::_WriteAVIHeaders() _WriteFourCC("strh"); _WriteUInt32(56); _WriteFourCC("vids"); // fccType - _WriteFourCC("MJPG"); // fccHandler + _WriteFourCC(fCodec == VIDEO_CODEC_RAW ? "\0\0\0\0" : "MJPG"); // fccHandler _WriteUInt32(0); // dwFlags _WriteUInt16(0); // wPriority _WriteUInt16(0); // wLanguage @@ -358,9 +404,14 @@ VideoRecorder::_WriteAVIHeaders() _WriteUInt32(fWidth); // biWidth _WriteUInt32(fHeight); // biHeight _WriteUInt16(1); // biPlanes - _WriteUInt16(24); // biBitCount - _WriteFourCC("MJPG"); // biCompression - _WriteUInt32(fWidth * fHeight * 3); // biSizeImage + _WriteUInt16(fCodec == VIDEO_CODEC_RAW ? 32 : 24); // biBitCount + if (fCodec == VIDEO_CODEC_RAW) { + _WriteUInt32(0); // biCompression (BI_RGB = 0) + _WriteUInt32(fWidth * fHeight * 4); // biSizeImage + } else { + _WriteFourCC("MJPG"); // biCompression + _WriteUInt32(fWidth * fHeight * 3); // biSizeImage + } _WriteUInt32(0); // biXPelsPerMeter _WriteUInt32(0); // biYPelsPerMeter _WriteUInt32(0); // biClrUsed diff --git a/src/utils/VideoRecorder.h b/src/utils/VideoRecorder.h index e7fb628..35c9978 100644 --- a/src/utils/VideoRecorder.h +++ b/src/utils/VideoRecorder.h @@ -18,6 +18,15 @@ #include #include +#include "AudioSink.h" + + +// Video codec selection for recording +enum video_codec_t { + VIDEO_CODEC_MJPEG = 0, // Motion JPEG (default, good compression) + VIDEO_CODEC_RAW = 1 // Uncompressed RGB32 (lossless, large files) +}; + struct AVIIndexEntry { off_t offset; @@ -25,10 +34,13 @@ struct AVIIndexEntry { }; -class VideoRecorder { +class VideoRecorder : public AudioSink { public: VideoRecorder(); - ~VideoRecorder(); + virtual ~VideoRecorder(); + + void SetCodec(video_codec_t codec) { fCodec = codec; } + video_codec_t Codec() const { return fCodec; } status_t Start(const char* path, int32 width, int32 height, float fps = 30.0f, int jpegQuality = 85); @@ -40,6 +52,11 @@ class VideoRecorder { status_t AddAudioBuffer(const void* data, size_t size); status_t Stop(); + // AudioSink: accepts raw PCM from the capture library and converts it + // (float -> int16 when needed) before storing it in the AVI stream. + virtual void WriteAudio(const void* data, size_t size, + const media_raw_audio_format& format); + bool IsRecording() const { return fRecording; } bool HasAudio() const { return fHasAudio; } uint32 FramesRecorded() const { return fFrameCount; } @@ -64,6 +81,7 @@ class VideoRecorder { int32 fHeight; float fFPS; int fJPEGQuality; + video_codec_t fCodec; uint32 fFrameCount; bigtime_t fStartTime; @@ -75,6 +93,11 @@ class VideoRecorder { uint32 fAudioChunkCount; uint32 fTotalAudioBytes; + // Reusable float->int16 conversion scratch. Avoids a heap allocation on + // every audio buffer: WriteAudio() runs on the real-time audio thread. + int16* fAudioScratch; + size_t fAudioScratchSamples; + // AVI structure tracking off_t fMoviListStart; off_t fMoviDataStart; diff --git a/src/views/DeskbarReplicant.cpp b/src/views/DeskbarReplicant.cpp new file mode 100644 index 0000000..36275da --- /dev/null +++ b/src/views/DeskbarReplicant.cpp @@ -0,0 +1,338 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + */ + +#include "DeskbarReplicant.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static const char* kReplicantName = "BubiCam"; +static const char* kAppSignature = "application/x-vnd.BubiCam"; + +// Deskbar tray icon size +static const float kIconSize = 16.0f; + + +DeskbarReplicant::DeskbarReplicant(BRect frame, const char* name) + : + BView(frame, name, B_FOLLOW_ALL, B_WILL_DRAW), + fStatus(WEBCAM_STATUS_IDLE), + fFPS(0.0f), + fDeviceName(""), + fPulseRunner(NULL), + fBlinkOn(true) +{ +} + + +DeskbarReplicant::DeskbarReplicant(BMessage* archive) + : + BView(archive), + fStatus(WEBCAM_STATUS_IDLE), + fFPS(0.0f), + fDeviceName(""), + fPulseRunner(NULL), + fBlinkOn(true) +{ + int32 status; + if (archive->FindInt32("webcam_status", &status) == B_OK) + fStatus = (webcam_status_t)status; + archive->FindFloat("webcam_fps", &fFPS); + const char* name; + if (archive->FindString("webcam_device", &name) == B_OK) + fDeviceName = name; +} + + +DeskbarReplicant::~DeskbarReplicant() +{ + delete fPulseRunner; +} + + +BArchivable* +DeskbarReplicant::Instantiate(BMessage* archive) +{ + if (!validate_instantiation(archive, "DeskbarReplicant")) + return NULL; + return new DeskbarReplicant(archive); +} + + +status_t +DeskbarReplicant::Archive(BMessage* data, bool deep) const +{ + status_t status = BView::Archive(data, deep); + if (status != B_OK) + return status; + + data->AddString("class", "DeskbarReplicant"); + data->AddString("add_on", kAppSignature); + data->AddInt32("webcam_status", (int32)fStatus); + data->AddFloat("webcam_fps", fFPS); + data->AddString("webcam_device", fDeviceName.String()); + + return B_OK; +} + + +void +DeskbarReplicant::AttachedToWindow() +{ + BView::AttachedToWindow(); + + AdoptParentColors(); + + // Pulse timer for blinking during recording + BMessage pulseMsg(MSG_REPLICANT_PULSE); + fPulseRunner = new BMessageRunner(BMessenger(this), &pulseMsg, + 500000); // 500ms +} + + +void +DeskbarReplicant::DetachedFromWindow() +{ + delete fPulseRunner; + fPulseRunner = NULL; + BView::DetachedFromWindow(); +} + + +void +DeskbarReplicant::Draw(BRect updateRect) +{ + _DrawIcon(); +} + + +void +DeskbarReplicant::_DrawIcon() +{ + BRect bounds = Bounds(); + rgb_color bgColor; + if (Parent() != NULL) + bgColor = Parent()->ViewColor(); + else + bgColor = ui_color(B_PANEL_BACKGROUND_COLOR); + + // Clear background + SetHighColor(bgColor); + FillRect(bounds); + + // Draw camera body (rounded rect) + float cx = bounds.Width() / 2; + float cy = bounds.Height() / 2; + + BRect body(cx - 6, cy - 4, cx + 6, cy + 4); + SetHighColor(_StatusColor()); + FillRoundRect(body, 2, 2); + + // Draw lens circle + SetHighColor(bgColor); + FillEllipse(BPoint(cx, cy), 2.5f, 2.5f); + + // Draw lens inner + SetHighColor(_StatusColor()); + FillEllipse(BPoint(cx, cy), 1.5f, 1.5f); + + // Draw flash/viewfinder bump + BRect bump(cx - 2, cy - 6, cx + 2, cy - 4); + SetHighColor(_StatusColor()); + FillRect(bump); + + // Recording indicator (red dot, blinking) + if (fStatus == WEBCAM_STATUS_RECORDING && fBlinkOn) { + SetHighColor(make_color(255, 0, 0)); + FillEllipse(BPoint(bounds.right - 3, bounds.top + 3), 2, 2); + } + + // Streaming indicator (green dot) + if (fStatus == WEBCAM_STATUS_STREAMING) { + SetHighColor(make_color(0, 200, 0)); + FillEllipse(BPoint(bounds.right - 3, bounds.top + 3), 2, 2); + } +} + + +rgb_color +DeskbarReplicant::_StatusColor() const +{ + switch (fStatus) { + case WEBCAM_STATUS_STREAMING: + return make_color(60, 140, 60); // Green + case WEBCAM_STATUS_RECORDING: + return make_color(180, 40, 40); // Red + case WEBCAM_STATUS_ERROR: + return make_color(180, 120, 0); // Orange + case WEBCAM_STATUS_IDLE: + default: + return make_color(120, 120, 120); // Gray + } +} + + +void +DeskbarReplicant::MessageReceived(BMessage* message) +{ + switch (message->what) { + case MSG_REPLICANT_UPDATE: + { + int32 status; + if (message->FindInt32("status", &status) == B_OK) + fStatus = (webcam_status_t)status; + float fps; + if (message->FindFloat("fps", &fps) == B_OK) + fFPS = fps; + const char* name; + if (message->FindString("device", &name) == B_OK) + fDeviceName = name; + Invalidate(); + break; + } + + case MSG_REPLICANT_PULSE: + if (fStatus == WEBCAM_STATUS_RECORDING) { + fBlinkOn = !fBlinkOn; + Invalidate(); + } + break; + + default: + BView::MessageReceived(message); + break; + } +} + + +void +DeskbarReplicant::MouseDown(BPoint where) +{ + BPopUpMenu* menu = new BPopUpMenu("BubiCam", false, false); + + // Status info + BString statusStr; + switch (fStatus) { + case WEBCAM_STATUS_IDLE: + statusStr = "Idle"; + break; + case WEBCAM_STATUS_STREAMING: + statusStr.SetToFormat("Streaming (%.1f fps)", fFPS); + break; + case WEBCAM_STATUS_RECORDING: + statusStr.SetToFormat("Recording (%.1f fps)", fFPS); + break; + case WEBCAM_STATUS_ERROR: + statusStr = "Error"; + break; + } + + BMenuItem* statusItem = new BMenuItem(statusStr.String(), NULL); + statusItem->SetEnabled(false); + menu->AddItem(statusItem); + + if (fDeviceName.Length() > 0) { + BMenuItem* devItem = new BMenuItem(fDeviceName.String(), NULL); + devItem->SetEnabled(false); + menu->AddItem(devItem); + } + + menu->AddSeparatorItem(); + + // Launch/activate BubiCam + BMenuItem* openItem = new BMenuItem("Open BubiCam", + new BMessage('open')); + openItem->SetTarget(this); + menu->AddItem(openItem); + + // Remove from Deskbar + BMenuItem* removeItem = new BMenuItem("Remove from Deskbar", + new BMessage('rmdb')); + removeItem->SetTarget(this); + menu->AddItem(removeItem); + + ConvertToScreen(&where); + BMenuItem* selected = menu->Go(where, false, true); + + if (selected != NULL) { + if (selected->Message()->what == 'open') { + // Launch or activate BubiCam + be_roster->Launch(kAppSignature); + } else if (selected->Message()->what == 'rmdb') { + RemoveFromDeskbar(); + } + } + + delete menu; +} + + +void +DeskbarReplicant::SetStatus(webcam_status_t status) +{ + fStatus = status; + Invalidate(); +} + + +void +DeskbarReplicant::SetFPS(float fps) +{ + fFPS = fps; +} + + +void +DeskbarReplicant::SetDeviceName(const char* name) +{ + fDeviceName = name; +} + + +// Static methods for Deskbar integration + +status_t +DeskbarReplicant::InstallInDeskbar() +{ + // Remove existing first + RemoveFromDeskbar(); + + BDeskbar deskbar; + + // BDeskbar::AddItem(BView*, int32*) installs a replicant from a view + BRect frame(0, 0, kIconSize - 1, kIconSize - 1); + DeskbarReplicant* view = new DeskbarReplicant(frame, kReplicantName); + + int32 id = -1; + status_t status = deskbar.AddItem(view, &id); + delete view; + + return status; +} + + +status_t +DeskbarReplicant::RemoveFromDeskbar() +{ + BDeskbar deskbar; + return deskbar.RemoveItem(kReplicantName); +} + + +bool +DeskbarReplicant::IsInstalledInDeskbar() +{ + BDeskbar deskbar; + return deskbar.HasItem(kReplicantName); +} diff --git a/src/views/DeskbarReplicant.h b/src/views/DeskbarReplicant.h new file mode 100644 index 0000000..ff93be8 --- /dev/null +++ b/src/views/DeskbarReplicant.h @@ -0,0 +1,70 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + * + * DeskbarReplicant - Deskbar tray icon showing webcam status. + */ + +#ifndef DESKBAR_REPLICANT_H +#define DESKBAR_REPLICANT_H + +#include +#include +#include +#include + +// Status the replicant can display +enum webcam_status_t { + WEBCAM_STATUS_IDLE = 0, + WEBCAM_STATUS_STREAMING = 1, + WEBCAM_STATUS_RECORDING = 2, + WEBCAM_STATUS_ERROR = 3 +}; + +// Messages between app and replicant +enum { + MSG_REPLICANT_UPDATE = 'rpup', + MSG_REPLICANT_PULSE = 'rpls' +}; + + +class DeskbarReplicant : public BView { +public: + DeskbarReplicant(BRect frame, const char* name); + DeskbarReplicant(BMessage* archive); + virtual ~DeskbarReplicant(); + + // BArchivable + static BArchivable* Instantiate(BMessage* archive); + virtual status_t Archive(BMessage* data, bool deep = true) const; + + // BView + virtual void AttachedToWindow(); + virtual void DetachedFromWindow(); + virtual void Draw(BRect updateRect); + virtual void MessageReceived(BMessage* message); + virtual void MouseDown(BPoint where); + + // Status control (called from app via message) + void SetStatus(webcam_status_t status); + void SetFPS(float fps); + void SetDeviceName(const char* name); + + // Install/remove from Deskbar + static status_t InstallInDeskbar(); + static status_t RemoveFromDeskbar(); + static bool IsInstalledInDeskbar(); + +private: + void _DrawIcon(); + rgb_color _StatusColor() const; + + webcam_status_t fStatus; + float fFPS; + BString fDeviceName; + BMessageRunner* fPulseRunner; + bool fBlinkOn; +}; + +#endif // DESKBAR_REPLICANT_H diff --git a/src/views/DriverInfoView.cpp b/src/views/DriverInfoView.cpp index 7c6423e..0741436 100644 --- a/src/views/DriverInfoView.cpp +++ b/src/views/DriverInfoView.cpp @@ -8,6 +8,7 @@ #include "WebcamDevice.h" #include "USBVideoParser.h" +#include #include @@ -182,6 +183,52 @@ DriverInfoView::SetDevice(WebcamDevice* device, bool isCapturing) strcmp(currentFormat.colorSpace, "NV21") == 0) bpp = 12; + // Determine planes and chroma subsampling + int planes = 1; + const char* chromaSub = "4:4:4"; + const char* planeLayout = "Packed"; + bool isCompressed = false; + + if (strcmp(currentFormat.colorSpace, "YUY2") == 0 || + strcmp(currentFormat.colorSpace, "UYVY") == 0) { + chromaSub = "4:2:2"; + planeLayout = "Packed (interleaved Y/U/V)"; + } else if (strcmp(currentFormat.colorSpace, "I420") == 0) { + planes = 3; + chromaSub = "4:2:0"; + planeLayout = "Planar (Y + U + V)"; + } else if (strcmp(currentFormat.colorSpace, "NV12") == 0) { + planes = 2; + chromaSub = "4:2:0"; + planeLayout = "Semi-planar (Y + interleaved UV)"; + } else if (strcmp(currentFormat.colorSpace, "NV21") == 0) { + planes = 2; + chromaSub = "4:2:0"; + planeLayout = "Semi-planar (Y + interleaved VU)"; + } else if (strcmp(currentFormat.colorSpace, "MJPEG") == 0) { + isCompressed = true; + chromaSub = "4:2:0 (typical)"; + planeLayout = "Compressed (JPEG stream)"; + } else if (strcmp(currentFormat.colorSpace, "RGB32") == 0 || + strcmp(currentFormat.colorSpace, "BGRA") == 0) { + planeLayout = "Packed (B/G/R/A)"; + } else if (strcmp(currentFormat.colorSpace, "RGB24") == 0) { + planeLayout = "Packed (R/G/B)"; + } + + // Stride (source bytes per row) + int32 srcStride = 0; + if (!isCompressed) { + switch (bpp) { + case 32: srcStride = currentFormat.width * 4; break; + case 24: srcStride = currentFormat.width * 3; break; + case 16: srcStride = currentFormat.width * 2; break; + case 12: srcStride = currentFormat.width; break; // Y plane stride + default: srcStride = currentFormat.width * bpp / 8; break; + } + } + + // Display format details if (bpp > 0) { float rawBandwidth = currentFormat.width * currentFormat.height * bpp * currentFormat.frameRate / 8.0f / 1024.0f / 1024.0f; @@ -191,10 +238,39 @@ DriverInfoView::SetDevice(WebcamDevice* device, bool isCapturing) _AppendField("Format Details", detailStr.String()); } + _AppendField("Chroma Subsampling", chromaSub); + + BString planeStr; + planeStr.SetToFormat("%d - %s", planes, planeLayout); + _AppendField("Planes", planeStr.String()); + + if (srcStride > 0) { + BString strideStr; + strideStr.SetToFormat("%d bytes/row (source), %d bytes/row (display B_RGB32)", + (int)srcStride, (int)(currentFormat.width * 4)); + _AppendField("Stride", strideStr.String()); + } + + // Destination buffer info + BString bufStr; + int32 destFrameSize = currentFormat.width * currentFormat.height * 4; + bufStr.SetToFormat("%d bytes (%.1f KB) per frame in B_RGB32", + (int)destFrameSize, destFrameSize / 1024.0f); + _AppendField("Display Buffer", bufStr.String()); + + // Resolution details BString pixelStr; - pixelStr.SetToFormat("%d total pixels, %.2f megapixels", + float aspect = (float)currentFormat.width / currentFormat.height; + const char* aspectName = "custom"; + if (fabs(aspect - 4.0f/3.0f) < 0.02f) aspectName = "4:3"; + else if (fabs(aspect - 16.0f/9.0f) < 0.02f) aspectName = "16:9"; + else if (fabs(aspect - 16.0f/10.0f) < 0.02f) aspectName = "16:10"; + else if (fabs(aspect - 5.0f/4.0f) < 0.02f) aspectName = "5:4"; + + pixelStr.SetToFormat("%.2f MP (%d pixels), aspect %s (%.3f:1)", + currentFormat.width * currentFormat.height / 1000000.0f, (int)(currentFormat.width * currentFormat.height), - currentFormat.width * currentFormat.height / 1000000.0f); + aspectName, aspect); _AppendField("Resolution Details", pixelStr.String()); } else { _AppendField("Current Format", "Not available (0x0)"); diff --git a/src/views/SyslogView.cpp b/src/views/SyslogView.cpp index 58f0ca9..46a0d99 100644 --- a/src/views/SyslogView.cpp +++ b/src/views/SyslogView.cpp @@ -7,6 +7,7 @@ #include "SyslogView.h" #include +#include #include #include #include @@ -202,10 +203,27 @@ SyslogView::_MatchesFilter(const char* line) if (fFilter.Length() == 0) return true; + // If filter starts with '/' treat it as a POSIX regex + if (fFilter[0] == '/') { + BString pattern(fFilter.String() + 1); + // Remove trailing '/' if present + if (pattern.Length() > 0 && pattern[pattern.Length() - 1] == '/') + pattern.Truncate(pattern.Length() - 1); + + regex_t regex; + if (regcomp(®ex, pattern.String(), + REG_EXTENDED | REG_ICASE | REG_NOSUB) == 0) { + bool match = (regexec(®ex, line, 0, NULL, 0) == 0); + regfree(®ex); + return match; + } + // Invalid regex, fall through to literal matching + } + BString lineLower(line); lineLower.ToLower(); - // Simple OR filter matching + // Simple OR filter matching (pipe-separated keywords) BString filter(fFilter); filter.ToLower(); diff --git a/src/views/VideoPreviewView.cpp b/src/views/VideoPreviewView.cpp index bcdc0a5..b68f9c4 100644 --- a/src/views/VideoPreviewView.cpp +++ b/src/views/VideoPreviewView.cpp @@ -26,6 +26,8 @@ VideoPreviewView::VideoPreviewView(const char* name) fCurrentFrame(NULL), fReferenceFrame(NULL), fCompareMode(false), + fInspectorMode(false), + fInspectorPoint(-1, -1), fFrameLock("frame lock"), fCurrentFPS(0.0f), fFramesReceived(0), @@ -143,6 +145,60 @@ VideoPreviewView::Draw(BRect updateRect) // Draw grid overlay if (fShowGrid) _DrawGrid(); + + // Draw inspector overlay + if (fInspectorMode && fInspectorPoint.x >= 0 && fCurrentFrame != NULL) { + // Map screen point to bitmap coordinates + BRect vr = fVideoRect; + float bmpX = (fInspectorPoint.x - vr.left) / vr.Width() + * fCurrentFrame->Bounds().Width(); + float bmpY = (fInspectorPoint.y - vr.top) / vr.Height() + * fCurrentFrame->Bounds().Height(); + + int32 px = (int32)bmpX; + int32 py = (int32)bmpY; + int32 bw = (int32)(fCurrentFrame->Bounds().Width() + 1); + int32 bh = (int32)(fCurrentFrame->Bounds().Height() + 1); + + if (px >= 0 && px < bw && py >= 0 && py < bh) { + int32 bpr = fCurrentFrame->BytesPerRow(); + const uint8* bits = (const uint8*)fCurrentFrame->Bits(); + const uint8* pixel = bits + py * bpr + px * 4; + uint8 b = pixel[0], g = pixel[1], r = pixel[2], a = pixel[3]; + + fInspectorInfo.SetToFormat("(%d, %d) R:%d G:%d B:%d A:%d #%02X%02X%02X", + px, py, r, g, b, a, r, g, b); + + // Draw crosshair + SetHighColor(255, 255, 0); + StrokeLine(BPoint(fInspectorPoint.x - 8, fInspectorPoint.y), + BPoint(fInspectorPoint.x + 8, fInspectorPoint.y)); + StrokeLine(BPoint(fInspectorPoint.x, fInspectorPoint.y - 8), + BPoint(fInspectorPoint.x, fInspectorPoint.y + 8)); + + // Draw info box + SetDrawingMode(B_OP_ALPHA); + BRect infoRect(5, bounds.bottom - 25, 320, bounds.bottom - 5); + SetHighColor(0, 0, 0, 180); + FillRoundRect(infoRect, 3, 3); + SetHighColor(255, 255, 255, 230); + BFont font(be_fixed_font); + font.SetSize(10); + SetFont(&font); + DrawString(fInspectorInfo.String(), + BPoint(infoRect.left + 5, infoRect.bottom - 7)); + + // Draw color swatch + BRect swatch(infoRect.right - 18, infoRect.top + 3, + infoRect.right - 3, infoRect.bottom - 3); + SetHighColor(r, g, b); + FillRect(swatch); + SetHighColor(255, 255, 255); + StrokeRect(swatch); + + SetDrawingMode(B_OP_COPY); + } + } } else { // No frame - draw placeholder SetHighColor(fBackgroundColor); @@ -220,6 +276,11 @@ VideoPreviewView::MouseDown(BPoint where) GetMouse(&where, &buttons); if (buttons & B_PRIMARY_MOUSE_BUTTON) { + if (fInspectorMode) { + fInspectorPoint = where; + Invalidate(); + return; + } if (fZoomLevel > 1.0f) { // Pan mode when zoomed in fIsPanning = true; @@ -334,6 +395,19 @@ VideoPreviewView::SetBackgroundColor(rgb_color color) } +void +VideoPreviewView::SetInspectorMode(bool enabled) +{ + fInspectorMode = enabled; + if (!enabled) + fInspectorPoint.Set(-1, -1); + if (LockLooper()) { + Invalidate(); + UnlockLooper(); + } +} + + void VideoPreviewView::CaptureReference() { diff --git a/src/views/VideoPreviewView.h b/src/views/VideoPreviewView.h index 61429de..5f0a365 100644 --- a/src/views/VideoPreviewView.h +++ b/src/views/VideoPreviewView.h @@ -40,6 +40,9 @@ class VideoPreviewView : public BView { void ResetZoom(); bool IsFrozen() const { return fFrozen; } void SetBackgroundColor(rgb_color color); + void SetInspectorMode(bool enabled); + bool InspectorMode() const { return fInspectorMode; } + void CaptureReference(); void ClearReference(); void SetCompareMode(bool enabled); @@ -67,6 +70,9 @@ class VideoPreviewView : public BView { BBitmap* fCurrentFrame; BBitmap* fReferenceFrame; bool fCompareMode; + bool fInspectorMode; + BPoint fInspectorPoint; + BString fInspectorInfo; BLocker fFrameLock; BRect fVideoRect; rgb_color fBackgroundColor; diff --git a/src/webcam/AudioConsumer.cpp b/src/webcam/AudioConsumer.cpp index f41ceaa..8af61bf 100644 --- a/src/webcam/AudioConsumer.cpp +++ b/src/webcam/AudioConsumer.cpp @@ -5,8 +5,6 @@ */ #include "AudioConsumer.h" -#include "MainWindow.h" -#include "VideoRecorder.h" #include #include @@ -40,7 +38,9 @@ AudioConsumer::AudioConsumer(const char* name, BLooper* target, fSmoothedRight(0.0f), fBufferCount(0), fLevelLogCount(0), - fRecorder(NULL) + fSink(NULL), + fSwapScratch(NULL), + fSwapScratchSize(0) { fInput = media_input(); fFormat = media_format(); @@ -72,6 +72,8 @@ AudioConsumer::~AudioConsumer() fprintf(stderr, "AudioConsumer: looper thread did not exit in time\n"); } } + + delete[] fSwapScratch; } @@ -282,18 +284,18 @@ AudioConsumer::SetTarget(BLooper* target) void -AudioConsumer::SetRecorder(VideoRecorder* recorder) +AudioConsumer::SetAudioSink(AudioSink* sink) { - BAutolock lock(fRecorderLock); - fRecorder = recorder; + BAutolock lock(fSinkLock); + fSink = sink; } void -AudioConsumer::ClearRecorder() +AudioConsumer::ClearAudioSink() { - BAutolock lock(fRecorderLock); - fRecorder = NULL; + BAutolock lock(fSinkLock); + fSink = NULL; } @@ -344,34 +346,15 @@ AudioConsumer::_HandleBuffer(BBuffer* buffer) if (target == NULL) return; - // Write audio directly to recorder (bypasses message loop to prevent - // buffer loss from queue congestion during video frame processing) + // Push audio directly to the sink (bypasses message loop to prevent + // buffer loss from queue congestion during video frame processing). + // Raw PCM is forwarded as-is with its format; any conversion the + // destination needs (e.g. float -> int16 for AVI) is the sink's job. size_t dataSize = buffer->SizeUsed(); if (dataSize > 0) { - BAutolock recLock(fRecorderLock); - if (fRecorder != NULL) { - uint32 audioFormat = fFormat.u.raw_audio.format; - - if (audioFormat == media_raw_audio_format::B_AUDIO_FLOAT) { - // Convert 32-bit float to 16-bit PCM for AVI compatibility - const float* floatData = static_cast(buffer->Data()); - size_t sampleCount = dataSize / sizeof(float); - size_t pcmSize = sampleCount * sizeof(int16); - int16* pcmData = new int16[sampleCount]; - - for (size_t i = 0; i < sampleCount; i++) { - float sample = floatData[i]; - if (sample > 1.0f) sample = 1.0f; - if (sample < -1.0f) sample = -1.0f; - pcmData[i] = (int16)(sample * 32767.0f); - } - - fRecorder->AddAudioBuffer(pcmData, pcmSize); - delete[] pcmData; - } else { - fRecorder->AddAudioBuffer(buffer->Data(), dataSize); - } - } + BAutolock sinkLock(fSinkLock); + if (fSink != NULL) + fSink->WriteAudio(buffer->Data(), dataSize, fFormat.u.raw_audio); } bigtime_t now = system_time(); @@ -414,6 +397,18 @@ AudioConsumer::_HandleBuffer(BBuffer* buffer) } +uint8* +AudioConsumer::_SwapBuffer(size_t bytes) +{ + if (bytes > fSwapScratchSize) { + delete[] fSwapScratch; + fSwapScratch = new uint8[bytes]; + fSwapScratchSize = bytes; + } + return fSwapScratch; +} + + void AudioConsumer::_CalculateLevels(const void* data, size_t size, float* outLeft, float* outRight) @@ -448,15 +443,15 @@ AudioConsumer::_CalculateLevels(const void* data, size_t size, // Haiku x86 is little-endian (B_MEDIA_LITTLE_ENDIAN = 2) if (byteOrder == B_MEDIA_BIG_ENDIAN) { // Swap bytes for each int16 sample before computing levels - // Work on a temporary copy to avoid modifying the buffer - int16* swapped = new int16[samples]; + // Work on a reusable copy to avoid modifying the buffer + int16* swapped = reinterpret_cast( + _SwapBuffer(samples * sizeof(int16))); const uint8* raw = static_cast(data); for (size_t i = 0; i < samples; i++) { swapped[i] = (int16)((raw[i * 2 + 1]) | (raw[i * 2] << 8)); } _CalculateLevelsTyped(swapped, samples, channels, (int16)32767, outLeft, outRight); - delete[] swapped; } else { _CalculateLevelsTyped(static_cast(data), samples, channels, (int16)32767, outLeft, outRight); @@ -470,7 +465,8 @@ AudioConsumer::_CalculateLevels(const void* data, size_t size, uint32 byteOrder = fFormat.u.raw_audio.byte_order; if (byteOrder == B_MEDIA_BIG_ENDIAN) { - int32* swapped = new int32[samples]; + int32* swapped = reinterpret_cast( + _SwapBuffer(samples * sizeof(int32))); const uint8* raw = static_cast(data); for (size_t i = 0; i < samples; i++) { size_t o = i * 4; @@ -479,7 +475,6 @@ AudioConsumer::_CalculateLevels(const void* data, size_t size, } _CalculateLevelsTyped(swapped, samples, channels, (int32)2147483647, outLeft, outRight); - delete[] swapped; } else { _CalculateLevelsTyped(static_cast(data), samples, channels, (int32)2147483647, outLeft, outRight); @@ -494,10 +489,10 @@ AudioConsumer::_CalculateLevels(const void* data, size_t size, size_t frameCount = sampleCount / channels; // Byte-swap floats if big-endian - float* swapped = NULL; uint32 byteOrder = fFormat.u.raw_audio.byte_order; if (byteOrder == B_MEDIA_BIG_ENDIAN) { - swapped = new float[sampleCount]; + float* swapped = reinterpret_cast( + _SwapBuffer(sampleCount * sizeof(float))); const uint8* raw = static_cast(data); for (size_t i = 0; i < sampleCount; i++) { uint32 v = (uint32)((raw[i*4+3]) | (raw[i*4+2]<<8) | @@ -532,7 +527,6 @@ AudioConsumer::_CalculateLevels(const void* data, size_t size, *outRight = channels > 1 ? (float)sqrt(sumRight / frameCount) : *outLeft; } - delete[] swapped; break; } diff --git a/src/webcam/AudioConsumer.h b/src/webcam/AudioConsumer.h index e5b2067..8ddb74b 100644 --- a/src/webcam/AudioConsumer.h +++ b/src/webcam/AudioConsumer.h @@ -12,6 +12,8 @@ #include #include +#include "AudioSink.h" + class AudioConsumer : public BMediaEventLooper, public BBufferConsumer { public: AudioConsumer(const char* name, BLooper* target, @@ -56,14 +58,18 @@ class AudioConsumer : public BMediaEventLooper, public BBufferConsumer { // Target management (thread-safe) void SetTarget(BLooper* target); - // Direct recorder access (bypasses message loop for audio data) - void SetRecorder(class VideoRecorder* recorder); - void ClearRecorder(); + // Direct audio sink (bypasses message loop for audio data). + // The sink receives raw PCM straight from the audio thread. + void SetAudioSink(AudioSink* sink); + void ClearAudioSink(); private: void _HandleBuffer(BBuffer* buffer); void _CalculateLevels(const void* data, size_t size, float* outLeft, float* outRight); + // Returns a reusable buffer of at least 'bytes'. Avoids per-buffer + // allocation on the real-time audio thread. + uint8* _SwapBuffer(size_t bytes); template void _CalculateLevelsTyped(const T* data, size_t samples, @@ -87,9 +93,13 @@ class AudioConsumer : public BMediaEventLooper, public BBufferConsumer { int32 fBufferCount; int32 fLevelLogCount; - // Direct recording path (bypasses message loop) - class VideoRecorder* fRecorder; - mutable BLocker fRecorderLock; + // Direct audio path (bypasses message loop) + AudioSink* fSink; + mutable BLocker fSinkLock; + + // Reusable byte-swap scratch (big-endian sources only) + uint8* fSwapScratch; + size_t fSwapScratchSize; }; #endif // AUDIO_CONSUMER_H diff --git a/src/webcam/AudioSink.h b/src/webcam/AudioSink.h new file mode 100644 index 0000000..62b4c48 --- /dev/null +++ b/src/webcam/AudioSink.h @@ -0,0 +1,37 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + * + * AudioSink - Abstract destination for captured audio. + * + * AudioConsumer pushes every captured audio buffer to an AudioSink directly + * from the real-time audio thread (it bypasses the message loop to avoid + * buffer loss during video processing). Implement this interface to route + * audio anywhere - an AVI recorder, an encoder, a network stream - without + * the capture library having to know about the destination. + * + * Contract: + * - WriteAudio() may be called from a real-time media thread: be fast and + * do your own locking; never block. + * - 'data' is valid only for the duration of the call. Copy it to keep it. + * - 'data' is raw PCM described by 'format' (sample format, channel count, + * byte order, frame rate). The sink is responsible for any conversion it + * needs (e.g. float -> int16). + */ + +#ifndef AUDIO_SINK_H +#define AUDIO_SINK_H + +#include +#include + +class AudioSink { +public: + virtual ~AudioSink() {} + + virtual void WriteAudio(const void* data, size_t size, + const media_raw_audio_format& format) = 0; +}; + +#endif // AUDIO_SINK_H diff --git a/src/webcam/VideoConsumer.cpp b/src/webcam/VideoConsumer.cpp index f529133..b2acc51 100644 --- a/src/webcam/VideoConsumer.cpp +++ b/src/webcam/VideoConsumer.cpp @@ -9,7 +9,6 @@ */ #include "VideoConsumer.h" -#include "MainWindow.h" #include #include @@ -51,14 +50,13 @@ const uint32 kFallbackHeight = 240; VideoConsumer::VideoConsumer(const char* name, BLooper* target, - uint32 frameMessage, uint32 audioMessage) + uint32 frameMessage) : BMediaNode(name), BMediaEventLooper(), BBufferConsumer(B_MEDIA_RAW_VIDEO), // Prefer RAW_VIDEO but accept others in AcceptFormat fTarget(target), fFrameMessage(frameMessage), - fAudioMessage(audioMessage), fConnected(false), fBuffers(NULL), fDisplayBitmap(NULL), @@ -605,7 +603,35 @@ VideoConsumer::_HandleBuffer(BBuffer* buffer) } } - if (_IsMJPEGData(bufData, bufSize)) { + if (_IsH264Data(bufData, bufSize)) { + // H.264 NAL unit detected. Currently we display an informational + // pattern since Haiku lacks a native H.264 decoder. A future + // version can integrate ffmpeg for actual decoding. + static int32 sH264WarnCount = 0; + if (++sH264WarnCount <= 3) { + LOG_WARNING("H.264 encoded frame detected (%zu bytes). " + "Decoding not yet supported - showing placeholder.", + bufSize); + } + + BBitmap* dest = fDisplayBitmap; + if (dest == NULL && fBitmap[0] != NULL) + dest = fBitmap[0]; + if (dest != NULL) { + uint8* dst = (uint8*)dest->Bits(); + int32 bpr = dest->BytesPerRow(); + // Blue gradient pattern with "H.264" indicator + for (int32 y = 0; y < fBitmapHeight; y++) { + uint32* row = reinterpret_cast(dst + y * bpr); + for (int32 x = 0; x < fBitmapWidth; x++) { + uint8 b = (uint8)(60 + y * 120 / fBitmapHeight); + uint8 g = (uint8)(20 + x * 40 / fBitmapWidth); + row[x] = 0xFF000000 | (g << 8) | b; + } + } + _SendFrameToTarget(dest); + } + } else if (_IsMJPEGData(bufData, bufSize)) { // MJPEG frame - decompress to display bitmap // Note: _DecompressMJPEG may recreate fDisplayBitmap if JPEG // dimensions differ, so use fDisplayBitmap after the call @@ -1296,6 +1322,20 @@ VideoConsumer::_IsMJPEGData(const uint8* data, size_t size) const } +bool +VideoConsumer::_IsH264Data(const uint8* data, size_t size) const +{ + // H.264 NAL units start with 0x00 0x00 0x01 or 0x00 0x00 0x00 0x01 + if (size >= 4 && data[0] == 0x00 && data[1] == 0x00) { + if (data[2] == 0x01) + return true; + if (size >= 5 && data[2] == 0x00 && data[3] == 0x01) + return true; + } + return false; +} + + bool VideoConsumer::_DecompressMJPEG(const uint8* src, size_t srcSize, BBitmap* destBitmap) diff --git a/src/webcam/VideoConsumer.h b/src/webcam/VideoConsumer.h index a2f017e..de47c01 100644 --- a/src/webcam/VideoConsumer.h +++ b/src/webcam/VideoConsumer.h @@ -32,7 +32,7 @@ class VideoConsumer : public BMediaEventLooper, public BBufferConsumer { public: VideoConsumer(const char* name, BLooper* target, - uint32 frameMessage, uint32 audioMessage); + uint32 frameMessage); virtual ~VideoConsumer(); // BMediaNode interface @@ -115,12 +115,12 @@ class VideoConsumer : public BMediaEventLooper, public BBufferConsumer { bool _DecompressMJPEG(const uint8* src, size_t srcSize, BBitmap* destBitmap); bool _IsMJPEGData(const uint8* data, size_t size) const; + bool _IsH264Data(const uint8* data, size_t size) const; void _SendFrameToTarget(BBitmap* bitmap); BLooper* fTarget; mutable BLocker fTargetLock; uint32 fFrameMessage; - uint32 fAudioMessage; media_input fInput; media_destination fDestination; diff --git a/src/webcam/VirtualProducer.cpp b/src/webcam/VirtualProducer.cpp new file mode 100644 index 0000000..eeaf968 --- /dev/null +++ b/src/webcam/VirtualProducer.cpp @@ -0,0 +1,276 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + */ + +#include "VirtualProducer.h" + +#include +#include +#include +#include + +#include +#include + + +VirtualProducer::VirtualProducer(const char* name) + : + BMediaNode(name), + BMediaEventLooper(), + BBufferProducer(B_MEDIA_RAW_VIDEO), + fConnected(false), + fEnabled(false), + fBufferGroup(NULL), + fWidth(640), + fHeight(480), + fFPS(30.0f) +{ + fOutput.destination = media_destination::null; + fOutput.source.port = ControlPort(); + fOutput.source.id = 0; + fOutput.node = Node(); + strcpy(fOutput.name, "BubiCam Virtual Output"); + + fOutput.format.type = B_MEDIA_RAW_VIDEO; + fOutput.format.u.raw_video.display.line_width = fWidth; + fOutput.format.u.raw_video.display.line_count = fHeight; + fOutput.format.u.raw_video.display.format = B_RGB32; + fOutput.format.u.raw_video.field_rate = fFPS; +} + + +VirtualProducer::~VirtualProducer() +{ + delete fBufferGroup; +} + + +void +VirtualProducer::SetFormat(int32 width, int32 height, float fps) +{ + BAutolock lock(fLock); + fWidth = width; + fHeight = height; + fFPS = fps; +} + + +status_t +VirtualProducer::PushFrame(BBitmap* bitmap) +{ + BAutolock lock(fLock); + + if (!fConnected || !fEnabled || bitmap == NULL) + return B_NOT_ALLOWED; + + if (fBufferGroup == NULL) + return B_NO_INIT; + + BBuffer* buffer = fBufferGroup->RequestBuffer( + fWidth * fHeight * 4, 10000); + if (buffer == NULL) + return B_WOULD_BLOCK; + + memcpy(buffer->Data(), bitmap->Bits(), + min_c((size_t)buffer->SizeAvailable(), (size_t)bitmap->BitsLength())); + + media_header* header = buffer->Header(); + header->type = B_MEDIA_RAW_VIDEO; + header->size_used = fWidth * fHeight * 4; + header->time_source = TimeSource()->ID(); + header->start_time = TimeSource()->Now(); + + status_t err = SendBuffer(buffer, fOutput.source, fOutput.destination); + if (err != B_OK) + buffer->Recycle(); + + return err; +} + + +BMediaAddOn* +VirtualProducer::AddOn(int32* internalId) const +{ + if (internalId != NULL) + *internalId = 0; + return NULL; +} + + +void +VirtualProducer::NodeRegistered() +{ + fOutput.source.port = ControlPort(); + fOutput.source.id = 0; + fOutput.node = Node(); + + SetPriority(B_REAL_TIME_PRIORITY); + Run(); +} + + +void +VirtualProducer::HandleEvent(const media_timed_event* event, + bigtime_t lateness, bool realTimeEvent) +{ + // No periodic events needed - frames are pushed externally +} + + +status_t +VirtualProducer::FormatSuggestionRequested(media_type type, + int32 quality, media_format* format) +{ + if (type != B_MEDIA_RAW_VIDEO && type != B_MEDIA_UNKNOWN_TYPE) + return B_MEDIA_BAD_FORMAT; + + *format = fOutput.format; + return B_OK; +} + + +status_t +VirtualProducer::FormatProposal(const media_source& output, + media_format* format) +{ + if (output != fOutput.source) + return B_MEDIA_BAD_SOURCE; + + format->type = B_MEDIA_RAW_VIDEO; + format->u.raw_video.display.format = B_RGB32; + format->u.raw_video.display.line_width = fWidth; + format->u.raw_video.display.line_count = fHeight; + format->u.raw_video.field_rate = fFPS; + + return B_OK; +} + + +status_t +VirtualProducer::FormatChangeRequested(const media_source& source, + const media_destination& destination, media_format* ioFormat, + int32* _deprecated_) +{ + return B_ERROR; +} + + +status_t +VirtualProducer::GetNextOutput(int32* cookie, media_output* outOutput) +{ + if (*cookie != 0) + return B_BAD_INDEX; + + *outOutput = fOutput; + (*cookie)++; + return B_OK; +} + + +status_t +VirtualProducer::DisposeOutputCookie(int32 cookie) +{ + return B_OK; +} + + +status_t +VirtualProducer::SetBufferGroup(const media_source& forSource, + BBufferGroup* group) +{ + if (forSource != fOutput.source) + return B_MEDIA_BAD_SOURCE; + + BAutolock lock(fLock); + delete fBufferGroup; + fBufferGroup = group; + return B_OK; +} + + +status_t +VirtualProducer::PrepareToConnect(const media_source& what, + const media_destination& where, media_format* format, + media_source* outSource, char* outName) +{ + if (what != fOutput.source) + return B_MEDIA_BAD_SOURCE; + + if (fConnected) + return B_MEDIA_ALREADY_CONNECTED; + + *format = fOutput.format; + *outSource = fOutput.source; + strcpy(outName, "BubiCam Virtual"); + + return B_OK; +} + + +void +VirtualProducer::Connect(status_t error, const media_source& source, + const media_destination& destination, const media_format& format, + char* ioName) +{ + if (error != B_OK) { + fOutput.destination = media_destination::null; + return; + } + + BAutolock lock(fLock); + fOutput.destination = destination; + fOutput.format = format; + fConnected = true; + + // Create buffer group + int32 frameSize = fWidth * fHeight * 4; + delete fBufferGroup; + fBufferGroup = new BBufferGroup(frameSize, 3); + + strcpy(ioName, "BubiCam Virtual"); +} + + +void +VirtualProducer::Disconnect(const media_source& what, + const media_destination& where) +{ + if (what != fOutput.source || where != fOutput.destination) + return; + + BAutolock lock(fLock); + fOutput.destination = media_destination::null; + fConnected = false; + fEnabled = false; + + delete fBufferGroup; + fBufferGroup = NULL; +} + + +void +VirtualProducer::EnableOutput(const media_source& what, + bool enabled, int32* _deprecated_) +{ + if (what == fOutput.source) + fEnabled = enabled; +} + + +status_t +VirtualProducer::GetLatency(bigtime_t* outLatency) +{ + *outLatency = 10000; // 10ms + return B_OK; +} + + +void +VirtualProducer::LatencyChanged(const media_source& source, + const media_destination& destination, bigtime_t newLatency, + uint32 flags) +{ + // Nothing to adjust +} diff --git a/src/webcam/VirtualProducer.h b/src/webcam/VirtualProducer.h new file mode 100644 index 0000000..d387765 --- /dev/null +++ b/src/webcam/VirtualProducer.h @@ -0,0 +1,88 @@ +/* + * BubiCam - Webcam Driver Tester for Haiku OS + * Copyright (c) 2024 BubiCam Contributors + * MIT License + * + * VirtualProducer - Virtual webcam media node producer. + * + * Creates a virtual video producer node in the Media Kit that other + * applications can connect to as if it were a real webcam. BubiCam + * feeds processed frames into this node, allowing other apps to + * receive the BubiCam video stream with any active filters applied. + * + * Status: Scaffold implementation. Register/unregister with the + * Media Kit works; frame delivery requires a connected consumer. + */ + +#ifndef VIRTUAL_PRODUCER_H +#define VIRTUAL_PRODUCER_H + +#include +#include +#include +#include +#include +#include + +class VirtualProducer : public BMediaEventLooper, public BBufferProducer { +public: + VirtualProducer(const char* name = "BubiCam Virtual"); + virtual ~VirtualProducer(); + + // Feed a frame into the virtual producer + status_t PushFrame(BBitmap* bitmap); + + // Configuration + void SetFormat(int32 width, int32 height, float fps); + bool IsConnected() const { return fConnected; } + + // BMediaNode interface + virtual BMediaAddOn* AddOn(int32* internalId) const; + virtual void NodeRegistered(); + + // BMediaEventLooper interface + virtual void HandleEvent(const media_timed_event* event, + bigtime_t lateness, bool realTimeEvent); + + // BBufferProducer interface + virtual status_t FormatSuggestionRequested(media_type type, + int32 quality, media_format* format); + virtual status_t FormatProposal(const media_source& output, + media_format* format); + virtual status_t FormatChangeRequested(const media_source& source, + const media_destination& destination, + media_format* ioFormat, int32* _deprecated_); + virtual status_t GetNextOutput(int32* cookie, + media_output* outOutput); + virtual status_t DisposeOutputCookie(int32 cookie); + virtual status_t SetBufferGroup(const media_source& forSource, + BBufferGroup* group); + virtual status_t PrepareToConnect(const media_source& what, + const media_destination& where, + media_format* format, media_source* outSource, + char* outName); + virtual void Connect(status_t error, const media_source& source, + const media_destination& destination, + const media_format& format, char* ioName); + virtual void Disconnect(const media_source& what, + const media_destination& where); + virtual void EnableOutput(const media_source& what, + bool enabled, int32* _deprecated_); + virtual status_t GetLatency(bigtime_t* outLatency); + virtual void LatencyChanged(const media_source& source, + const media_destination& destination, + bigtime_t newLatency, uint32 flags); + +private: + media_output fOutput; + bool fConnected; + bool fEnabled; + BBufferGroup* fBufferGroup; + BLocker fLock; + + int32 fWidth; + int32 fHeight; + float fFPS; +}; + +#endif // VIRTUAL_PRODUCER_H diff --git a/src/webcam/WebcamDevice.cpp b/src/webcam/WebcamDevice.cpp index c9acfe4..7153069 100644 --- a/src/webcam/WebcamDevice.cpp +++ b/src/webcam/WebcamDevice.cpp @@ -7,7 +7,6 @@ #include "WebcamDevice.h" #include "VideoConsumer.h" #include "AudioConsumer.h" -#include "MainWindow.h" // Logging macros using centralized ErrorUtils #define LOG_MODULE "WebcamDevice" @@ -178,6 +177,8 @@ WebcamDevice::WebcamDevice(const media_node& node, const dormant_node_info& info fAudioProducerInstantiated(false), fIsCapturing(false), fTarget(NULL), + fFrameMessage(MSG_WEBCAM_FRAME), + fAudioLevelMessage(MSG_WEBCAM_AUDIO_LEVEL), fUsedLiveNode(false), fAudioNodeID(-1) { @@ -211,6 +212,8 @@ WebcamDevice::WebcamDevice(const dormant_node_info& info, status_t instantiateEr fAudioProducerInstantiated(false), fIsCapturing(false), fTarget(NULL), + fFrameMessage(MSG_WEBCAM_FRAME), + fAudioLevelMessage(MSG_WEBCAM_AUDIO_LEVEL), fUsedLiveNode(false), fAudioNodeID(-1) { @@ -303,6 +306,32 @@ WebcamDevice::GetCurrentFrame() const } +void +WebcamDevice::SetAudioSink(AudioSink* sink) +{ + AudioConsumer* consumer = NULL; + { + BAutolock lock(fCaptureLock); + consumer = fAudioConsumer; + } + if (consumer != NULL) + consumer->SetAudioSink(sink); +} + + +void +WebcamDevice::ClearAudioSink() +{ + AudioConsumer* consumer = NULL; + { + BAutolock lock(fCaptureLock); + consumer = fAudioConsumer; + } + if (consumer != NULL) + consumer->ClearAudioSink(); +} + + status_t WebcamDevice::GatherDeviceInfo() { @@ -574,7 +603,8 @@ WebcamDevice::_GatherAudioInfo() status_t -WebcamDevice::StartCapture(BLooper* target) +WebcamDevice::StartCapture(BLooper* target, uint32 frameMessage, + uint32 audioLevelMessage) { LOG_INFO("Starting capture for '%s'", fName.String()); @@ -586,6 +616,8 @@ WebcamDevice::StartCapture(BLooper* target) } fTarget = target; + fFrameMessage = frameMessage; + fAudioLevelMessage = audioLevelMessage; fUsedLiveNode = false; BMediaRoster* roster = BMediaRoster::Roster(); @@ -874,7 +906,7 @@ WebcamDevice::_SetupVideoConnection() // Create video consumer fVideoConsumer = new VideoConsumer("BubiCam Video", fTarget, - MSG_FRAME_RECEIVED, MSG_AUDIO_LEVEL); + fFrameMessage); // Register consumer status = roster->RegisterNode(fVideoConsumer); @@ -1365,7 +1397,7 @@ WebcamDevice::_SetupAudioConnection() // Create audio consumer fAudioConsumer = new AudioConsumer("BubiCam Audio", fTarget, - MSG_AUDIO_LEVEL); + fAudioLevelMessage); // Register consumer status = roster->RegisterNode(fAudioConsumer); diff --git a/src/webcam/WebcamDevice.h b/src/webcam/WebcamDevice.h index 2b0c9ac..9c7afe0 100644 --- a/src/webcam/WebcamDevice.h +++ b/src/webcam/WebcamDevice.h @@ -22,6 +22,16 @@ class VideoConsumer; class AudioConsumer; +class AudioSink; + +// Default messages posted to the capture target looper. +// They are owned by the capture library (not the application) so the +// component has no backward dependency on any app header. Override them +// per-capture via StartCapture() to plug into your own message protocol. +enum { + MSG_WEBCAM_FRAME = 'frcv', + MSG_WEBCAM_AUDIO_LEVEL = 'audl' +}; // Video format information struct VideoFormat { @@ -124,8 +134,13 @@ class WebcamDevice { const dormant_node_info& DormantInfo() const { return fDormantInfo; } void MarkNodeReleased() { fNodeInstantiated = false; } - // Capture control - status_t StartCapture(BLooper* target); + // Capture control. + // frameMessage / audioLevelMessage are the BMessage 'what' codes posted + // to 'target' for each video frame and audio level update. Defaults keep + // backward compatibility; override to integrate with a custom protocol. + status_t StartCapture(BLooper* target, + uint32 frameMessage = MSG_WEBCAM_FRAME, + uint32 audioLevelMessage = MSG_WEBCAM_AUDIO_LEVEL); void StopCapture(); bool IsCapturing() const { return fIsCapturing; } @@ -133,10 +148,14 @@ class WebcamDevice { void SetAudioNodeID(int32 nodeID) { fAudioNodeID = nodeID; } int32 AudioNodeID() const { return fAudioNodeID; } + // Route captured audio to a sink (recorder, encoder, ...). Safe to call + // before or during capture; ignored if there is no active audio consumer. + void SetAudioSink(AudioSink* sink); + void ClearAudioSink(); + // Frame access (for MCP server) BBitmap* GetCurrentFrame() const; VideoConsumer* GetVideoConsumer() const { return fVideoConsumer; } - AudioConsumer* GetAudioConsumer() const { return fAudioConsumer; } // Capture statistics uint32 FramesCaptured() const; @@ -214,6 +233,8 @@ class WebcamDevice { // Capture state bool fIsCapturing; BLooper* fTarget; + uint32 fFrameMessage; // posted per video frame + uint32 fAudioLevelMessage; // posted per audio level update bool fUsedLiveNode; // True if we used an existing live node int32 fAudioNodeID; // -1=auto, 0=none, >0=specific node mutable BLocker fCaptureLock; // Protects consumer pointers during capture/stop diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..c9f7430 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,23 @@ +# BubiCam Test Suite +# Build: make -f tests/Makefile +# Run: objects.x86_64-cc13-release/BubiCamTests + +NAME = BubiCamTests +TYPE = APP +APP_MIME_SIG = application/x-vnd.BubiCam-Tests + +SRCS = tests/TestRunner.cpp + +LIBS = be media translation $(STDCPPLIBS) + +SYSTEM_INCLUDE_PATHS = /boot/system/develop/headers/private/shared + +LOCAL_INCLUDE_PATHS = src src/webcam src/utils + +OPTIMIZE := FULL + +WARNINGS = ALL + +COMPILER_FLAGS = -msse2 + +include $(BUILDHOME)/etc/makefile-engine diff --git a/tests/TestRunner.cpp b/tests/TestRunner.cpp new file mode 100644 index 0000000..49cd2e8 --- /dev/null +++ b/tests/TestRunner.cpp @@ -0,0 +1,672 @@ +/* + * BubiCam Test Runner + * Automated tests and benchmarks for the webcam capture component. + * + * Build: make -f tests/Makefile + * Run: objects.x86_64-cc13-release/BubiCamTests + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// ============================================================================ +// Minimal test framework +// ============================================================================ + +static int sTestsPassed = 0; +static int sTestsFailed = 0; +static int sTestsTotal = 0; + +#define TEST_ASSERT(cond, msg) do { \ + sTestsTotal++; \ + if (!(cond)) { \ + fprintf(stderr, " FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ + sTestsFailed++; \ + } else { \ + sTestsPassed++; \ + } \ +} while (0) + +#define TEST_SECTION(name) \ + fprintf(stderr, "\n--- %s ---\n", name) + +#define BENCH_START(name, iterations) \ + { \ + const char* _bench_name = name; \ + int32 _bench_iters = iterations; \ + bigtime_t _bench_start = system_time(); + +#define BENCH_END() \ + bigtime_t _bench_elapsed = system_time() - _bench_start; \ + double _bench_ms = _bench_elapsed / 1000.0; \ + double _bench_per = _bench_ms / _bench_iters; \ + fprintf(stderr, " BENCH: %-40s %8.2f ms total, %6.3f ms/iter (%d iters)\n", \ + _bench_name, _bench_ms, _bench_per, (int)_bench_iters); \ + } + + +// ============================================================================ +// YUV conversion reference (scalar, known-correct) +// ============================================================================ + +static inline void +yuv_to_bgra(int y, int u, int v, uint8* out) +{ + int c = y - 16; + int d = u - 128; + int e = v - 128; + + int r = (298 * c + 409 * e + 128) >> 8; + int g = (298 * c - 100 * d - 208 * e + 128) >> 8; + int b = (298 * c + 516 * d + 128) >> 8; + + out[0] = (uint8)(b < 0 ? 0 : (b > 255 ? 255 : b)); + out[1] = (uint8)(g < 0 ? 0 : (g > 255 ? 255 : g)); + out[2] = (uint8)(r < 0 ? 0 : (r > 255 ? 255 : r)); + out[3] = 255; +} + + +// Scalar YUV422 (YUYV) to BGRA +static void +ConvertYUV422_Scalar(const uint8* src, uint8* dst, int32 width, int32 height, + int32 srcBytesPerRow, int32 dstBytesPerRow) +{ + for (int32 y = 0; y < height; y++) { + const uint8* srcRow = src + y * srcBytesPerRow; + uint8* dstRow = dst + y * dstBytesPerRow; + + for (int32 x = 0; x < width; x += 2) { + int y0 = srcRow[0]; + int u = srcRow[1]; + int y1 = srcRow[2]; + int v = srcRow[3]; + + yuv_to_bgra(y0, u, v, dstRow); + yuv_to_bgra(y1, u, v, dstRow + 4); + + srcRow += 4; + dstRow += 8; + } + } +} + + +// SSE2 YUV422 (YUYV) to BGRA +#ifdef __SSE2__ +static void +ConvertYUV422_SSE2(const uint8* src, uint8* dst, int32 width, int32 height, + int32 srcBytesPerRow, int32 dstBytesPerRow) +{ + const __m128i mask_y = _mm_set1_epi16(0x00FF); + const __m128i sub16 = _mm_set1_epi16(16); + const __m128i sub128 = _mm_set1_epi16(128); + const __m128i coeff_y = _mm_set1_epi16(298); + const __m128i coeff_rv = _mm_set1_epi16(409); + const __m128i coeff_gu = _mm_set1_epi16(100); + const __m128i coeff_gv = _mm_set1_epi16(208); + const __m128i coeff_bu = _mm_set1_epi16(516); + const __m128i zero = _mm_setzero_si128(); + const __m128i alpha = _mm_set1_epi8((char)0xFF); + + for (int32 y = 0; y < height; y++) { + const uint8* srcRow = src + y * srcBytesPerRow; + uint8* dstRow = dst + y * dstBytesPerRow; + + int32 x = 0; + for (; x + 7 < width; x += 8) { + // Load 16 bytes (8 pixels in YUYV) + __m128i yuyv = _mm_loadu_si128((const __m128i*)srcRow); + + // Extract Y (even bytes) and UV (odd bytes) + __m128i y_vals = _mm_and_si128(yuyv, mask_y); + __m128i uv_raw = _mm_srli_epi16(yuyv, 8); + + // Separate U and V + __m128i u_packed = _mm_and_si128(uv_raw, _mm_set1_epi32(0x0000FFFF)); + __m128i v_packed = _mm_srli_epi32(uv_raw, 16); + + // Duplicate U and V for each pixel pair + __m128i u_vals = _mm_or_si128(u_packed, _mm_slli_epi32(u_packed, 16)); + __m128i v_vals = _mm_or_si128(v_packed, _mm_slli_epi32(v_packed, 16)); + + // Apply YUV to RGB conversion + __m128i c = _mm_sub_epi16(y_vals, sub16); + __m128i d = _mm_sub_epi16(u_vals, sub128); + __m128i e = _mm_sub_epi16(v_vals, sub128); + + __m128i cy = _mm_mullo_epi16(c, coeff_y); + + __m128i r16 = _mm_srai_epi16(_mm_add_epi16( + _mm_add_epi16(cy, _mm_mullo_epi16(e, coeff_rv)), + _mm_set1_epi16(128)), 8); + __m128i g16 = _mm_srai_epi16(_mm_add_epi16( + _mm_sub_epi16( + _mm_sub_epi16(cy, _mm_mullo_epi16(d, coeff_gu)), + _mm_mullo_epi16(e, coeff_gv)), + _mm_set1_epi16(128)), 8); + __m128i b16 = _mm_srai_epi16(_mm_add_epi16( + _mm_add_epi16(cy, _mm_mullo_epi16(d, coeff_bu)), + _mm_set1_epi16(128)), 8); + + // Clamp 0-255 + r16 = _mm_max_epi16(_mm_min_epi16(r16, _mm_set1_epi16(255)), zero); + g16 = _mm_max_epi16(_mm_min_epi16(g16, _mm_set1_epi16(255)), zero); + b16 = _mm_max_epi16(_mm_min_epi16(b16, _mm_set1_epi16(255)), zero); + + // Pack to bytes + __m128i r8 = _mm_packus_epi16(r16, zero); + __m128i g8 = _mm_packus_epi16(g16, zero); + __m128i b8 = _mm_packus_epi16(b16, zero); + + // Interleave to BGRA + __m128i bg = _mm_unpacklo_epi8(b8, g8); + __m128i ra = _mm_unpacklo_epi8(r8, alpha); + __m128i bgra_lo = _mm_unpacklo_epi16(bg, ra); + __m128i bgra_hi = _mm_unpackhi_epi16(bg, ra); + + _mm_storeu_si128((__m128i*)(dstRow), bgra_lo); + _mm_storeu_si128((__m128i*)(dstRow + 16), bgra_hi); + + srcRow += 16; + dstRow += 32; + } + + // Scalar fallback for remaining pixels + for (; x + 1 < width; x += 2) { + int y0 = srcRow[0], u = srcRow[1], y1 = srcRow[2], v = srcRow[3]; + yuv_to_bgra(y0, u, v, dstRow); + yuv_to_bgra(y1, u, v, dstRow + 4); + srcRow += 4; + dstRow += 8; + } + } +} +#endif + + +// Scalar NV12 to BGRA +static void +ConvertNV12_Scalar(const uint8* src, uint8* dst, int32 width, int32 height, + int32 dstBytesPerRow) +{ + const uint8* yPlane = src; + const uint8* uvPlane = src + width * height; + + for (int32 y = 0; y < height; y++) { + const uint8* yRow = yPlane + y * width; + const uint8* uvRow = uvPlane + (y / 2) * width; + uint8* dstRow = dst + y * dstBytesPerRow; + + for (int32 x = 0; x < width; x += 2) { + int u = uvRow[x]; + int v = uvRow[x + 1]; + + yuv_to_bgra(yRow[x], u, v, dstRow + x * 4); + if (x + 1 < width) + yuv_to_bgra(yRow[x + 1], u, v, dstRow + (x + 1) * 4); + } + } +} + + +// ============================================================================ +// Test: YUV422 conversion correctness +// ============================================================================ + +static void +TestYUV422Conversion() +{ + TEST_SECTION("YUV422 (YUYV) to BGRA Conversion"); + + const int32 width = 320; + const int32 height = 240; + const int32 srcBytesPerRow = width * 2; + const int32 dstBytesPerRow = width * 4; + + uint8* srcBuf = new uint8[srcBytesPerRow * height]; + uint8* dstScalar = new uint8[dstBytesPerRow * height]; + uint8* dstSSE2 = new uint8[dstBytesPerRow * height]; + + // Generate test pattern: gradient + for (int32 y = 0; y < height; y++) { + for (int32 x = 0; x < width; x += 2) { + int32 offset = y * srcBytesPerRow + x * 2; + srcBuf[offset + 0] = (uint8)((x * 255) / width); // Y0 + srcBuf[offset + 1] = (uint8)(128 + (y * 50) / height); // U + srcBuf[offset + 2] = (uint8)((x * 200) / width + 30); // Y1 + srcBuf[offset + 3] = (uint8)(128 - (y * 50) / height); // V + } + } + + // Convert with scalar + ConvertYUV422_Scalar(srcBuf, dstScalar, width, height, + srcBytesPerRow, dstBytesPerRow); + + // Verify scalar output is non-zero + bool hasNonZero = false; + for (int32 i = 0; i < dstBytesPerRow * height; i++) { + if (dstScalar[i] != 0) { hasNonZero = true; break; } + } + TEST_ASSERT(hasNonZero, "Scalar YUV422 produces non-zero output"); + + // Verify alpha channel is always 255 + bool alphaOK = true; + for (int32 y = 0; y < height && alphaOK; y++) { + for (int32 x = 0; x < width && alphaOK; x++) { + if (dstScalar[y * dstBytesPerRow + x * 4 + 3] != 255) + alphaOK = false; + } + } + TEST_ASSERT(alphaOK, "Scalar YUV422 alpha is always 255"); + + // Verify RGB values are in valid range (clamped) + bool rangeOK = true; + for (int32 i = 0; i < dstBytesPerRow * height; i++) { + if (dstScalar[i] > 255) { rangeOK = false; break; } + } + TEST_ASSERT(rangeOK, "Scalar YUV422 values in 0-255 range"); + +#ifdef __SSE2__ + // Convert with SSE2 + ConvertYUV422_SSE2(srcBuf, dstSSE2, width, height, + srcBytesPerRow, dstBytesPerRow); + + // Verify SSE2 produces non-zero output (basic sanity) + bool sse2HasOutput = false; + for (int32 i = 0; i < dstBytesPerRow * height; i++) { + if (dstSSE2[i] != 0) { sse2HasOutput = true; break; } + } + TEST_ASSERT(sse2HasOutput, "SSE2 YUV422 produces non-zero output"); + + // Verify SSE2 alpha channel is always 255 + bool sse2AlphaOK = true; + for (int32 y = 0; y < height && sse2AlphaOK; y++) { + for (int32 x = 0; x < width && sse2AlphaOK; x++) { + if (dstSSE2[y * dstBytesPerRow + x * 4 + 3] != 255) + sse2AlphaOK = false; + } + } + TEST_ASSERT(sse2AlphaOK, "SSE2 YUV422 alpha is always 255"); +#else + fprintf(stderr, " SKIP: SSE2 not available\n"); +#endif + + // Test known values: pure white (Y=235, U=128, V=128) -> should be ~(255,255,255) + uint8 whiteYUYV[4] = { 235, 128, 235, 128 }; + uint8 whiteOut[8] = {0}; + ConvertYUV422_Scalar(whiteYUYV, whiteOut, 2, 1, 4, 8); + TEST_ASSERT(whiteOut[0] >= 250 && whiteOut[1] >= 250 && whiteOut[2] >= 250, + "White YUV (235,128,128) -> near-white RGB"); + + // Test known values: pure black (Y=16, U=128, V=128) -> should be ~(0,0,0) + uint8 blackYUYV[4] = { 16, 128, 16, 128 }; + uint8 blackOut[8] = {0}; + ConvertYUV422_Scalar(blackYUYV, blackOut, 2, 1, 4, 8); + TEST_ASSERT(blackOut[0] <= 5 && blackOut[1] <= 5 && blackOut[2] <= 5, + "Black YUV (16,128,128) -> near-black RGB"); + + // Test known values: pure red (Y=82, U=90, V=240) + uint8 redYUYV[4] = { 82, 90, 82, 240 }; + uint8 redOut[8] = {0}; + ConvertYUV422_Scalar(redYUYV, redOut, 2, 1, 4, 8); + TEST_ASSERT(redOut[2] > 200, "Red YUV -> R channel > 200"); + TEST_ASSERT(redOut[1] < 50, "Red YUV -> G channel < 50"); + TEST_ASSERT(redOut[0] < 50, "Red YUV -> B channel < 50"); + + delete[] srcBuf; + delete[] dstScalar; + delete[] dstSSE2; +} + + +// ============================================================================ +// Test: NV12 conversion correctness +// ============================================================================ + +static void +TestNV12Conversion() +{ + TEST_SECTION("NV12 to BGRA Conversion"); + + const int32 width = 320; + const int32 height = 240; + const int32 dstBytesPerRow = width * 4; + size_t srcSize = width * height * 3 / 2; + + uint8* srcBuf = new uint8[srcSize]; + uint8* dstBuf = new uint8[dstBytesPerRow * height]; + + // Generate NV12 test pattern: gray gradient + uint8* yPlane = srcBuf; + uint8* uvPlane = srcBuf + width * height; + for (int32 y = 0; y < height; y++) { + for (int32 x = 0; x < width; x++) { + yPlane[y * width + x] = (uint8)((y * 219) / height + 16); + } + } + for (int32 y = 0; y < height / 2; y++) { + for (int32 x = 0; x < width; x += 2) { + uvPlane[y * width + x] = 128; // U + uvPlane[y * width + x + 1] = 128; // V + } + } + + ConvertNV12_Scalar(srcBuf, dstBuf, width, height, dstBytesPerRow); + + // Gray (U=128, V=128) should produce R == G == B + bool grayOK = true; + int maxChannelDiff = 0; + for (int32 y = 0; y < height && grayOK; y++) { + for (int32 x = 0; x < width && grayOK; x++) { + uint8* px = dstBuf + y * dstBytesPerRow + x * 4; + int diff = abs((int)px[0] - (int)px[2]); + if (diff > maxChannelDiff) maxChannelDiff = diff; + if (diff > 2) grayOK = false; + } + } + TEST_ASSERT(grayOK, "NV12 gray gradient: R ~= G ~= B (diff <= 2)"); + + // Verify size validation would reject truncated buffer + TEST_ASSERT(srcSize == (size_t)(width * height * 3 / 2), + "NV12 expected size = width * height * 3/2"); + + delete[] srcBuf; + delete[] dstBuf; +} + + +// ============================================================================ +// Test: Buffer size validation +// ============================================================================ + +static void +TestBufferValidation() +{ + TEST_SECTION("Buffer Size Validation"); + + int32 width = 640, height = 480; + + // YUV422: expected = width * height * 2 + size_t expectedYUV422 = (size_t)(width * height * 2); + TEST_ASSERT(expectedYUV422 == 614400, "YUV422 expected size 640x480 = 614400"); + + // YUV420: expected = width * height * 3/2 + size_t expectedYUV420 = (size_t)(width * height * 3 / 2); + TEST_ASSERT(expectedYUV420 == 460800, "YUV420 expected size 640x480 = 460800"); + + // NV12: same as YUV420 + TEST_ASSERT(expectedYUV420 == expectedYUV420, "NV12 size == YUV420 size"); + + // RGB32: expected = width * height * 4 + size_t expectedRGB32 = (size_t)(width * height * 4); + TEST_ASSERT(expectedRGB32 == 1228800, "RGB32 expected size 640x480 = 1228800"); + + // Truncated buffer should be rejected + size_t truncated = expectedYUV422 - 100; + TEST_ASSERT(truncated < expectedYUV422, "Truncated buffer < expected size"); +} + + +// ============================================================================ +// Test: CSV/JSON export format +// ============================================================================ + +static void +TestExportFormats() +{ + TEST_SECTION("Export Format Validation"); + + // Test CSV escaping + BString field1("simple"); + TEST_ASSERT(field1.FindFirst(',') < 0, "Simple CSV field has no commas"); + + BString field2("has,comma"); + TEST_ASSERT(field2.FindFirst(',') >= 0, "Comma field detected"); + + BString field3("has\"quote"); + TEST_ASSERT(field3.FindFirst('"') >= 0, "Quote field detected"); + + // Test JSON escaping + BString json("line1\nline2"); + TEST_ASSERT(json.FindFirst('\n') >= 0, "Newline in JSON needs escaping"); + + BString jsonQuote("say \"hello\""); + TEST_ASSERT(jsonQuote.FindFirst('"') >= 0, "Quote in JSON needs escaping"); + + // Test timestamp format (should be YYYYMMDD_HHMMSS) + time_t now = time(NULL); + TEST_ASSERT(now > 0, "time() returns positive value"); +} + + +// ============================================================================ +// Test: BBitmap allocation +// ============================================================================ + +static void +TestBitmapAllocation() +{ + TEST_SECTION("BBitmap Allocation"); + + // Common webcam resolutions + struct { int32 w; int32 h; const char* name; } resolutions[] = { + { 160, 120, "QQVGA" }, + { 320, 240, "QVGA" }, + { 640, 480, "VGA" }, + { 1280, 720, "720p" }, + { 1920, 1080, "1080p" }, + }; + + for (size_t i = 0; i < sizeof(resolutions)/sizeof(resolutions[0]); i++) { + BRect rect(0, 0, resolutions[i].w - 1, resolutions[i].h - 1); + BBitmap* bmp = new BBitmap(rect, B_RGB32); + + char msg[128]; + snprintf(msg, sizeof(msg), "BBitmap %s (%dx%d) allocation succeeds", + resolutions[i].name, (int)resolutions[i].w, (int)resolutions[i].h); + TEST_ASSERT(bmp != NULL && bmp->IsValid(), msg); + + snprintf(msg, sizeof(msg), "BBitmap %s has correct dimensions", + resolutions[i].name); + TEST_ASSERT(bmp->Bounds().Width() == resolutions[i].w - 1 && + bmp->Bounds().Height() == resolutions[i].h - 1, msg); + + snprintf(msg, sizeof(msg), "BBitmap %s BytesPerRow >= width*4", + resolutions[i].name); + TEST_ASSERT(bmp->BytesPerRow() >= resolutions[i].w * 4, msg); + + delete bmp; + } +} + + +// ============================================================================ +// Benchmark: YUV422 conversion (Scalar vs SSE2) +// ============================================================================ + +static void +BenchmarkYUV422() +{ + TEST_SECTION("Benchmark: YUV422 Conversion"); + + struct { int32 w; int32 h; const char* name; int32 iters; } sizes[] = { + { 320, 240, "QVGA 320x240", 500 }, + { 640, 480, "VGA 640x480", 200 }, + { 1280, 720, "720p 1280x720", 50 }, + { 1920, 1080, "1080p 1920x1080", 20 }, + }; + + for (size_t s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++) { + int32 w = sizes[s].w, h = sizes[s].h; + int32 srcBPR = w * 2; + int32 dstBPR = w * 4; + int32 iters = sizes[s].iters; + + uint8* src = new uint8[srcBPR * h]; + uint8* dst = new uint8[dstBPR * h]; + + // Fill with random-ish data + for (int32 i = 0; i < srcBPR * h; i++) + src[i] = (uint8)(i * 7 + 13); + + char label[128]; + + // Scalar benchmark + snprintf(label, sizeof(label), "Scalar %s", sizes[s].name); + BENCH_START(label, iters) + for (int32 i = 0; i < iters; i++) + ConvertYUV422_Scalar(src, dst, w, h, srcBPR, dstBPR); + BENCH_END() + +#ifdef __SSE2__ + // SSE2 benchmark + snprintf(label, sizeof(label), "SSE2 %s", sizes[s].name); + BENCH_START(label, iters) + for (int32 i = 0; i < iters; i++) + ConvertYUV422_SSE2(src, dst, w, h, srcBPR, dstBPR); + BENCH_END() +#endif + + delete[] src; + delete[] dst; + } +} + + +// ============================================================================ +// Benchmark: NV12 conversion +// ============================================================================ + +static void +BenchmarkNV12() +{ + TEST_SECTION("Benchmark: NV12 Conversion"); + + struct { int32 w; int32 h; const char* name; int32 iters; } sizes[] = { + { 640, 480, "VGA 640x480", 200 }, + { 1280, 720, "720p 1280x720", 50 }, + { 1920, 1080, "1080p 1920x1080", 20 }, + }; + + for (size_t s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++) { + int32 w = sizes[s].w, h = sizes[s].h; + int32 dstBPR = w * 4; + size_t srcSize = w * h * 3 / 2; + int32 iters = sizes[s].iters; + + uint8* src = new uint8[srcSize]; + uint8* dst = new uint8[dstBPR * h]; + + for (size_t i = 0; i < srcSize; i++) + src[i] = (uint8)(i * 11 + 3); + + char label[128]; + snprintf(label, sizeof(label), "NV12 %s", sizes[s].name); + + BENCH_START(label, iters) + for (int32 i = 0; i < iters; i++) + ConvertNV12_Scalar(src, dst, w, h, dstBPR); + BENCH_END() + + delete[] src; + delete[] dst; + } +} + + +// ============================================================================ +// Benchmark: BBitmap memcpy (simulates direct RGB32 copy) +// ============================================================================ + +static void +BenchmarkMemcpy() +{ + TEST_SECTION("Benchmark: Frame Copy (memcpy)"); + + struct { int32 w; int32 h; const char* name; int32 iters; } sizes[] = { + { 640, 480, "VGA 640x480", 500 }, + { 1280, 720, "720p 1280x720", 100 }, + { 1920, 1080, "1080p 1920x1080", 30 }, + }; + + for (size_t s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++) { + int32 w = sizes[s].w, h = sizes[s].h; + size_t frameSize = w * h * 4; + int32 iters = sizes[s].iters; + + uint8* src = (uint8*)malloc(frameSize); + uint8* dst = (uint8*)malloc(frameSize); + memset(src, 0x42, frameSize); + + char label[128]; + snprintf(label, sizeof(label), "memcpy %s (%zu KB)", sizes[s].name, + frameSize / 1024); + + BENCH_START(label, iters) + for (int32 i = 0; i < iters; i++) + memcpy(dst, src, frameSize); + BENCH_END() + + free(src); + free(dst); + } +} + + +// ============================================================================ +// Main +// ============================================================================ + +int +main(int argc, char* argv[]) +{ + BApplication app("application/x-vnd.BubiCam-Tests"); + + fprintf(stderr, "========================================\n"); + fprintf(stderr, "BubiCam Test Suite & Benchmarks\n"); + fprintf(stderr, "========================================\n"); + + bool runBench = true; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--no-bench") == 0) + runBench = false; + } + + // --- Tests --- + TestYUV422Conversion(); + TestNV12Conversion(); + TestBufferValidation(); + TestExportFormats(); + TestBitmapAllocation(); + + // --- Benchmarks --- + if (runBench) { + fprintf(stderr, "\n========================================\n"); + fprintf(stderr, "Benchmarks\n"); + fprintf(stderr, "========================================\n"); + + BenchmarkYUV422(); + BenchmarkNV12(); + BenchmarkMemcpy(); + } + + // --- Summary --- + fprintf(stderr, "\n========================================\n"); + fprintf(stderr, "Results: %d passed, %d failed, %d total\n", + sTestsPassed, sTestsFailed, sTestsTotal); + fprintf(stderr, "========================================\n"); + + return sTestsFailed > 0 ? 1 : 0; +}