USB ऐप्लिकेशन को वेब पर पोर्ट किया जा रहा है. भाग 1: li एक्सटेंशन

जानें कि बाहरी डिवाइसों से इंटरैक्ट करने वाले कोड को WebAssembly और Fugu API की मदद से, वेब पर कैसे पोर्ट किया जा सकता है.

पिछली पोस्ट में, मैंने File System Access API, WebAssembly, और Asyncify की मदद से, फ़ाइल सिस्टम एपीआई का इस्तेमाल करके ऐप्लिकेशन को वेब पर पोर्ट करने का तरीका दिखाया था. अब मैं ज़रूरी सुविधाओं को खोए बिना, WebAssembly के साथ Fugu API को इंटिग्रेट करने और ऐप्लिकेशन को वेब पर पोर्ट करने के विषय को जारी रखना चाहता हूं.

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

सबसे पहली बात: डेमो

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

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

मैं इस डेमो को बनाने का तरीका दो हिस्सों में बताऊँगी. इस ब्लॉग पोस्ट में, मैं आपको बताऊंगा कि मैंने लाइब्स को कैसे पोर्ट किया है और दूसरी लोकप्रिय लाइब्रेरी को Fugu API में पोर्ट करने के लिए कौनसे ट्रिक की ज़रूरत पड़ सकती है. दूसरी पोस्ट में, मैं gPhotos2 को पोर्ट करने और इंटिग्रेट करने के बारे में जानकारी दूंगी.

आखिर में, मेरे पास एक ऐसा वेब ऐप्लिकेशन है जो काम करता है. यह डीएसएलआर से लाइव फ़ीड की झलक दिखाता है और यूएसबी की मदद से इसकी सेटिंग को कंट्रोल कर सकता है. तकनीकी जानकारी पढ़ने से पहले, लाइव या पहले से रिकॉर्ड किया गया डेमो देखें:

Sony कैमरे से कनेक्ट किए गए लैपटॉप पर डेमो चल रहा है.

कैमरे की खास सुविधाओं के बारे में नोट

आपने देखा होगा कि वीडियो में सेटिंग बदलने में कुछ समय लगता है. ज़्यादातर दूसरी समस्याओं की तरह, ऐसा WebAssembly या WebUSB की परफ़ॉर्मेंस की वजह से, बल्कि डेमो के लिए चुने गए कैमरे के साथ gPhotos2 के इंटरैक्ट करने के तरीके की वजह से नहीं होता.

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

इनमें से किसी भी वैल्यू को सेट करते समय, gPhotos2 के पास इनके अलावा कोई विकल्प नहीं होता:

  1. चुनी गई वैल्यू की दिशा में कोई कदम (या कुछ) बनाएं.
  2. सेटिंग अपडेट करने के लिए कैमरे के थोड़ा इंतज़ार करें.
  3. कैमरे के इस्तेमाल किए गए मान को फिर से पढ़ें.
  4. पक्का करें कि आखिरी चरण, मनचाहे वैल्यू के ऊपर न चला गया हो और न ही सूची की शुरुआत में पूरा हो गया हो.
  5. दोहराएं.

इस प्रोसेस में कुछ समय लग सकता है. हालांकि, अगर कैमरा, वैल्यू के साथ काम करता है, तो वैल्यू को वहां मिल जाएगा. अगर ऐसा नहीं होता है, तो वैल्यू उसके आस-पास की वैल्यू पर काम करना बंद कर देगी.

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

क्रॉस-प्लैटफ़ॉर्म पर काम करने की सुविधा के बारे में अहम जानकारी

अफ़सोस की बात यह है कि Windows पर किसी भी "जाने-पहचाने" डिवाइस को एक सिस्टम ड्राइवर असाइन किया जाता है, जिसमें डीएसएलआर कैमरे भी शामिल हैं. यह डिवाइस WebUSB के साथ काम नहीं करता. अगर आपको Windows पर डेमो आज़माना है, तो आपको Zadig जैसे टूल का इस्तेमाल करके, कनेक्ट किए गए DSLR के ड्राइवर को WinUSB या libub पर बदलना होगा. यह तरीका मेरे और कई अन्य लोगों के लिए सही है. हालांकि, आपको अपने जोखिम पर इसका इस्तेमाल करना चाहिए.

Linux पर, आपको WebUSB के ज़रिए अपने डीएसएलआर का ऐक्सेस देने के लिए, कस्टम अनुमतियां सेट करनी होंगी. हालांकि, यह आपके डिस्ट्रिब्यूशन पर निर्भर करता है.

