USB ऐप्लिकेशन को वेब पर पोर्ट किया जा रहा है. भाग 2: gPhoto2

जानें कि किसी वेब ऐप्लिकेशन से यूएसबी से बाहरी कैमरे कंट्रोल करने के लिए, gPhotos2 को WebAssembly में कैसे पोर्ट किया गया है.

पिछली पोस्ट में मैंने दिखाया कि WebAssembly / Emscripten, Asyncify, और WebUSB की मदद से, वेब पर चलाने के लिए libub लाइब्रेरी को कैसे पोर्ट किया गया था.

मैंने gPhoto2 की मदद से बनाया गया एक डेमो भी दिखाया. इसकी मदद से, वेब ऐप्लिकेशन की मदद से यूएसबी की मदद से डीएसएलआर और मिररलेस कैमरे कंट्रोल किए जा सकते हैं. इस पोस्ट में, हम gPhotos2 पोर्ट से जुड़ी तकनीकी जानकारी के बारे में गहराई से जानेंगे.

बिल्ड सिस्टम को कस्टम फ़ोर्क की ओर इशारा करना

मैं WebAssembly को टारगेट कर रहा था. इसलिए, सिस्टम डिस्ट्रिब्यूशन से मिलने वाले libub और libgphoto2 का इस्तेमाल नहीं कर सका. इसके बजाय, मुझे अपने ऐप्लिकेशन को libgphoto2 के कस्टम फ़ॉर्क का इस्तेमाल करना था, जबकि libgphoto2 के उस फ़ोर्क को मेरे कस्टम फ़ोर्क का इस्तेमाल करना था.

इसके अलावा, libgphoto2 डाइनैमिक प्लगिन को लोड करने के लिए, libtool का इस्तेमाल करता है. भले ही, मुझे दूसरी दो लाइब्रेरी की तरह libtool को फ़ोर्क नहीं करना पड़ता था. इसके बावजूद, मुझे अब भी इसे WebAssembly में बनाना था और सिस्टम पैकेज के बजाय उस कस्टम बिल्ड पर पॉइंट libgphoto2 करना था.

यहां अनुमानित डिपेंडेंसी डायग्राम दिया गया है (डैश वाली लाइनें डाइनैमिक लिंकिंग को दिखाती हैं):

एक डायग्राम में 'libgphoto2 fork' के आधार पर 'ऐप्लिकेशन' दिखाया गया है, जो 'libtool' पर निर्भर है. 'libtool' ब्लॉक 'libgphoto2 पोर्ट्स' और 'libgphoto2 camlibs' पर डाइनैमिक रूप से निर्भर करता है. अंत में, 'libgphoto2 पोर्ट', 'libubb fork' पर स्थिर रूप से निर्भर करता है.

ज़्यादातर कॉन्फ़िगर-आधारित बिल्ड सिस्टम, अलग-अलग फ़्लैग के ज़रिए डिपेंडेंसी के लिए पाथ बदलने की अनुमति देते हैं. इनमें, इन लाइब्रेरी में इस्तेमाल किए गए बिल्ड सिस्टम भी शामिल हैं. इसलिए, मैंने सबसे पहले यही करने की कोशिश की थी. हालांकि, जब डिपेंडेंसी ग्राफ़ जटिल हो जाता है, तब हर लाइब्रेरी की डिपेंडेंसी के लिए पाथ ओवरराइड की सूची ज़्यादा शब्दों में हो जाती है और गड़बड़ी होने की आशंका होती है. मुझे कुछ ऐसी गड़बड़ियां भी मिली हैं जिनमें बिल्ड सिस्टम, नॉन-स्टैंडर्ड पाथ में रहने के लिए डिपेंडेंसी के लिए तैयार नहीं थे.

