Video Capture APIs
« | 24 Oct 2020 | »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:
- capGetDriverDescription nutzt man um per hochzählenden Index die verfügbaren Treiber auflisten zu können.
- capCreateCaptureWindow erzeugt ein Fenster, das Nachrichten vom Video-Treiber empfängt bzw. mit ihm kommuniziert.
- WM_CAP_DRIVER_CONNECT verbindet das Fenster mit einem Video Treiber.
- WM_CAP_SET_CALLBACK_* setzt Callback Funktionen für Fehler, Frames und Audio Samples, wo man Daten vom Treiber zugesendet bekommt.
- WM_CAP_GET_SEQUENCE_SETUP und WM_CAP_SET_SEQUENCE_SETUP lesen Einstellungen aus und legen sie für eine Videositzung fest.
- WM_CAP_SEQUENCE_NOFILE startet eine Videoaufnahme, die nur Callbacks für jedes Frame aufruft.
- 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.)
- 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:
- Man listet Geräte
über die Klasse
CLSID_SystemDeviceEnum
auf und sucht dort nachCLSID_VideoInputDeviceCategory
Einträgen - Mit der IMoniker Schnittstelle kann man sich zum IPropertyBag verbinden um dort den Gerätepfad und den Namen eines Videogeräts auszulesen.
- 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. - Man erzeugt eine Instanz der Klasse FilterGraph, welches das IGraphBuilder Interface bereitstellt.
- An
IGraphbuilder
kann man nun perAddFilter
denIBaseFilter
der Quelle anfügen. - 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. - Der
SampleGrabber
(oder eine Alternative) wird ebenso dem IGraphBuilder angefügt. - Jeder
IBaseFilter
beinhaltet eine Sammlung von IPin Instanzen, die als Input oder Output definiert sind und mit diversen Eigenschaften versehen sind. - Der Output
IPin
des Quell-IBaseFilters lässt alle Formate auflisten, die die Quelle unterstützt. - Am
SampleGrabber
kann man das gewünschte Zielformat angeben und eine Callback Funktion angeben, die bei jedem empfangenen (und konvertierten) Frame aufgerufen wird. - Nun wird am Output
IPin
der Quelle das gewünschte Format gesetzt und dieser dann mit dem InputIPin
des SampleGrabbers über IGraphBuilder->Connect verbunden. - Über das IMediaControl
Interface der
FilterGraph
Instanz kann man nun perRun
die Aufnahme der Videodaten starten. - 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.
- IMediaControl::Stop() beendet den Aufnahmestream.
- Am Ende werden alle Filter aus dem
IGraphBuilder
perRemoveFilter()
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.
- Man öffnet ein Videogerät wie eine Datei (meist:
/dev/video*
) und nutzt VIDIOC_QUERYCAP um den richtigen Gerätenamen herauszufinden. - Über die IOCTLs VIDIOC_G_FMT und VIDIOC_ENUM_FMT lassen sich Details über unterstützte Formate und Auflösungen herausfinden.
- Per capabilities von
VIDIOC_QUERYCAP
lässt sich feststellen, welche Aufnahmemöglichkeiten der Treiber bietet. - 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. - Schlägt die mmap-Streaming-Registrierung fehl, kann man normale
malloc() Puffer als
V4L2_MEMORY_USERPTR
per
VIDIOC_QBUF
registrieren. - Per VIDIOC_STREAMON wird der MMAP oder USERPTR Stream aktiviert.
- 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. - In einer Schleife kann man nun per select() auf das geöffnete Videogerät warten, bis es Daten bereit hält.
- 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. - Ohne Streaming wird einfach read() aufgerufen um das aktuelle Frame zu empfangen.
- 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. - 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.