macOS और Android पर, डेमो अन्य काम करना चाहिए. अगर आपको इसे Android फ़ोन पर आज़माना है, तो लैंडस्केप मोड पर स्विच करना न भूलें, क्योंकि मैंने इसे प्रतिक्रियाशील बनाने में ज़्यादा मेहनत नहीं की है (पीआरएस आपका स्वागत है!):

Android फ़ोन को यूएसबी-सी केबल के ज़रिए Canon कैमरे से कनेक्ट किया गया है.
वही डेमो Android फ़ोन पर चल रहा है. सूरमा ने तस्वीर दी है.

WebUSB के क्रॉस-प्लैटफ़ॉर्म इस्तेमाल के बारे में ज़्यादा जानकारी वाली गाइड के लिए, "WebUSB के लिए एक डिवाइस बनाना" का "प्लैटफ़ॉर्म के हिसाब से ध्यान रखना" सेक्शन देखें.

libubb में नया बैकएंड जोड़ना

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

अच्छी बात यह है कि libub README में बताया गया है:

“लिब्सब को अंदरूनी तौर पर इस तरह से ऐब्स्ट्रैक्ट किया गया है कि इसे दूसरे ऑपरेटिंग सिस्टम में पोर्ट किया जा सकता है. ज़्यादा जानकारी के लिए, कृपया पोर्टिंग फ़ाइल देखें.”

libubb को इस तरह से बनाया गया है कि सार्वजनिक एपीआई "बैकएंड" से अलग है. ये बैकएंड, ऑपरेटिंग सिस्टम के लो-लेवल एपीआई की मदद से डिवाइसों को सूची में जोड़ने, उन्हें खोलने, बंद करने, और उनके साथ डेटा शेयर करने के लिए ज़िम्मेदार होते हैं. इस तरह libubb में Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku, और Solaris के बीच के अंतर को पहले ही खत्म कर दिया जाता है. साथ ही, यह इन सभी प्लैटफ़ॉर्म पर काम करता है.

मुझे Emscripten+WebUSB "ऑपरेटिंग सिस्टम" के लिए एक और बैकएंड जोड़ना था. उन बैकएंड को लागू करने की प्रोसेस libusb/os फ़ोल्डर में होती है:

~/w/d/libusb $ ls libusb/os
darwin_usb.c           haiku_usb_raw.h  threads_posix.lo
darwin_usb.h           linux_netlink.c  threads_posix.o
events_posix.c         linux_udev.c     threads_windows.c
events_posix.h         linux_usbfs.c    threads_windows.h
events_posix.lo        linux_usbfs.h    windows_common.c
events_posix.o         netbsd_usb.c     windows_common.h
events_windows.c       null_usb.c       windows_usbdk.c
events_windows.h       openbsd_usb.c    windows_usbdk.h
haiku_pollfs.cpp       sunos_usb.c      windows_winusb.c
haiku_usb_backend.cpp  sunos_usb.h      windows_winusb.h
haiku_usb.h            threads_posix.c
haiku_usb_raw.cpp      threads_posix.h

हर बैकएंड में, libusbi.h हेडर शामिल होता है. यह हेडर, सामान्य टाइप और हेल्पर होता है. साथ ही, इसे usbi_os_backend टाइप का usbi_backend वैरिएबल दिखाना होता है. उदाहरण के लिए, Windows बैकएंड ऐसा दिखता है:

const struct usbi_os_backend usbi_backend = {
  "Windows",
  USBI_CAP_HAS_HID_ACCESS,
  windows_init,
  windows_exit,
  windows_set_option,
  windows_get_device_list,
  NULL,   /* hotplug_poll */
  NULL,   /* wrap_sys_device */
  windows_open,
  windows_close,
  windows_get_active_config_descriptor,
  windows_get_config_descriptor,
  windows_get_config_descriptor_by_value,
  windows_get_configuration,
  windows_set_configuration,
  windows_claim_interface,
  windows_release_interface,
  windows_set_interface_altsetting,
  windows_clear_halt,
  windows_reset_device,
  NULL,   /* alloc_streams */
  NULL,   /* free_streams */
  NULL,   /* dev_mem_alloc */
  NULL,   /* dev_mem_free */
  NULL,   /* kernel_driver_active */
  NULL,   /* detach_kernel_driver */
  NULL,   /* attach_kernel_driver */
  windows_destroy_device,
  windows_submit_transfer,
  windows_cancel_transfer,
  NULL,   /* clear_transfer_priv */
  NULL,   /* handle_events */
  windows_handle_transfer_completion,
  sizeof(struct windows_context_priv),
  sizeof(union windows_device_priv),
  sizeof(struct windows_device_handle_priv),
  sizeof(struct windows_transfer_priv),
};