इसके बजाय, एक आसान तरीका यह है कि कस्टम सिस्टम रूट के तौर पर एक अलग फ़ोल्डर बनाया जाए (जिसे अक्सर "sysroot" भी छोटा कर दिया जाता है) और इसमें सभी शामिल बिल्ड सिस्टम को टारगेट किया जाता है. इस तरह, हर लाइब्रेरी बिल्ड के दौरान बताए गए sysroot में अपनी डिपेंडेंसी के लिए खोज करेगी और यह खुद को उसी sysroot में इंस्टॉल कर देगी जिससे दूसरे लोग इसे ज़्यादा आसानी से ढूंढ सकें.

Emscripten के पास पहले से ही (path to emscripten cache)/sysroot का अपना sysroot है, जिसका इस्तेमाल वह अपनी सिस्टम लाइब्रेरी, Emscripten पोर्ट, और C Make और pkg-config जैसे टूल के लिए करता है. मैंने अपनी डिपेंडेंसी के लिए भी उसी सिस्टम को दोबारा इस्तेमाल करना चुना.

# 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) # …

इस तरह के कॉन्फ़िगरेशन के साथ, मुझे सिर्फ़ हर डिपेंडेंसी में make install चलाना होता था, जिसने इसे sysroot के तहत इंस्टॉल किया था. इसके बाद, लाइब्रेरी एक-दूसरे को अपने-आप ढूंढ लेती थीं.

डाइनैमिक लोडिंग से जुड़ी समस्या

जैसा कि ऊपर बताया गया है, libgphoto2, I/O पोर्ट अडैप्टर और कैमरा लाइब्रेरी की गिनती करने और डाइनैमिक तरीके से लोड करने के लिए, libtool का इस्तेमाल करता है. उदाहरण के लिए, I/O लाइब्रेरी लोड करने के लिए कोड ऐसा दिखता है:

lt_dlinit ();
lt_dladdsearchdir (iolibs);
result = lt_dlforeachfile (iolibs, foreach_func, list);
lt_dlexit ();

वेब पर इस तरीके में कुछ समस्याएं हैं:

  • WebAssembly मॉड्यूल को डाइनैमिक तरीके से लिंक करने के लिए कोई स्टैंडर्ड सहायता उपलब्ध नहीं है. Emscripten में पसंद के मुताबिक लागू करने की सुविधा है जो libtool में इस्तेमाल किए गए dlopen() API को सिम्युलेट कर सकती है. हालांकि, इसके लिए आपको अलग-अलग फ़्लैग वाले "main'' और "साइड" मॉड्यूल बनाने होंगे. खास तौर पर, dlopen() के लिए, ऐप्लिकेशन के चालू होने के दौरान, एमुलेट किए गए फ़ाइल सिस्टम में साइड मॉड्यूल पहले से लोड करने की ज़रूरत होगी. उन फ़्लैग और ट्वीक को बहुत सारी डाइनैमिक लाइब्रेरी वाले मौजूदा ऑटोकॉन्फ़्रेंस बिल्ड सिस्टम में इंटिग्रेट करना मुश्किल हो सकता है.
  • अगर dlopen() खुद लागू किया गया है, तब भी वेब पर किसी खास फ़ोल्डर में सभी डाइनैमिक लाइब्रेरी की गिनती नहीं की जा सकती. ऐसा इसलिए होता है, क्योंकि सुरक्षा की वजहों से, ज़्यादातर एचटीटीपी सर्वर, डायरेक्ट्री लिस्टिंग को सार्वजनिक नहीं करते.
  • डाइनैमिक लाइब्रेरी को रनटाइम में कैलकुलेट करने के बजाय, कमांड लाइन पर लिंक करने से डुप्लीकेट सिंबल की समस्या जैसी समस्याएं हो सकती हैं. ये समस्याएं Emscripten और दूसरे प्लैटफ़ॉर्म पर शेयर की गई लाइब्रेरी के दिखने में अंतर की वजह से होती हैं.

बिल्ड सिस्टम को उन अंतरों के हिसाब से ढाला जा सकता है और बिल्ड के दौरान कहीं-कहीं डाइनैमिक प्लगिन की सूची को हार्डकोड किया जा सकता है, लेकिन उन सभी समस्याओं को हल करने का एक आसान तरीका यह है कि शुरुआत में डाइनैमिक लिंकिंग से बचा जाए.

