Hier erfahren Sie, wie gPhoto2 auf WebAssembly umgestellt wurde, um externe Kameras über USB über eine Webanwendung zu steuern.
Im vorherigen Beitrag habe ich gezeigt, wie die libusb-Bibliothek für die Ausführung im Web mit WebAssembly/Emscripten, Asyncify und WebUSB portiert wurde.
Außerdem habe ich eine Demo mit gPhoto2 gezeigt, mit der DSLR- und spiegellose Kameras über USB über eine Webanwendung gesteuert werden können. In diesem Beitrag gehe ich näher auf die technischen Details des gPhoto2-Ports ein.
Build-Systeme auf benutzerdefinierte Forks verweisen
Da ich auf WebAssembly ausgerichtet war, konnte ich die von den Systemdistributionen bereitgestellten libusb und libgphoto2 nicht verwenden. Stattdessen musste meine Anwendung meine benutzerdefinierte Fork von libgphoto2 verwenden, während diese Fork von libgphoto2 meine benutzerdefinierte Fork von libusb verwenden musste.
Außerdem verwendet libgphoto2 libtool zum Laden dynamischer Plug-ins. Auch wenn ich libtool nicht wie die anderen beiden Bibliotheken forken musste, musste ich es trotzdem in WebAssembly kompilieren und libgphoto2 auf diesen benutzerdefinierten Build statt auf das Systempaket verweisen.
Hier ist ein Diagramm zu einer Abhängigkeit (gestrichelte Linien kennzeichnen dynamische Verlinkungen):
Die meisten konfigurationsbasierten Build-Systeme, einschließlich der in diesen Bibliotheken verwendeten, ermöglichen das Überschreiben von Pfaden für Abhängigkeiten über verschiedene Flags. Das habe ich zuerst versucht. Wenn das Abhängigkeitsdiagramm jedoch komplex wird, wird die Liste der Pfadüberschreibungen für die Abhängigkeiten jeder Bibliothek ausführlich und fehleranfällig. Außerdem habe ich einige Fehler gefunden, bei denen die Build-Systeme nicht darauf vorbereitet waren, dass ihre Abhängigkeiten sich in nicht standardmäßigen Pfaden befinden.
Stattdessen ist es einfacher, einen separaten Ordner als benutzerdefinierten Systemstamm (oft zu „sysroot“ verkürzt) zu erstellen und alle beteiligten Build-Systeme darauf zu verweisen. Auf diese Weise sucht jede Bibliothek während des Build-Prozesses im angegebenen Sysroot nach ihren Abhängigkeiten und installiert sich selbst im selben Sysroot, damit andere sie leichter finden können.
Emscripten hat bereits ein eigenes sysroot unter (path to emscripten cache)/sysroot
, das für seine Systembibliotheken, Emscripten-Ports und Tools wie CMake und pkg-config verwendet wird. Ich habe auch dasselbe sysroot für meine Abhängigkeiten wiederverwendet.
# This is the default path, but you can override it
# to store the cache elsewhere if you want.
#
# For example, it might be useful for Docker builds
# if you want to preserve the deps between reruns.
EM_CACHE = $(EMSCRIPTEN)/cache
# Sysroot is always under the `sysroot` subfolder.
SYSROOT = $(EM_CACHE)/sysroot
# …
# For all dependencies I've used the same ./configure command with the
# earlier defined SYSROOT path as the --prefix.
deps/%/Makefile: deps/%/configure
cd $(@D) && ./configure --prefix=$(SYSROOT) # …
Bei dieser Konfiguration musste ich nur make install
in jeder Abhängigkeit ausführen, wodurch sie unter dem sysroot installiert wurde. Die Bibliotheken fanden sich dann automatisch.
Dynamisches Laden
Wie bereits erwähnt, verwendet libgphoto2 libtool, um I/O-Port-Adapter und Kamerabibliotheken zu zählen und dynamisch zu laden. Der Code zum Laden von E/A-Bibliotheken sieht beispielsweise so aus:
lt_dlinit ();
lt_dladdsearchdir (iolibs);
result = lt_dlforeachfile (iolibs, foreach_func, list);
lt_dlexit ();
Bei diesem Ansatz gibt es jedoch einige Probleme im Web:
- Es gibt keine Standardunterstützung für die dynamische Verknüpfung von WebAssembly-Modulen. Emscripten hat eine benutzerdefinierte Implementierung, mit der die von libtool verwendete
dlopen()
API simuliert werden kann. Sie müssen jedoch „Haupt“- und „Neben“-Module mit unterschiedlichen Flags erstellen und speziell fürdlopen()
auch die Nebenmodule beim Starten der Anwendung in das emulierte Dateisystem vorladen. Es kann schwierig sein, diese Flags und Optimierungen in ein vorhandenes Autoconf-Build-System mit vielen dynamischen Bibliotheken zu integrieren. - Selbst wenn
dlopen()
selbst implementiert ist, gibt es keine Möglichkeit, alle dynamischen Bibliotheken in einem bestimmten Ordner im Web aufzulisten, da die meisten HTTP-Server aus Sicherheitsgründen keine Verzeichnislisten bereitstellen. - Das Verknüpfen dynamischer Bibliotheken über die Befehlszeile anstelle der Aufzählung zur Laufzeit kann ebenfalls zu Problemen führen, z. B. zum Problem mit doppelten Symbolen, das durch Unterschiede zwischen der Darstellung freigegebener Bibliotheken in Emscripten und auf anderen Plattformen verursacht wird.
Es ist möglich, das Build-System an diese Unterschiede anzupassen und die Liste der dynamischen Plug-ins irgendwo während des Builds hartzucodieren. Aber noch einfacher können Sie all diese Probleme lösen, indem Sie zunächst keine dynamischen Links erstellen.
Wie sich herausstellte, abstrahiert libtool verschiedene Methoden der dynamischen Verknüpfung auf verschiedenen Plattformen und unterstützt sogar das Schreiben benutzerdefinierter Lader für andere. Einer der unterstützten integrierten Lader heißt „Dlpreopening“:
„Libtool unterstützt das dlopening-Objekt libtool und die libtool-Bibliotheksdateien, sodass ihre Symbole auch auf Plattformen ohne dlopen- und dlsym-Funktionen aufgelöst werden können.
…
Libtool emuliert -dlopen auf statischen Plattformen, indem es Objekte zur Laufzeit in das Programm linkt und Datenstrukturen erstellt, die die Symboltabelle des Programms darstellen. Wenn Sie diese Funktion verwenden möchten, müssen Sie die Objekte deklarieren, die Ihre Anwendung dlopen soll. Verwenden Sie dazu beim Verknüpfen Ihres Programms die Flags „-dlopen“ oder „-dlpreopen“ (siehe Link-Modus).“
Mit diesem Mechanismus kann das dynamische Laden auf libtool-Ebene statt auf Emscripten-Ebene emuliert werden, während alles statisch in einer einzigen Bibliothek verknüpft wird.
Das einzige Problem, das dadurch nicht gelöst wird, ist die Aufzählung dynamischer Bibliotheken. Die Liste dieser Elemente muss noch irgendwo hartcodiert werden. Zum Glück benötigen wir nur ein Minimum an Plug-ins für die App:
- Bei den Ports geht es mir nur um die libusb-basierte Kameraverbindung und nicht um PTP/IP, den seriellen Zugriff oder USB-Laufwerkmodi.
- Für die Camlibs gibt es verschiedene anbieterspezifische Plug-ins, die einige spezielle Funktionen bieten. Für die allgemeine Einstellung und Aufnahme reicht es jedoch aus, das Picture Transfer Protocol zu verwenden, das durch die Camlib „ptp2“ dargestellt wird und von praktisch jeder Kamera auf dem Markt unterstützt wird.
So sieht das aktualisierte Abhängigkeitsdiagramm aus, in dem alle Elemente statisch miteinander verknüpft sind:
Das habe ich also für Emscripten-Builds hartcodiert:
LTDL_SET_PRELOADED_SYMBOLS();
lt_dlinit ();
#ifdef __EMSCRIPTEN__
result = foreach_func("libusb1", list);
#else
lt_dladdsearchdir (iolibs);
result = lt_dlforeachfile (iolibs, foreach_func, list);
#endif
lt_dlexit ();
und
LTDL_SET_PRELOADED_SYMBOLS();
lt_dlinit ();
#ifdef __EMSCRIPTEN__
ret = foreach_func("libptp2", &foreach_data);
#else
lt_dladdsearchdir (dir);
ret = lt_dlforeachfile (dir, foreach_func, &foreach_data);
#endif
lt_dlexit ();
Im autoconf-Buildsystem musste ich -dlpreopen
mit beiden Dateien als Link-Flags für alle ausführbaren Dateien (Beispiele, Tests und meine eigene Demo-App) hinzufügen, z. B. so:
if HAVE_EMSCRIPTEN
LDADD += -dlpreopen $(top_builddir)/libgphoto2_port/usb1.la \
-dlpreopen $(top_builddir)/camlibs/ptp2.la
endif
Nachdem alle Symbole jetzt statisch in einer einzigen Bibliothek verknüpft sind, muss libtool ermitteln können, welches Symbol zu welcher Bibliothek gehört. Dazu müssen Entwickler alle freigegebenen Symbole wie {function name}
in {library name}_LTX_{function name}
umbenennen. Am einfachsten geht das mit #define
, um Symbolnamen oben in der Implementierungsdatei neu zu definieren:
// …
#include "config.h"
/* Define _LTX_ names - required to prevent clashes when using libtool preloading. */
#define gp_port_library_type libusb1_LTX_gp_port_library_type
#define gp_port_library_list libusb1_LTX_gp_port_library_list
#define gp_port_library_operations libusb1_LTX_gp_port_library_operations
#include <gphoto2/gphoto2-port-library.h>
// …
Dieses Benennungsschema verhindert auch Namenskonflikte, falls ich in Zukunft kameraspezifische Plug-ins in derselben App verknüpfen möchte.
Nachdem ich alle diese Änderungen implementiert hatte, konnte ich die Testanwendung erstellen und die Plug-ins erfolgreich laden.
Benutzeroberfläche für Einstellungen generieren
Mit gPhoto2 können Kamerabibliotheken ihre eigenen Einstellungen in Form eines Widget-Baums definieren. Die Hierarchie der Widgettypen besteht aus:
- Fenster – Konfigurationscontainer der obersten Ebene
- Abschnitte: benannte Gruppen anderer Widgets
- Schaltflächenfelder
- Textfelder
- Numerische Felder
- Datumsfelder
- Toggles
- Optionsfelder
Der Name, der Typ, die untergeordneten Elemente und alle anderen relevanten Eigenschaften der einzelnen Widgets können über die externe C API abgefragt und im Falle von Werten auch geändert werden. Zusammen bilden sie die Grundlage für die automatische Generierung der Benutzeroberfläche für Einstellungen in jeder Sprache, die mit C interagieren kann.
Die Einstellungen können jederzeit entweder über gPhoto2 oder in der Kamera selbst geändert werden. Außerdem können einige Widgets schreibgeschützt sein. Selbst der schreibgeschützte Status hängt vom Kameramodus und anderen Einstellungen ab. Beispielsweise ist der Auslösergeschwindigkeit in M (manueller Modus) ein beschreibbares numerisches Feld, wird aber in P (Programmmodus) zu einem schreibgeschützten Feld, das nur zu Informationszwecken dient. Im P-Modus ist auch der Wert der Belichtungszeit dynamisch und ändert sich abhängig von der Helligkeit des von der Kamera erfassten Bereichs kontinuierlich.
Insgesamt ist es wichtig, dass die Informationen der verbundenen Kamera immer auf dem neuesten Stand sind und gleichzeitig dem Nutzer die Möglichkeit gegeben wird, diese Einstellungen über dieselbe Benutzeroberfläche zu bearbeiten. Die Verarbeitung eines solchen bidirektionalen Datenflusses ist komplexer.
gPhoto2 bietet keinen Mechanismus, um nur geänderte Einstellungen abzurufen, sondern nur den gesamten Baum oder einzelne Widgets. Um die Benutzeroberfläche auf dem neuesten Stand zu halten, ohne zu flackern und den Eingabefokus oder die Scrollposition zu verlieren, brauchte ich eine Möglichkeit, die Widget-Bäume zwischen den Aufrufen zu vergleichen und nur die geänderten UI-Eigenschaften zu aktualisieren. Glücklicherweise ist dieses Problem im Web gelöst und bildet die Hauptfunktion von Frameworks wie React oder Preact. Ich habe für dieses Projekt Preact verwendet, da es viel schlanker ist und alles bietet, was ich benötige.
Auf der C++-Seite musste ich nun den Einstellungsbaum über die zuvor verknüpfte C-API abrufen und rekursiv durchlaufen und jedes Widget in ein JavaScript-Objekt konvertieren:
static std::pair<val, val> walk_config(CameraWidget *widget) {
val result = val::object();
val name(GPP_CALL(const char *, gp_widget_get_name(widget, _)));
result.set("name", name);
result.set("info", /* … */);
result.set("label", /* … */);
result.set("readonly", /* … */);
auto type = GPP_CALL(CameraWidgetType, gp_widget_get_type(widget, _));
switch (type) {
case GP_WIDGET_RANGE: {
result.set("type", "range");
result.set("value", GPP_CALL(float, gp_widget_get_value(widget, _)));
float min, max, step;
gpp_try(gp_widget_get_range(widget, &min, &max, &step));
result.set("min", min);
result.set("max", max);
result.set("step", step);
break;
}
case GP_WIDGET_TEXT: {
result.set("type", "text");
result.set("value",
GPP_CALL(const char *, gp_widget_get_value(widget, _)));
break;
}
// …
Auf der JavaScript-Seite könnte ich jetzt configToJS
aufrufen, die zurückgegebene JavaScript-Darstellung des Einstellungsbaums durchgehen und die Benutzeroberfläche über die Preact-Funktion h
erstellen:
let inputElem;
switch (config.type) {
case 'range': {
let { min, max, step } = config;
inputElem = h(EditableInput, {
type: 'number',
min,
max,
step,
…attrs
});
break;
}
case 'text':
inputElem = h(EditableInput, attrs);
break;
case 'toggle': {
inputElem = h('input', {
type: 'checkbox',
…attrs
});
break;
}
// …
Durch wiederholtes Ausführen dieser Funktion in einem unendlichen Ereignis-Loop konnte ich dafür sorgen, dass die Benutzeroberfläche der Einstellungen immer die neuesten Informationen enthält. Außerdem wurden Befehle an die Kamera gesendet, wenn eines der Felder vom Nutzer bearbeitet wurde.
Preact kann die Ergebnisse vergleichen und das DOM nur für die geänderten Teile der Benutzeroberfläche aktualisieren, ohne den Fokus der Seite oder die Bearbeitungsstatus zu stören. Ein Problem bleibt jedoch: der bidirektionale Datenfluss. Frameworks wie React und Preact wurden für den unidirektionalen Datenfluss entwickelt, da es viel einfacher ist, Daten zu analysieren und zwischen Wiederholungen zu vergleichen. Ich übertrete jedoch diese Erwartung, indem ich einer externen Quelle – der Kamera – erlaube, die Benutzeroberfläche für Einstellungen jederzeit zu aktualisieren.
Ich habe die Aktualisierungen der Benutzeroberfläche für alle Eingabefelder deaktiviert, die derzeit vom Nutzer bearbeitet werden, um dieses Problem zu umgehen:
/**
* Wrapper around <input /> that doesn't update it while it's in focus to allow editing.
*/
class EditableInput extends Component {
ref = createRef();
shouldComponentUpdate() {
return this.props.readonly || document.activeElement !== this.ref.current;
}
render(props) {
return h('input', Object.assign(props, {ref: this.ref}));
}
}
So gibt es für jedes Feld immer nur einen Inhaber. Entweder bearbeitet der Nutzer das Feld gerade und wird nicht durch die aktualisierten Werte der Kamera gestört, oder die Kamera aktualisiert den Feldwert, während das Bild unscharf ist.
Live-Videofeed erstellen
Während der Pandemie haben viele Menschen auf Online-Meetings umgestellt. Dies führte unter anderem zu Knappheiten auf dem Webcam-Markt. Aufgrund der besseren Videoqualität im Vergleich zu den integrierten Kameras in Laptops und aufgrund der genannten Engpässe haben viele Besitzer von DSLR- und spiegellosen Kameras nach Möglichkeiten gesucht, ihre Fotokameras als Webcam zu verwenden. Einige Kameraanbieter haben sogar offizielle Dienstprogramme für diesen Zweck versendet.
Wie die offiziellen Tools unterstützt gPhoto2 das Streamen von Videos von der Kamera in eine lokal gespeicherte Datei oder direkt an eine virtuelle Webcam. Ich wollte diese Funktion nutzen, um in meiner Demo eine Liveansicht zu zeigen. Obwohl es im Dienstprogramm der Console verfügbar ist, konnte ich es nirgendwo in den libgphoto2-Bibliotheks-APIs finden.
Im Quellcode der entsprechenden Funktion im Konsolen-Dienstprogramm habe ich festgestellt, dass eigentlich gar kein Video abgerufen wird. Stattdessen abruft sie die Kameravorschau weiter als einzelne JPEG-Bilder in einer Endlosschleife und schreibt sie nacheinander aus, um einen M-JPEG-Stream zu bilden:
while (1) {
const char *mime;
r = gp_camera_capture_preview (p->camera, file, p->context);
// …
Ich war erstaunt, dass dieser Ansatz so effizient funktioniert, dass ich den Eindruck von flüssigen Echtzeitvideos bekommt. Noch skeptischer war ich, dass ich auch in der Webanwendung mit allen zusätzlichen Abstraktionen und der Asynchronität dieselbe Leistung erzielen konnte. Ich beschloss jedoch, es trotzdem zu versuchen.
In C++ habe ich eine Methode namens capturePreviewAsBlob()
verfügbar gemacht, die dieselbe gp_camera_capture_preview()
-Funktion aufruft und die resultierende speicherinterne Datei in eine Blob
konvertiert, die einfacher an andere Web-APIs übergeben werden kann:
val capturePreviewAsBlob() {
return gpp_rethrow([=]() {
auto &file = get_file();
gpp_try(gp_camera_capture_preview(camera.get(), &file, context.get()));
auto params = blob_chunks_and_opts(file);
return Blob.new_(std::move(params.first), std::move(params.second));
});
}
Auf der JavaScript-Seite habe ich eine Schleife, ähnlich der in gPhoto2, die Vorschaubilder immer wieder als Blob
s abruft, sie im Hintergrund mit createImageBitmap
decodiert und sie im nächsten Animationsframe in den Canvas überträgt:
while (this.canvasRef.current) {
try {
let blob = await this.props.getPreview();
let img = await createImageBitmap(blob, { /* … */ });
await new Promise(resolve => requestAnimationFrame(resolve));
canvasCtx.transferFromImageBitmap(img);
} catch (err) {
// …
}
}
Durch die Verwendung dieser modernen APIs wird sichergestellt, dass die gesamte Decodierung im Hintergrund ausgeführt wird und der Canvas nur dann aktualisiert wird, wenn sowohl das Bild als auch der Browser vollständig für das Zeichnen vorbereitet sind. Dadurch erreichte ich auf meinem Laptop konstante 30 fps, was der nativen Leistung von gPhoto2 und der offiziellen Sony-Software entspricht.
USB-Zugriff synchronisieren
Wenn eine USB-Datenübertragung angefordert wird, während ein anderer Vorgang bereits läuft, führt dies in der Regel zu der Fehlermeldung „Gerät ist belegt“. Da sich die Vorschau und die Einstellungen regelmäßig aktualisieren und der Nutzer gleichzeitig ein Bild aufnehmen oder die Einstellungen ändern möchte, sind solche Konflikte zwischen verschiedenen Vorgängen sehr häufig.
Um sie zu vermeiden, musste ich alle Zugriffe innerhalb der Anwendung synchronisieren. Dazu habe ich eine promisebasierte asynchrone Warteschlange erstellt:
let context = await new Module.Context();
let queue = Promise.resolve();
function schedule(op) {
let res = queue.then(() => op(context));
queue = res.catch(rethrowIfCritical);
return res;
}
Indem ich jeden Vorgang in einem then()
-Callback des vorhandenen queue
-Promise verkettee und das verkettete Ergebnis als neuen Wert von queue
speichere, kann ich dafür sorgen, dass alle Vorgänge nacheinander und ohne Überschneidungen ausgeführt werden.
Alle Vorgangsfehler werden an den Aufrufer zurückgegeben, während kritische (unerwartete) Fehler die gesamte Kette als abgelehntes Promise markieren und sichergestellt wird, dass danach kein neuer Vorgang geplant wird.
Indem ich den Modulkontext in einer privaten (nicht exportierten) Variablen belasse, minimiere ich das Risiko, dass versehentlich an anderer Stelle in der App auf context
zugegriffen wird, ohne den schedule()
-Aufruf zu verwenden.
Um alles zusammenzuführen, muss jeder Zugriff auf den Gerätekontext jetzt in einen schedule()
-Aufruf wie diesen eingewickelt werden:
let config = await this.connection.schedule((context) => context.configToJS());
und
this.connection.schedule((context) => context.captureImageAsFile());
Danach wurden alle Vorgänge ohne Konflikte erfolgreich ausgeführt.
Fazit
Weitere Informationen zur Implementierung finden Sie in der Codebasis auf GitHub. Ich möchte mich auch bei Marcus Meißner für die Wartung von gPhoto2 und für seine Rezensionen zu meinen vorgelagerten PRs bedanken.
Wie in diesen Beiträgen gezeigt, bieten WebAssembly-, Asyncify- und Fugu-APIs ein leistungsfähiges Kompilierungsziel für selbst die komplexesten Anwendungen. Sie ermöglichen es Ihnen, eine Bibliothek oder eine Anwendung, die zuvor für eine einzige Plattform erstellt wurde, ins Web zu übertragen, sodass sie einer weitaus größeren Anzahl von Nutzern auf Desktop- und Mobilgeräten verfügbar ist.