प्रॉपर्टी पर नज़र डालने पर, हम देख सकते हैं कि स्ट्रक्चर में बैकएंड नाम, इसकी क्षमताओं का एक सेट, फ़ंक्शन पॉइंटर के रूप में अलग-अलग लो-लेवल यूएसबी ऑपरेशन के लिए हैंडलर, और आखिर में, निजी डिवाइस-/context-/ट्रांसफ़र-लेवल डेटा को सेव करने के लिए साइज़ शामिल होते हैं.

निजी डेटा फ़ील्ड, ओएस के हैंडल को इन सभी चीज़ों के लिए सेव करने में मददगार होते हैं. ऐसा इसलिए, क्योंकि हैंडल के बिना हम यह नहीं जानते कि कोई कार्रवाई किस आइटम पर लागू होती है. वेब में लागू किए गए ओएस हैंडल, WebUSB JavaScript ऑब्जेक्ट होंगे. Emscripten में इन्हें दिखाने और सेव करने का एक आम तरीका है, emscripten::val क्लास का इस्तेमाल करना. यह क्लास Embind (Emscripten का बाइंडिंग सिस्टम) के हिस्से के तौर पर दी गई है.

फ़ोल्डर में ज़्यादातर बैकएंड, C में लागू किए जाते हैं, लेकिन कुछ बैकएंड C++ में लागू किए जाते हैं. Embind सिर्फ़ C++ के साथ काम करता है. इसलिए, मैंने यह विकल्प चुना और मैंने libusb/libusb/os/emscripten_webusb.cpp को ज़रूरी स्ट्रक्चर के साथ और निजी डेटा फ़ील्ड के लिए sizeof(val) के साथ जोड़ दिया:

#include <emscripten.h>
#include <emscripten/val.h>

#include "libusbi.h"

using namespace emscripten;

// …function implementations

const usbi_os_backend usbi_backend = {
  .name = "Emscripten + WebUSB backend",
  .caps = LIBUSB_CAP_HAS_CAPABILITY,
  // …handlers—function pointers to implementations above
  .device_priv_size = sizeof(val),
  .transfer_priv_size = sizeof(val),
};

WebUSB ऑब्जेक्ट को डिवाइस हैंडल के तौर पर सेव करना

li Busb, निजी डेटा के लिए तय किए गए इलाके के लिए इस्तेमाल के लिए तैयार पॉइंटर उपलब्ध कराता है. उन पॉइंटर के साथ val इंस्टेंस के तौर पर काम करने के लिए, मैंने छोटे हेल्पर जोड़े हैं जो उन्हें सही जगह पर बनाते हैं. साथ ही, उन्हें रेफ़रंस के तौर पर फिर से पाएं, और वैल्यू को बाहर ले जाएं:

// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
 public:
  void init_to(val &&value) { new (ptr) val(std::move(value)); }

  val &get() { return *ptr; }
  val take() { return std::move(get()); }

 protected:
  ValPtr(val *ptr) : ptr(ptr) {}

 private:
  val *ptr;
};

struct WebUsbDevicePtr : ValPtr {
 public:
  WebUsbDevicePtr(libusb_device *dev)
      : ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};

val &get_web_usb_device(libusb_device *dev) {
  return WebUsbDevicePtr(dev).get();
}

struct WebUsbTransferPtr : ValPtr {
 public:
  WebUsbTransferPtr(usbi_transfer *itransfer)
      : ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};

सिंक्रोनस C कॉन्टेक्स्ट में एसिंक्रोनस वेब एपीआई

अब एक साथ काम न करने वाले WebUSB API को मैनेज करने के तरीके की ज़रूरत होगी, जहां लाइबसबी सिंक्रोनस कार्रवाई की उम्मीद करती है. इसके लिए, मैं Asyncify का इस्तेमाल कर सकता हूं या खास तौर पर val::await() के ज़रिए इसके Embind इंटिग्रेशन का इस्तेमाल कर सकता हूं.