libtool अलग-अलग प्लैटफ़ॉर्म पर डाइनैमिक लिंकिंग के तरीकों को हटा देता है. साथ ही, यह दूसरों के लिए कस्टम लोडर लिखने की सुविधा भी देता है. इसके साथ काम करने वाले बिल्ट-इन लोडर में से एक "Dlpreopening" है:

“Libtool, dlopening libtool ऑब्जेक्ट और libtool लाइब्रेरी फ़ाइलों के लिए खास सहायता उपलब्ध कराता है जिनमें dlopen या DLS फ़ंक्शन का इस्तेमाल नहीं हुआ है, उन प्लैटफ़ॉर्म पर भी उनके सिंबल को हल किया जा सकता है.
...
Libtool को कंपाइल करने के दौरान ऑब्जेक्ट को प्रोग्राम में लिंक करके, स्टैटिक प्लैटफ़ॉर्म पर -dlopen को एम्युलेट करता है. साथ ही, ऐसा डेटा स्ट्रक्चर बनाता है जो प्रोग्राम की सिंबल टेबल को दिखाता है. इस सुविधा का इस्तेमाल करने के लिए, आपको अपने प्रोग्राम को लिंक करते समय -dlopen या -dlpreopen फ़्लैग का इस्तेमाल करके, उन ऑब्जेक्ट का एलान करना होगा जिन्हें आपको अपने ऐप्लिकेशन को dlopen करना है. (लिंक मोड देखें).”

इस तरीके की मदद से, Emscripten के बजाय libtool लेवल पर डाइनैमिक लोडिंग को एम्युलेट किया जा सकता है. साथ ही, हर चीज़ को स्टैटिक रूप से सिंगल लाइब्रेरी में लिंक किया जा सकता है.

इस समस्या का एक ही हल नहीं है, लेकिन वह है डाइनैमिक लाइब्रेरी की सूची बनाना. इनकी सूची को अब भी कहीं हार्डकोड किया जाना चाहिए. अच्छी बात यह है कि मुझे इस ऐप्लिकेशन के लिए बहुत कम प्लग इन की ज़रूरत है:

  • पोर्ट की तरफ़, मुझे सिर्फ़ लाइबब्स-आधारित कैमरा कनेक्शन की परवाह है, पीटीपी/आईपी, सीरियल ऐक्सेस या यूएसबी ड्राइव मोड की नहीं.
  • camlibs की तरफ़, वेंडर के लिए अलग-अलग प्लगिन मौजूद होते हैं, जो कुछ खास काम कर सकते हैं. हालांकि, सामान्य सेटिंग कंट्रोल और कैप्चर के लिए, पिक्चर ट्रांसफ़र प्रोटोकॉल का इस्तेमाल करना काफ़ी होता है, जिसे ptp2 camlib दिखाया जाता है. यह बाज़ार में मौजूद हर कैमरे के साथ काम करता है.

यहां बताया गया है कि सभी चीज़ों को स्टैटिक रूप से एक साथ लिंक करने पर, अपडेट किया गया डिपेंडेंसी डायग्राम कैसा दिखता है:

एक डायग्राम में 'libgphoto2 fork' के आधार पर 'ऐप्लिकेशन' दिखाया गया है, जो 'libtool' पर निर्भर है. 'libtool' 'ports: libub1' और 'camlibs: libptp2' पर निर्भर करता है. 'पोर्ट: libasb1', 'लिब्सब फ़ॉर्क' पर निर्भर करती है.

मैंने Emscripten के लिए इसे हार्डकोड किया है:

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 ();

और

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 ();

ऑटोकॉन्फ़्रेंस बिल्ड सिस्टम में, अब मुझे उन दोनों फ़ाइलों के साथ -dlpreopen को सभी एक्ज़ीक्यूटेबल (उदाहरण, टेस्ट, और मेरे खुद के डेमो ऐप्लिकेशन) के लिए लिंक फ़्लैग के रूप में जोड़ना पड़ता था, इस तरह से:

if HAVE_EMSCRIPTEN
LDADD += -dlpreopen $(top_builddir)/libgphoto2_port/usb1.la \
         -dlpreopen $(top_builddir)/camlibs/ptp2.la
endif

अब जब सभी सिंबल एक ही लाइब्रेरी में स्टैटिक रूप से लिंक हैं, तो libtool को यह पता करने का तरीका चाहिए कि कौनसा सिंबल किस लाइब्रेरी से है. इसके लिए, डेवलपर को सभी एक्सपोज़्ड सिंबल के नाम बदलकर {library name}_LTX_{function name} कर देने होंगे. जैसे- {function name}. इसका सबसे आसान तरीका यह है कि लागू की जाने वाली फ़ाइल में सबसे ऊपर सिंबल के नाम फिर से परिभाषित करने के लिए #define का इस्तेमाल किया जाए:

// …
#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>
// …

आने वाले समय में, जब मैं उसी ऐप्लिकेशन में कैमरा से जुड़े प्लगिन को लिंक करने का फ़ैसला करूं, तो नाम रखने की इस स्कीम में नाम के टकराव से भी बचा जा सकता है.

ये सभी परिवर्तन लागू होने के बाद, मैं परीक्षण ऐप्लिकेशन बना सकता/सकती और प्लग-इन को सफलतापूर्वक लोड कर सकता/सकती.

सेटिंग का यूज़र इंटरफ़ेस (यूआई) जनरेट किया जा रहा है

gPhotos2, कैमरा लाइब्रेरी को विजेट ट्री के रूप में अपनी खुद की सेटिंग तय करने देता है. विजेट टाइप की हैरारकी में ये शामिल हैं:

  • विंडो - टॉप-लेवल का कॉन्फ़िगरेशन कंटेनर
    • सेक्शन - अन्य विजेट के नाम वाले ग्रुप
    • बटन फ़ील्ड
    • टेक्स्ट फ़ील्ड
    • संख्यात्मक फ़ील्ड
    • तारीख के फ़ील्ड
    • टॉगल
    • रेडियो बटन

एक्सपोज़्ड C API की मदद से, हर विजेट के नाम, टाइप, चिल्ड्रन, और उसके बारे में काम की अन्य सभी प्रॉपर्टी के बारे में क्वेरी की जा सकती है. साथ ही, वैल्यू के मामले में, उनमें बदलाव भी किया जा सकता है. ये दोनों मिलकर, C से इंटरैक्ट करने वाली किसी भी भाषा में सेटिंग के यूज़र इंटरफ़ेस (यूआई) को अपने-आप जनरेट करने की सुविधा देते हैं.

gPhotos2 के ज़रिए या किसी भी समय कैमरे पर ही सेटिंग बदली जा सकती हैं. इसके अलावा, कुछ विजेट रीड-ओनली हो सकते हैं. यहां तक कि उनकी रीड ओनली स्थिति भी कैमरा मोड और अन्य सेटिंग पर निर्भर करती है. उदाहरण के लिए, शटर स्पीड एक ऐसा फ़ील्ड है जिसे M (मैन्युअल मोड) में लिखा जा सकता है. हालांकि, यह P (प्रोग्राम मोड) में, जानकारी देने वाला रीड ओनली फ़ील्ड बन जाता है. P मोड में, शटर स्पीड की वैल्यू भी डाइनैमिक होगी और लगातार बदलती रहेगी. यह इस बात पर निर्भर करेगा कि कैमरे में वीडियो की चमक कैसी होगी.

कुल मिलाकर, यूज़र इंटरफ़ेस (यूआई) में हमेशा कनेक्ट किए गए कैमरे से अप-टू-डेट जानकारी दिखाना ज़रूरी है. साथ ही, उपयोगकर्ता एक ही यूज़र इंटरफ़ेस (यूआई) से उन सेटिंग में बदलाव भी कर सकते हैं. इस तरह के दो-तरफ़ा डेटा फ़्लो को मैनेज करना ज़्यादा मुश्किल होता है.

