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

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

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

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

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

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

इसके अलावा, 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 पोर्ट अडैप्टर और कैमरा लाइब्रेरी की जानकारी देने और उन्हें डाइनैमिक तौर पर लोड करने के लिए, libtool का इस्तेमाल करता है. उदाहरण के लिए, 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 करना है. इसके लिए, प्रोग्राम को लिंक करते समय -dlopen या -dlpreopen फ़्लैग का इस्तेमाल करें (लिंक मोड देखें).

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

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

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

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

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

इसलिए, मैंने 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
();

autoconf बिल्ड सिस्टम में, मुझे अब उन दोनों फ़ाइलों के साथ -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);
 
// …

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