मुझे WebUSB की गड़बड़ियों को सही तरीके से हैंडल करना और उन्हें libub गड़बड़ी के कोड में बदलना था. फ़िलहाल, Embind के पास C++ की ओर से JavaScript के अपवादों या Promise को अस्वीकार करने का कोई तरीका नहीं है. इस समस्या को ठीक करने के लिए, JavaScript साइड पर अस्वीकार किए गए नतीजे को पहचाना जा सकता है. साथ ही, नतीजे को { error, value } ऑब्जेक्ट में बदला जा सकता है, जिसे अब C++ साइड से सुरक्षित तरीके से पार्स किया जा सकता है. मैंने EM_JS मैक्रो और Emval.to{Handle, Value} API के संयोजन के साथ ऐसा किया:

EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), {
  let promise = Emval.toValue(handle);
  promise = promise.then(
    value => ({error : 0, value}),
    error => {
      const ERROR_CODES = {
        // LIBUSB_ERROR_IO
        NetworkError : -1,
        // LIBUSB_ERROR_INVALID_PARAM
        DataError : -2,
        TypeMismatchError : -2,
        IndexSizeError : -2,
        // LIBUSB_ERROR_ACCESS
        SecurityError : -3,
        …
      };
      console.error(error);
      let errorCode = -99; // LIBUSB_ERROR_OTHER
      if (error instanceof DOMException)
      {
        errorCode = ERROR_CODES[error.name] ?? errorCode;
      }
      else if (error instanceof RangeError || error instanceof TypeError)
      {
        errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM
      }
      return {error: errorCode, value: undefined};
    }
  );
  return Emval.toHandle(promise);
});

val em_promise_catch(val &&promise) {
  EM_VAL handle = promise.as_handle();
  handle = em_promise_catch_impl(handle);
  return val::take_ownership(handle);
}

// C++ struct representation for {value, error} object from above
// (performs conversion in the constructor).
struct promise_result {
  libusb_error error;
  val value;

  promise_result(val &&result)
      : error(static_cast<libusb_error>(result["error"].as<int>())),
        value(result["value"]) {}

  // C++ counterpart of the promise helper above that takes a promise, catches
  // its error, converts to a libusb status and returns the whole thing as
  // `promise_result` struct for easier handling.
  static promise_result await(val &&promise) {
    promise = em_promise_catch(std::move(promise));
    return {promise.await()};
  }
};

अब मैं WebUSB ऑपरेशन से लौटाए गए किसी भी Promise पर promise_result::await() का इस्तेमाल कर सकता था. साथ ही, उसके error और value फ़ील्ड की अलग-अलग जांच कर सकता था.

उदाहरण के लिए, libusb_device_handle से USBDevice को दिखाने वाले val को फिर से पाना, इसके open() तरीके को कॉल करना, इसके नतीजे का इंतज़ार करना, और लिब्सब स्टेटस कोड के रूप में गड़बड़ी कोड देना कुछ ऐसा दिखता है:

int em_open(libusb_device_handle *handle) {
  auto web_usb_device = get_web_usb_device(handle->dev);
  return promise_result::await(web_usb_device.call<val>("open")).error;
}

डिवाइस की सूची

बेशक, इससे पहले कि मैं किसी भी डिवाइस को खोल सकूँ, libubb को उपलब्ध डिवाइसों की एक सूची फिर से हासिल करनी होगी. बैकएंड को यह कार्रवाई, get_device_list हैंडलर की मदद से लागू करनी होगी.

हालांकि, मुश्किल यह है कि सुरक्षा वजहों से, वेब पर कनेक्ट किए गए सभी यूएसबी डिवाइसों की गिनती करने का कोई तरीका नहीं है. अन्य प्लैटफ़ॉर्म के मुकाबले, ऐसा कोई तरीका नहीं है. इसके बजाय, फ़्लो को दो हिस्सों में बांटा जाता है. सबसे पहले, वेब ऐप्लिकेशन navigator.usb.requestDevice() की मदद से, खास प्रॉपर्टी वाले डिवाइसों के लिए अनुरोध करता है. इसके बाद, उपयोगकर्ता मैन्युअल तरीके से यह चुनता है कि किस डिवाइस को अनुमति के अनुरोध को सार्वजनिक करना है या अस्वीकार करना है. इसके बाद, navigator.usb.getDevices() के ज़रिए, पहले से स्वीकार किए गए और कनेक्ट किए गए डिवाइसों की सूची दिखेगी.

