Video Capture APIs

Video for Windows VfW, Direct-Show dshow und Video for Linux v4l - das sind die drei Schnittstellen, mit denen ich über die Jahre hinweg immer wieder mal Webcam Bilder mitgeschnitten habe.

Nun wird es also mal Zeit diese drei zu dokumentieren und das Modul gate/io/videosources.h hat diese Aufgabe nun übernommen.


Video for Windows

Diese älteste Schnittstelle von Windows stammt noch aus den Windows 3.x Tagen und ist offiziell schon seit Jahrzehnten tot. Und dennoch wurde der Sourcecode bis zur aktuellen Windows 10 Release nicht entfernt, womit das alte Teil in vielen Fällen immer noch brav arbeitet.
Man erzeugt ein spezielles Fenster und den Rest erledigt man über Nachrichten um eine Stream-Aufnahme zu starten oder zu stoppen. Die Bilder trudeln dann in zuvor registrierten Callback-Funktionen herein. Alle Funktionen müssen in einem einzigen Thread ausgeführt werden, in dem eine Fensternachrichtenschleife läuft:

  1. capGetDriverDescription nutzt man um per hochzählenden Index die verfügbaren Treiber auflisten zu können.
  2. capCreateCaptureWindow erzeugt ein Fenster, das Nachrichten vom Video-Treiber empfängt bzw. mit ihm kommuniziert.
  3. WM_CAP_DRIVER_CONNECT verbindet das Fenster mit einem Video Treiber.
  4. WM_CAP_SET_CALLBACK_* setzt Callback Funktionen für Fehler, Frames und Audio Samples, wo man Daten vom Treiber zugesendet bekommt.
  5. WM_CAP_GET_SEQUENCE_SETUP und WM_CAP_SET_SEQUENCE_SETUP lesen Einstellungen aus und legen sie für eine Videositzung fest.
  6. WM_CAP_SEQUENCE_NOFILE startet eine Videoaufnahme, die nur Callbacks für jedes Frame aufruft.
  7. Die DrawDib* Funktionen sollten genutzt werden um die nativ kodierten Framedaten auf typischen Windows Bitmaps zu zeichnen. Vor dort aus kann man die Rastergrafiken ganz einfach weiterverarbeiten. Nutzt man diese Funktionen nicht, muss man sich mit den Pixeldatenformaten beschäftigen, die direkt vom Video-Treiber stammen (z.B. YUV2 usw.)
  8. WM_CAP_DRIVER_DISCONNECT schließt die Verbindung des Fensters zum Video-Treiber.

Die meisten Window-Messages haben auch Funktions-Makros in den Headern, die alle mit cap starten.

Direct-Show

Der Nachfolger von VfW ist vollständig von der UI unabhängig und setzt auf COM auf. Man hat die Idee einer “Verarbeitungskette” aufkommen lassen, in der Sample-Quellen ihre Daten an eine Reihe von Filtern weitergeben. Die Filter können die Daten manipulieren und wieder weiterreichen, oder aber auch irgend wo ausgeben.
Eine Webcam ist also eine solche Quelle und diese wird entweder mit einem Fenster-Renderer verknüpft, der die Bilder auf den Schirm malt, oder die Daten werden abgefangen und z.B. per Netzwerk weggeschickt.

Die APIs hierfür sehen so aus:

  1. Man listet Geräte über die Klasse CLSID_SystemDeviceEnum auf und sucht dort nach CLSID_VideoInputDeviceCategory Einträgen
  2. Mit der IMoniker Schnittstelle kann man sich zum IPropertyBag verbinden um dort den Gerätepfad und den Namen eines Videogeräts auszulesen.
  3. Hat man das gewünschte Gerät gefunden, kann man sich per BindToObject zur Schnittstelle IBaseFilter verbinden, die dann als Quelle für Videodaten fungieren wird.
  4. Man erzeugt eine Instanz der Klasse FilterGraph, welches das IGraphBuilder Interface bereitstellt.
  5. An IGraphbuilder kann man nun per AddFilter den IBaseFilter der Quelle anfügen.
  6. Nun erzeugt man eine Instanz von CLSID_SampleGrabber, der Videoframes empfangen und für uns exportieren soll.
    Alternativ kann man hier auch einen eigenen Filter implementieren, der ebenso empfangene Daten zu uns weiterleitet.
  7. Der SampleGrabber (oder eine Alternative) wird ebenso dem IGraphBuilder angefügt.
  8. Jeder IBaseFilter beinhaltet eine Sammlung von IPin Instanzen, die als Input oder Output definiert sind und mit diversen Eigenschaften versehen sind.
  9. Der Output IPin des Quell-IBaseFilters lässt alle Formate auflisten, die die Quelle unterstützt.
  10. Am SampleGrabber kann man das gewünschte Zielformat angeben und eine Callback Funktion angeben, die bei jedem empfangenen (und konvertierten) Frame aufgerufen wird.
  11. Nun wird am Output IPin der Quelle das gewünschte Format gesetzt und dieser dann mit dem Input IPin des SampleGrabbers über IGraphBuilder->Connect verbunden.
  12. Über das IMediaControl Interface der FilterGraph Instanz kann man nun per Run die Aufnahme der Videodaten starten.
  13. Die empfangenen Daten in der Callbackfunktion sollten nur in einen anderen Puffer kopiert werden. Da diese Routine im Kontext des Treibers laufen kann, sind nur wenige APIs erlaubt von hier aus aufzurufen. Nach dem Kopieren kann man per SetEvent() einen anderen Thread benachrichtigen, der die Daten weiterverarbeitet.
  14. IMediaControl::Stop() beendet den Aufnahmestream.
  15. Am Ende werden alle Filter aus dem IGraphBuilder per RemoveFilter() entfernt und die Objekte danach freigegeben.

