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

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

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

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

बिल्ड सिस्टम को कस्टम फ़ॉर्क पर ले जाना

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

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

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

इस डायग्राम में दिखाया गया है कि 'ऐप्लिकेशन', 'libgphoto2 फ़ॉर्क' पर निर्भर करता है. यह 'libtool' पर निर्भर करता है. 'libtool' ब्लॉक, 'libgphoto2 ports' और 'libgphoto2 camlibs' पर डाइनैमिक तरीके से निर्भर करता है. आखिर में, 'libgphoto2 ports', 'libusb fork' पर स्टैटिक तौर पर निर्भर करता है.

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

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

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

# 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 पोर्ट अडैप्टर और कैमरा लाइब्रेरी की गिनती करने और डाइनैमिक तौर पर लोड करने के लिए libgtool का इस्तेमाल करता है. उदाहरण के लिए, I/O लाइब्रेरी लोड करने के लिए कोड कुछ ऐसा दिखता है:

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

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

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

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

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

“Libtool, libtool ऑब्जेक्ट और libtool लाइब्रेरी फ़ाइलों को dlopen करने के लिए खास सहायता उपलब्ध कराता है, ताकि उनके सिंबल को dlopen और dlsym फ़ंक्शन के बिना भी प्लैटफ़ॉर्म पर हल किया जा सके.

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

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

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

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

यहां अपडेट किया गया डिपेंडेंसी डायग्राम दिखाया गया है. इसमें सभी चीज़ें स्टैटिक तौर पर लिंक की गई हैं:

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

इसलिए, मैंने 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 को यह तय करने का तरीका चाहिए कि कौनसा सिंबल किस लाइब्रेरी से जुड़ा है. इसे पाने के लिए, डेवलपर के लिए यह ज़रूरी है कि वे, बिना अनुमति के सार्वजनिक किए गए सभी सिंबल के नाम बदलकर {function name} कर {library name}_LTX_{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>
// …

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

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

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

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

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

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

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

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

gPhoto2 में, सिर्फ़ बदली गई सेटिंग वापस पाने का कोई तरीका नहीं है. इसमें सिर्फ़ पूरा ट्री या अलग-अलग विजेट हैं. इनपुट फ़ोकस या स्क्रोल पोज़िशन में कोई बदलाव किए बिना, यूज़र इंटरफ़ेस (यूआई) को अप-टू-डेट रखने के लिए, मुझे एक तरीका चाहिए था, ताकि शुरू करने वालों के बीच विजेट ट्री में अंतर किया जा सके. साथ ही, सिर्फ़ बदली गई यूज़र इंटरफ़ेस (यूआई) प्रॉपर्टी को अपडेट किया जा सके. हालांकि, वेब पर इस समस्या को हल कर दिया गया है. यह 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}));
  }
}

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

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

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

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

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

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

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

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 की ओर से, मेरे पास gPhoto2 में मौजूद लूप जैसा एक लूप है, जो झलक वाली इमेज को Blob के तौर पर वापस लाता रहता है. साथ ही, 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+ एफ़पीएस (फ़्रेम प्रति सेकंड) तक पहुंच गए, जो gफ़ोटो2 और Sony के आधिकारिक सॉफ़्टवेयर दोनों की स्थानीय परफ़ॉर्मेंस से मेल खाते थे.

यूएसबी ऐक्सेस सिंक करना

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

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

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 पर कोडबेस ब्राउज़ करें. मुझे gPhoto2 को मैनेज करने और मेरे अपस्ट्रीम पीआर की समीक्षा करने के लिए, मार्कस मेस्नर का भी धन्यवाद करना है.

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