सबसे पहले मैंने get_device_list हैंडलर को लागू करने के दौरान, सीधे requestDevice() को इस्तेमाल करने की कोशिश की. हालांकि, कनेक्ट किए गए डिवाइसों की सूची के साथ अनुमति का प्रॉम्प्ट दिखाना, एक संवेदनशील कार्रवाई माना जाता है. यह उपयोगकर्ता के इंटरैक्शन (जैसे, किसी पेज पर एक बटन पर क्लिक) से ट्रिगर होना चाहिए. ऐसा न करने पर, यह हमेशा अस्वीकार किया गया प्रॉमिस दिखाता है. लिब्सब ऐप्लिकेशन, ऐप्लिकेशन शुरू होने पर अक्सर कनेक्ट किए गए डिवाइसों को लिस्ट करना चाह सकते हैं. इसलिए, requestDevice() का इस्तेमाल करना एक विकल्प नहीं है.

इसके बजाय, मुझे असली डेवलपर को navigator.usb.requestDevice() का अनुरोध छोड़ना पड़ा और सिर्फ़ navigator.usb.getDevices() के लिए पहले से मंज़ूर किए गए डिवाइसों को दिखाना पड़ा:

// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];

int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
  // C++ equivalent of `await navigator.usb.getDevices()`.
  // Note: at this point we must already have some devices exposed -
  // caller must have called `await navigator.usb.requestDevice(...)`
  // in response to user interaction before going to LibUSB.
  // Otherwise this list will be empty.
  auto result = promise_result::await(web_usb.call<val>("getDevices"));
  if (result.error) {
    return result.error;
  }
  auto &web_usb_devices = result.value;
  // Iterate over the exposed devices.
  uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
  for (uint8_t i = 0; i < devices_num; i++) {
    auto web_usb_device = web_usb_devices[i];
    // …
    *devs = discovered_devs_append(*devs, dev);
  }
  return LIBUSB_SUCCESS;
}

ज़्यादातर बैकएंड कोड val और promise_result का इस्तेमाल, ऊपर दिखाए गए तरीके की तरह ही करते हैं. डेटा ट्रांसफ़र हैंडलिंग कोड में कुछ और दिलचस्प हैकिंग हैं, लेकिन इस लेख में दिए गए हैकिंग को लागू करने के बारे में जानकारी ज़्यादा ज़रूरी नहीं है. अगर आपकी दिलचस्पी है, तो GitHub पर कोड और टिप्पणियों को देखना न भूलें.

इवेंट लूप को वेब पर पोर्ट करना

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

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

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

आखिर में, सबसे बड़ी समस्या यह है कि वेब का अपना इवेंट लूप है. इस ग्लोबल इवेंट लूप का इस्तेमाल, किसी भी बाहरी I/O कार्रवाइयों (इसमें fetch(), टाइमर या इस मामले में WebUSB शामिल है) के लिए किया जाता है. साथ ही, इससे जुड़ी कार्रवाइयां पूरी होने पर, यह इवेंट या Promise हैंडलर को शुरू करता है. एक अन्य, नेस्ट किए गए, इनफ़ाइनाइट इवेंट लूप को चलाने से ब्राउज़र का इवेंट लूप, हमेशा आगे नहीं चलेगा. इसका मतलब है कि न सिर्फ़ यूज़र इंटरफ़ेस (यूआई) काम नहीं करेगा, बल्कि यह भी होगा कि कोड को कभी भी उन I/O इवेंट के लिए सूचनाएं नहीं मिलेंगी जिनका वह इंतज़ार कर रहा है. आम तौर पर, इसकी वजह से ट्रैफ़िक में रुकावट आती है. ऐसा तब हुआ, जब मैंने एक डेमो में लिब्सब का इस्तेमाल करने की कोशिश की. पेज फ़्रीज़ हो गया.

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

मैं libubb या gPhotos2 में कोई खास बदलाव नहीं करना चाहती थी. साथ ही, मैंने Promise इंटिग्रेशन के लिए Asyncify का इस्तेमाल पहले ही कर लिया है, इसलिए मैंने यही पाथ चुना है. poll() के ब्लॉक करने वाले वैरिएंट को सिम्युलेट करने के लिए, सिद्धांत के शुरुआती सबूत के लिए मैंने लूप का इस्तेमाल किया है, जैसा कि यहां दिखाया गया है:

#ifdef __EMSCRIPTEN__
  // TODO: optimize this. Right now it will keep unwinding-rewinding the stack
  // on each short sleep until an event comes or the timeout expires.
  // We should probably create an actual separate thread that does signaling
  // or come up with a custom event mechanism to report events from
  // `usbi_signal_event` and process them here.
  double until_time = emscripten_get_now() + timeout_ms;
  do {
    // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
    // in case.
    num_ready = poll(fds, nfds, 0);
    if (num_ready != 0) break;
    // Yield to the browser event loop to handle events.
    emscripten_sleep(0);
  } while (emscripten_get_now() < until_time);
#else
  num_ready = poll(fds, nfds, timeout_ms);
#endif

यह क्या करता है:

  1. poll() को कॉल करके पता लगाता है कि बैकएंड ने कोई इवेंट रिपोर्ट किया या नहीं. अगर कुछ नतीजे दिखते हैं, तो लूप रुक जाता है. अगर ऐसा नहीं है, तो Emscripten को poll() लागू करने पर, तुरंत 0 लागू हो जाएगा.
  2. emscripten_sleep(0) पर कॉल करता है. यह फ़ंक्शन, हुड के तहत Asyncify और setTimeout() का इस्तेमाल करता है. इसका इस्तेमाल कंट्रोल को मुख्य ब्राउज़र इवेंट लूप में वापस लाने के लिए किया जाता है. इससे ब्राउज़र, उपयोगकर्ता के किसी भी इंटरैक्शन और I/O इवेंट को मैनेज कर सकता है. इसमें WebUSB भी शामिल है.
  3. देखें कि तय किया गया समय खत्म तो नहीं हो गया है. अगर नहीं हुआ है, तो लूप जारी रखें.

जैसा कि टिप्पणी में बताया गया है, यह तरीका सबसे अच्छा नहीं था, क्योंकि यह Asyncify की मदद से पूरे कॉल स्टैक को सेव करके, तब भी सुरक्षित रखता है, जब अभी तक हैंडल करने के लिए कोई यूएसबी इवेंट नहीं थे (ज़्यादातर समय के लिए). साथ ही, मॉडर्न ब्राउज़र में setTimeout() का समय कम से कम 4 मि॰से॰ होता है. हालांकि, यह डीएसएलआर से 13 से 14 FPS (फ़्रेम प्रति सेकंड) तक की लाइव स्ट्रीम बनाने में कारगर साबित हुई.

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

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent(new Event("em-libusb"));
});

EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
  let onEvent, timeoutId;

  try {
    return await new Promise(resolve => {
      onEvent = () => resolve(0);
      addEventListener('em-libusb', onEvent);

      timeoutId = setTimeout(resolve, timeout, -1);
    });
  } finally {
    removeEventListener('em-libusb', onEvent);
    clearTimeout(timeoutId);
  }
});

जब भी लाइबब किसी इवेंट, जैसे कि डेटा ट्रांसफ़र को पूरा करने की रिपोर्ट करने की कोशिश करता है, तब em_libusb_notify() फ़ंक्शन का इस्तेमाल किया जाता है:

void usbi_signal_event(usbi_event_t *event)
{
  uint64_t dummy = 1;
  ssize_t r;

  r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
  if (r != sizeof(dummy))
    usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
  em_libusb_notify();
#endif
}

इस दौरान, em_libusb_wait() वाले हिस्से का इस्तेमाल, em-libusb इवेंट के मिलने या टाइम आउट की समयसीमा खत्म होने पर, Asyncify नींद से "जागने" के लिए किया जाता है:

double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
  // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
  // in case.
  num_ready = poll(fds, nfds, 0);
  if (num_ready != 0) break;
  int timeout = until_time - emscripten_get_now();
  if (timeout <= 0) break;
  int result = em_libusb_wait(timeout);
  if (result != 0) break;
}

नींद और जागने में काफ़ी कमी होने की वजह से, इस सुविधा ने emscripten_sleep() के मुताबिक पहले से लागू किए गए काम करने के तरीके में आने वाली समस्याओं को ठीक किया है. साथ ही, डीएसएलआर डेमो की क्षमता को 13 से 14 FPS (फ़्रेम प्रति सेकंड) से बढ़ाकर 30+ FPS (फ़्रेम प्रति सेकंड) तक किया है. यह लाइव फ़ीड बेहतर बनाने के लिए काफ़ी है.

सिस्टम बनाएं और पहला टेस्ट करें

बैकएंड पूरा होने के बाद, मुझे इसे Makefile.am और configure.ac में जोड़ना होगा. यहां बस एक मज़ेदार बात है, जो Emscripten के हिसाब से फ़्लैग में बदलाव है:

