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

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

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

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

बिल्ड सिस्टम को पसंद के मुताबिक बनाए गए फ़ोर्क की ओर पॉइंट करना

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

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

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

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

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

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

Emscripten के पास पहले से ही (path to emscripten cache)/sysroot के तहत अपना खुद का सिस्टम होता है, जिसका इस्तेमाल वह अपनी सिस्टम लाइब्रेरी, Emscripten पोर्ट, और CMake और 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 पोर्ट अडैप्टर और कैमरा लाइब्रेरी की गिनती करने और डाइनैमिक तौर पर लोड करने के लिए libgtool का इस्तेमाल करता है. उदाहरण के लिए, I/O लाइब्रेरी लोड करने का कोड ऐसा दिखता है:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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