gPhotos2 में सिर्फ़ बदली गई सेटिंग को वापस पाने की कोई सुविधा नहीं है. सिर्फ़ पूरे ट्री या अलग-अलग विजेट को वापस लाया जा सकता है. झिलमिलाहट और इनपुट फ़ोकस या स्क्रोल की स्थिति को खोए बिना यूज़र इंटरफ़ेस (यूआई) को अप-टू-डेट रखने के लिए, मुझे एक ऐसा तरीका चाहिए था जिससे ऐप्लिकेशन को शुरू करने के लिए विजेट ट्री के बीच अंतर किया जा सके और सिर्फ़ बदली गई यूज़र इंटरफ़ेस (यूआई) प्रॉपर्टी को अपडेट किया जा सके. अच्छी बात यह है कि वेब पर इस समस्या को हल किया जा चुका है. यह React या Preact जैसे फ़्रेमवर्क का मुख्य फ़ंक्शन है. मैंने इस प्रोजेक्ट के लिए Preact का इस्तेमाल किया, क्योंकि यह हल्का है और मेरी ज़रूरत के सभी काम करता है.

C++ साइड पर, अब मुझे पहले लिंक किए गए C API के ज़रिए सेटिंग ट्री को वापस पाना था और उसे बार-बार वॉक करना था. साथ ही, हर विजेट को JavaScript ऑब्जेक्ट में बदलना था:

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;
    }
    // …

JavaScript की ओर से, अब configToJS को कॉल किया जा सकता है, सेटिंग ट्री के JavaScript वर्शन को देखा जा सकता है, और Preact फ़ंक्शन h की मदद से यूज़र इंटरफ़ेस (यूआई) बनाया जा सकता है:

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;
  }
  // …

इस फ़ंक्शन को एक इनफ़ाइनाइट इवेंट लूप में बार-बार चलाने से, मुझे सेटिंग का यूज़र इंटरफ़ेस (यूआई) मिल सकता है, ताकि हमेशा सबसे नई जानकारी दिखाई जा सके. साथ ही, उपयोगकर्ता जब भी किसी फ़ील्ड में बदलाव करे, तो कैमरे को निर्देश भेजें.

Preact, नतीजों को अलग-अलग कर सकती है और यूज़र इंटरफ़ेस (यूआई) के बदले हुए बिट के लिए डीओएम को अपडेट कर सकती है. इससे पेज के फ़ोकस या पेज की स्थिति में बदलाव करने में कोई रुकावट नहीं आती. एक समस्या जो बची है वह है दोतरफ़ा डेटा फ़्लो. React और Preact जैसे फ़्रेमवर्क, एकतरफ़ा डेटा फ़्लो को ध्यान में रखकर डिज़ाइन किए गए थे. ऐसा इसलिए, क्योंकि इससे डेटा को समझने में आसानी होती है और फिर से चलाए जाने के बीच डेटा की तुलना की जा सकती है. हालांकि, मैंने किसी बाहरी सोर्स यानी कैमरे को किसी भी समय सेटिंग यूज़र इंटरफ़ेस (यूआई) अपडेट करने की अनुमति देकर, इस उम्मीद को तोड़ दिया है.

मैंने इस समस्या को हल करने के लिए, ऐसे सभी इनपुट फ़ील्ड के लिए यूज़र इंटरफ़ेस (यूआई) अपडेट से ऑप्ट आउट किया है जिनमें फ़िलहाल बदलाव किए जा रहे हैं:

/**
 * 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}));
  }
}

इस तरह, किसी भी फ़ील्ड का सिर्फ़ एक मालिक होता है. उपयोगकर्ता, फ़िलहाल फ़ील्ड में बदलाव कर रहा है और कैमरे से अपडेट की गई वैल्यू में उसका कोई असर नहीं होगा. इसके अलावा, फ़ोकस से बाहर होने पर, कैमरा फ़ील्ड की वैल्यू अपडेट कर रहा होता है.

लाइव "वीडियो" फ़ीड बनाना

महामारी के दौरान, बहुत से लोग ऑनलाइन मीटिंग करने लगे. इसके अलावा, इससे वेबकैम मार्केट में कमी देखने को मिली. लैपटॉप में पहले से मौजूद कैमरे की तुलना में, बेहतर वीडियो क्वालिटी पाने के लिए, डीएसएलआर और बिना मिरर वाले कैमरे के कई मालिकों ने अपने फ़ोटोग्राफ़ी कैमरों को वेबकैम के तौर पर इस्तेमाल करने के तरीके ढूंढने शुरू कर दिए. कई कैमरा वेंडर ने इस काम के लिए आधिकारिक सुविधाएं भी शिप की हैं.

आधिकारिक टूल की तरह, gPhotos2, कैमरे से वीडियो स्ट्रीम करने की सुविधा देता है. ऐसा, डिवाइस पर सेव की गई फ़ाइल या सीधे किसी वर्चुअल वेबकैम पर भी किया जा सकता है. मुझे अपने डेमो में लाइव व्यू देने के लिए, इस सुविधा का इस्तेमाल करना था. हालांकि, यह कंसोल यूटिलिटी में उपलब्ध है, लेकिन मुझे libgphoto2 लाइब्रेरी के एपीआई में यह कहीं भी नहीं मिला.

कंसोल यूटिलिटी में उससे जुड़े फ़ंक्शन के सोर्स कोड को देखने पर, मैंने पाया कि वह असल में कोई वीडियो नहीं बना रहा है. इसके बजाय, यह कैमरे की झलक को एक कभी न खत्म होने वाले लूप में अलग-अलग JPEG इमेज के तौर पर सेव करता रहता है. साथ ही, M-JPEG स्ट्रीम बनाने के लिए, उसे एक-एक करके लिखता है:

while (1) {
  const char *mime;
  r = gp_camera_capture_preview (p->camera, file, p->context);
  // …

मुझे हैरानी हुई कि यह तरीका असरदार तरीके से काम करता है और आपको आसानी से रीयल टाइम में वीडियो का इंप्रेशन मिल जाता है. मुझे वेब ऐप्लिकेशन में भी एक जैसी परफ़ॉर्मेंस को मैच करने में ज़्यादा मुश्किल हुई. अब इसमें अतिरिक्त ऐब्सट्रैक्ट और Asyncify का इस्तेमाल किया जा सकता है. हालांकि, मैंने फिर भी कोशिश करने का फ़ैसला किया.

C++ साइड पर मैंने capturePreviewAsBlob() नाम का एक तरीका दिखाया है, जो उसी gp_camera_capture_preview() फ़ंक्शन को शुरू करता है और नतीजे में मौजूद इन-मेमोरी फ़ाइल को Blob में बदल देता है. इसे अन्य वेब एपीआई को ज़्यादा आसानी से पास किया जा सकता है:

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));
  });
}

JavaScript की साइड पर, gPhotos2 की तरह ही एक लूप है, जो झलक वाली इमेज को Blobs के तौर पर वापस लाता रहता है, उन्हें createImageBitmap की मदद से बैकग्राउंड में डिकोड करता है, और अगले ऐनिमेशन फ़्रेम पर उन्हें कैनवस पर ट्रांसफ़र करता है:

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) {
    // …
  }
}

उन मॉडर्न एपीआई के इस्तेमाल से यह पक्का होता है कि डिकोड करने का पूरा काम बैकग्राउंड में होता है. साथ ही, कैनवस सिर्फ़ तब अपडेट होता है, जब इमेज और ब्राउज़र, दोनों को ड्रॉइंग के लिए पूरी तरह से तैयार किया गया हो. इसकी वजह से, मेरे लैपटॉप पर लगातार 30 FPS (फ़्रेम प्रति सेकंड) की परफ़ॉर्मेंस बेहतर हुई. यह परफ़ॉर्मेंस, gPhotos2 और Sony के आधिकारिक सॉफ़्टवेयर, दोनों की नेटिव परफ़ॉर्मेंस से मिलती-जुलती थी.

USB ऐक्सेस को सिंक्रोनाइज़ करना

जब किसी दूसरी कार्रवाई के पहले से जारी होने के दौरान, यूएसबी डेटा ट्रांसफ़र का अनुरोध किया जाता है, तो आम तौर पर "डिवाइस व्यस्त है" गड़बड़ी के तौर पर दिखता है. झलक और सेटिंग का यूज़र इंटरफ़ेस (यूआई) नियमित रूप से अपडेट होता है. इसलिए, हो सकता है कि उपयोगकर्ता एक ही समय पर कोई इमेज कैप्चर करने या सेटिंग में बदलाव करने की कोशिश कर रहा हो, इसलिए अलग-अलग कार्रवाइयों के बीच इस तरह का टकराव बहुत बार देखने को मिला.

इनसे बचने के लिए, मुझे ऐप्लिकेशन में सभी ऐक्सेस को सिंक करना था. इसके लिए, मैंने प्रॉमिस पर आधारित एक सिंक सूची तैयार की है:

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;
}

मौजूदा queue प्रॉमिस के then() कॉलबैक में, हर कार्रवाई को चेन करके और चेन वाले नतीजे को queue की नई वैल्यू के तौर पर सेव करके, मैं यह पक्का कर सकता हूं कि सभी कार्रवाइयां एक-एक करके पूरी हों. सभी कार्रवाइयां बिना ओवरलैप के हैं.

ऑपरेशन से जुड़ी कोई भी गड़बड़ी कॉलर को वापस कर दी जाती है, जबकि गंभीर (अचानक) गड़बड़ियां पूरी चेन को अस्वीकार किए गए प्रॉमिस के तौर पर मार्क करती हैं. साथ ही, यह पक्का करती हैं कि इसके बाद कोई नई कार्रवाई शेड्यूल नहीं की जाएगी.

मॉड्यूल के कॉन्टेक्स्ट को एक निजी (एक्सपोर्ट नहीं किया गया) वैरिएबल में रखकर, मैंने schedule() कॉल के बिना ऐप्लिकेशन में कहीं और से गलती से context को ऐक्सेस करने के जोखिम को कम कर दिया है.

चीज़ों को एक साथ जोड़ने के लिए, अब डिवाइस के संदर्भ की हर ऐक्सेस को schedule() कॉल में इस तरह से रैप करना होगा:

let config = await this.connection.schedule((context) => context.configToJS());

और

this.connection.schedule((context) => context.captureImageAsFile());

इसके बाद, बिना किसी रुकावट के सभी ऑपरेशन बिना किसी रुकावट के लागू किए जा रहे थे.

नतीजा

लागू करने से जुड़ी अहम जानकारी के लिए, GitHub पर कोडबेस को ब्राउज़ करें. मैं gPhotos2 के रखरखाव और अपस्ट्रीम PRs की समीक्षाओं के लिए, मार्कस मीसनर को भी धन्यवाद देना चाहता हूं.

जैसा कि इन पोस्ट में दिखाया गया है, WebAssembly, Asyncify, और Fugu API, सबसे मुश्किल ऐप्लिकेशन के लिए भी बेहतर कंपाइलेशन टारगेट उपलब्ध कराते हैं. इन सुविधाओं की मदद से, किसी प्लैटफ़ॉर्म के लिए पहले बनाई गई लाइब्रेरी या ऐप्लिकेशन को वेब पर पोर्ट किया जा सकता है. ऐसा करके, डेस्कटॉप और मोबाइल डिवाइसों का इस्तेमाल करने वाले ज़्यादातर लोगों को यह सुविधा मिलती है.