emscripten)
  AC_SUBST(EXEEXT, [.html])
  # Note: LT_LDFLAGS is not enough here because we need link flags for executable.
  AM_LDFLAGS="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
  ;;

पहली बात, Unix प्लैटफ़ॉर्म पर चलने वाले एक्ज़ीक्यूटेबल में आम तौर पर फ़ाइल एक्सटेंशन नहीं होते. हालांकि, Emscripten हर एक्सटेंशन के लिए अलग-अलग नतीजे देता है. यह इस बात पर निर्भर करता है कि आपने किस एक्सटेंशन का अनुरोध किया है. एक्ज़ीक्यूटेबल एक्सटेंशन को .html में बदलने के लिए, मैं AC_SUBST(EXEEXT, …) का इस्तेमाल कर रहा/रही हूं, ताकि पैकेज—टेस्ट और उदाहरणों—में मौजूद एक्ज़ीक्यूटेबल, Emscripten के डिफ़ॉल्ट शेल वाला एचटीएमएल बन जाए. यह Emscripten के डिफ़ॉल्ट शेल वाला एचटीएमएल बन जाता है. यह JavaScript और WebAssembly को लोड करने और इंस्टैंशिएट करने का काम करता है.

दूसरी वजह यह है कि आपने Embind और Asyncify का इस्तेमाल किया है. इसलिए, मुझे ये सुविधाएं (--bind -s ASYNCIFY) चालू करनी होंगी. साथ ही, लिंकर पैरामीटर की मदद से डाइनैमिक मेमोरी ग्रोथ (-s ALLOW_MEMORY_GROWTH) की अनुमति देनी होगी. माफ़ करें, लाइब्रेरी के पास लिंकर को उन फ़्लैग की रिपोर्ट करने का कोई तरीका नहीं है. इसलिए, इस लिब्सब पोर्ट का इस्तेमाल करने वाले हर ऐप्लिकेशन को अपने बिल्ड कॉन्फ़िगरेशन में एक जैसे लिंकर फ़्लैग जोड़ने होंगे.

आखिर में, जैसा कि पहले बताया गया है, WebUSB के लिए उपयोगकर्ता के जेस्चर का इस्तेमाल करके डिवाइस की गिनती करना ज़रूरी होता है. लाइबसब के उदाहरणों और टेस्ट में यह माना जाता है कि वे डिवाइस के स्टार्ट-अप के समय गिनती कर सकते हैं और बिना बदलाव किए गड़बड़ी के साथ फ़ेल हो जाते हैं. इसके बजाय, मुझे अपने-आप एक्ज़ीक्यूशन (-s INVOKE_RUN=0) को बंद करना पड़ा और callMain() का मैन्युअल तरीका (-s EXPORTED_RUNTIME_METHODS=...) दिखाना पड़ा.

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

स्क्रीनशॉट में एक Chrome विंडो दिखाई गई है, जिसमें स्थानीय तौर पर दिखाए जाने वाले `testlibub` पेज पर DevTools खुला हुआ है. DevTools कंसोल, `navigator.usb.requestDevice({filter: [] })` की जांच कर रहा है. इससे अनुमति का अनुरोध ट्रिगर हुआ. फ़िलहाल, यह उपयोगकर्ता से ऐसा यूएसबी डिवाइस चुनने के लिए कह रहा है जिसे पेज के साथ शेयर किया जाना चाहिए. फ़िलहाल, ILCE-6600 (Sony कैमरा) को चुना गया है.

अगले चरण का स्क्रीनशॉट, जिसमें DevTools अब भी खुला है. डिवाइस को चुनने के बाद, Console ने नए एक्सप्रेशन `Module.callMain([&#39;-v&#39;])` का आकलन किया है. इससे `testlibub` ऐप्लिकेशन को वर्बोस मोड में चलाया जाता है. आउटपुट में, पहले से कनेक्ट किए गए यूएसबी कैमरे के बारे में पूरी जानकारी दिखती है. जैसे, Sony का प्रॉडक्ट, ILCE-6600 प्रॉडक्ट, सीरियल नंबर, कॉन्फ़िगरेशन वगैरह.

यह इतना आसान नहीं है, लेकिन लाइब्रेरी को किसी नए प्लैटफ़ॉर्म पर पोर्ट करते समय, पहली बार मान्य आउटपुट देने वाले स्टेज पर पहुंचना काफ़ी रोमांचक होता है!

पोर्ट का इस्तेमाल करना