Video 4 Linux

Obwohl es an VfW erinnert hat diese Linux Video-Schnittstelle überhaupt nichts mit der antiken Windows-Implementierung gemeinsam. Es handelt sich dabei lediglich um ein Set von C Strukturen und IOCTL Codes, die man an ein Videogerät schicken kann, welches dann über standardisierte Datenpuffer Bilddaten zurückgibt.
Wenn man “einfach” nur ein paar Bilder speichern möchte, ist diese Schnittstelle die effizienteste von den dreien meiner Meinung nach.

  1. Man öffnet ein Videogerät wie eine Datei (meist: /dev/video*) und nutzt VIDIOC_QUERYCAP um den richtigen Gerätenamen herauszufinden.
  2. Über die IOCTLs VIDIOC_G_FMT und VIDIOC_ENUM_FMT lassen sich Details über unterstützte Formate und Auflösungen herausfinden.
  3. Per capabilities von VIDIOC_QUERYCAP lässt sich feststellen, welche Aufnahmemöglichkeiten der Treiber bietet.
  4. Wird V4L2_CAP_STREAMING unterstützt, sollten ein paar V4L2_MEMORY_MMAP Puffer mit mmap angelegt werden und per VIDIOC_REQBUFS, VIDIOC_QUERYBUF und VIDIOC_QBUF in der Queue des Gerätes registriert werden.
  5. Schlägt die mmap-Streaming-Registrierung fehl, kann man normale malloc() Puffer als V4L2_MEMORY_USERPTR per VIDIOC_QBUF registrieren.
  6. Per VIDIOC_STREAMON wird der MMAP oder USERPTR Stream aktiviert.
  7. Ist Streaming generell nicht verfügbar, kann man noch auf V4L2_CAP_READWRITE prüfen. Hierbei wird dann nicht gepuffert sondern per “read” ein Frame direkt in einen lokalen Puffer kopiert.
    Diese Variante sollte immer funktionieren, ist aber am ineffizientesten.
  8. In einer Schleife kann man nun per select() auf das geöffnete Videogerät warten, bis es Daten bereit hält.
  9. Im Fall von Streaming nutzt man VIDIOC_DQBUF um einen vollen Puffer aus dem Gerät herauszulösen. Dann bearbeitet man die Daten und fügt den Puffer per VIDIOC_QBUF wieder dem Gerät hinzu, damit dieser neu befüllt werden kann.
  10. Ohne Streaming wird einfach read() aufgerufen um das aktuelle Frame zu empfangen.
  11. Um die native kodierten Videodaten besser bearbeiten zu können, empfiehlt sich der Einsatz der Bibliothek v4l_convert. Sie kann eine Vielzahl an Bildformaten nach RGB oder andere Standards übersetzen.
  12. Beenden kann man das Streaming per VIDIOC_STREAMOFF. Danach kann der Gerätedatei-Deskriptor mit close() freigegeben werden.

Fazit

Das größte Manko der Windows Schnittstellen ist, dass in den Callbacks nur einige wenige WinAPIs benutzt werden dürfen. Und das sind Mutexe und Speicherallokierungen. Man darf also nur die Daten wo anders hinkopieren, weil der Callback mehr oder weniger direkt aus dem Treiber heraus aufgerufen wird.

Das macht die Programmierung unter Windows mühsam und fehleranfällig. Mit der Media Foundation seit Windows Vista gibt es eine neuere Schnittstelle und die Windows Runtime ist offenbar nochmals ein bisschen mehr “geil” laut MSDN, aber bisher bin ich bei meinen alten Bekannten geblieben.

Linux geht hier seinen üblichen Weg und baut anfangs nur auf klassische open() - ioctl() - read() - write() - close() Abfolgen auf. Doch wenn man alle Farbkodierungen unterstützen möchte, empfiehlt sich der Einsatz von Hilfsroutinen.

Wenn man von diversen aufwendigeren Workarounds aber mal absieht, so kann man hier recht einfach den “kleinsten gemeinsamen Nenner” zwischen den APIs finden und einen einheitlichen Wrapper darüberlegen.
… und genau das versucht ja das GATE Projekt zu erreichen.