जैसा कि ऊपर बताया गया है, पोर्ट कुछ Emscripten सुविधाओं पर निर्भर है जिन्हें फ़िलहाल ऐप्लिकेशन के लिंकिंग चरण में चालू करने की ज़रूरत है. अगर आप इस लिब्सब पोर्ट का इस्तेमाल अपने ऐप्लिकेशन में करना चाहते हैं, तो आपको ऐसा करना होगा:

  1. सबसे नए libasb को अपने बिल्ड के हिस्से के तौर पर संग्रह के तौर पर डाउनलोड करें या फिर इसे अपने प्रोजेक्ट में गिट सबमॉड्यूल के तौर पर जोड़ें.
  2. autoreconf -fiv को libusb फ़ोल्डर में चलाएं.
  3. क्रॉस-कंपाइलेशन के लिए प्रोजेक्ट शुरू करने और बनाए गए आर्टफ़ैक्ट को जहां रखना है वहां पाथ सेट करने के लिए, emconfigure ./configure –host=wasm32 –prefix=/some/installation/path चलाएं.
  4. emmake make install चलाएं.
  5. पहले चुने गए पाथ में लाइब्सब खोजने के लिए, अपने ऐप्लिकेशन या हाई-लेवल की लाइब्रेरी को पॉइंट करें.
  6. अपने ऐप्लिकेशन के लिंक तर्कों में ये फ़्लैग जोड़ें: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

फ़िलहाल, इस लाइब्रेरी की कुछ सीमाएं हैं:

  • ट्रांसफ़र रद्द करने की सुविधा उपलब्ध नहीं है. यह WebUSB की एक सीमा है, जो कि लाइबसब में क्रॉस-प्लैटफ़ॉर्म ट्रांसफ़र रद्द न होने की वजह से होती है.
  • कोई आइसोक्रोनस ट्रांसफ़र सपोर्ट नहीं करता. मौजूदा ट्रांसफ़र मोड को उदाहरण के तौर पर इस्तेमाल करके, इसे जोड़ना मुश्किल नहीं होना चाहिए. हालांकि, यह मोड बहुत कम काम करता है और इसकी जांच करने के लिए मेरे पास कोई डिवाइस नहीं है. इसलिए, फ़िलहाल मैंने इसे काम नहीं किया. अगर आपके पास ऐसे डिवाइस हैं और आप लाइब्रेरी में योगदान देना चाहते हैं, तो लोगों का स्वागत है!
  • पहले में क्रॉस-प्लैटफ़ॉर्म की सीमाओं के बारे में बताया गया था. ये सीमाएं ऑपरेटिंग सिस्टम की वजह से लागू होती हैं. इसलिए, हम यहां ज़्यादा काम नहीं कर सकते. बस उपयोगकर्ताओं को ड्राइवर या अनुमतियों को बदलने के लिए कहें. हालांकि, अगर एचआईडी या सीरियल डिवाइसों को पोर्ट किया जा रहा है, तो लाइबसब के उदाहरण का इस्तेमाल करके, किसी दूसरी लाइब्रेरी को किसी अन्य Fugu API में पोर्ट किया जा सकता है. उदाहरण के लिए, सी लाइब्रेरी hidapi को WebHID पर पोर्ट किया जा सकता है और लो-लेवल यूएसबी ऐक्सेस से जुड़ी समस्याओं को साइड-स्टेप किया जा सकता है.

नतीजा

इस पोस्ट में, मैंने दिखाया है कि कैसे Emscripten, Asyncify, और Fugu API की मदद से, libub जैसी लो-लेवल की लाइब्रेरी को कुछ इंटिग्रेशन तरीकों की मदद से वेब पर पोर्ट किया जा सकता है.

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

अगली पोस्ट में, मैं वेब gPhotos2 डेमो बनाने के उन चरणों के बारे में बताऊँगी जो न सिर्फ़ डिवाइस की जानकारी इकट्ठा करते हैं, बल्कि लिब्सब की ट्रांसफ़र सुविधा का भी बड़े पैमाने पर इस्तेमाल करते हैं. इस बीच, हमें उम्मीद है कि आपको बेहतरीन उदाहरण से प्रेरणा मिली होगी. साथ ही, आप डेमो का इस्तेमाल करके, लाइब्रेरी के साथ खेलें या शायद आगे बढ़ें और ज़्यादातर इस्तेमाल होने वाली किसी दूसरी लाइब्रेरी को Fugu API के साथ पोर्ट करें.