การพอร์ตแอปพลิเคชัน USB ไปยังเว็บ ส่วนที่ 2: gPhoto2

ดูวิธีพอร์ต gPhoto2 ไปยัง WebAssembly เพื่อควบคุมกล้องภายนอกผ่าน USB จากเว็บแอป

ในโพสต์ก่อนหน้า เราได้แสดงวิธีการพอร์ตไลบรารี libusb เพื่อเรียกใช้บนเว็บด้วย WebAssembly / Emscripten, Asyncify และ WebUSB

นอกจากนั้น เรายังแสดงการสาธิตที่สร้างด้วย gPhoto2 ซึ่งควบคุมกล้อง DSLR และกล้องมิเรอร์ผ่าน USB จากเว็บแอปพลิเคชันได้ด้วย ในโพสต์นี้ เราจะเจาะลึกรายละเอียดทางเทคนิคเบื้องหลังพอร์ต gPhoto2

ชี้ระบบบิลด์ไปยังส้อมที่กำหนดเอง

เนื่องจากฉันกำหนดเป้าหมาย WebAssembly จึงไม่สามารถใช้ libusb และ libgphoto2 จากการกระจายของระบบ แต่ฉันต้องการให้แอปพลิเคชันใช้ส้อมที่กำหนดเองของ libgphoto2 ส่วนส้อมของ libgphoto2 ต้องใช้ส้อมที่กำหนดเองของ libgphoto2

นอกจากนี้ libgphoto2 ใช้ libgtool ในการโหลดปลั๊กอินแบบไดนามิก และถึงแม้ฉันจะไม่ต้องแยก libtool เหมือนอีก 2 ไลบรารี แต่ฉันก็ยังต้องสร้าง libgphoto แล้วชี้ libgphoto2 ไปยังบิลด์ที่กำหนดเองนั้นแทนแพ็กเกจระบบ

ต่อไปนี้เป็นแผนภาพแสดงการขึ้นต่อกันโดยประมาณ (เส้นประหมายถึงการลิงก์แบบไดนามิก)

แผนภาพแสดง "แอป" ตาม "libgphoto2 Fork" ซึ่งขึ้นอยู่กับ "libtool" การบล็อก "libtool" ขึ้นอยู่กับ "พอร์ต libgphoto2" และ "libgphoto2 camlibs" แบบไดนามิก สุดท้าย "พอร์ต libgphoto2" จะขึ้นอยู่กับ "ส้อม Libusb"

ระบบบิลด์ที่อิงตามการกำหนดค่าส่วนใหญ่ รวมถึงระบบที่ใช้ในไลบรารีเหล่านี้อนุญาตให้ลบล้างเส้นทางสำหรับทรัพยากร Dependency ผ่านแฟล็กต่างๆ นี่คือสิ่งที่ฉันลองทำเป็นอันดับแรก อย่างไรก็ตาม เมื่อกราฟทรัพยากร Dependency มีความซับซ้อน รายการการลบล้างเส้นทางสำหรับทรัพยากร Dependency ของไลบรารีแต่ละรายการจะกลายเป็นข้อมูลแบบละเอียดและเกิดข้อผิดพลาดได้ง่าย ฉันยังพบข้อบกพร่องที่ระบบไม่ได้เตรียมการสำหรับทรัพยากร Dependency ในการใช้เส้นทางที่ไม่เป็นไปตามมาตรฐาน

แต่วิธีที่ง่ายกว่าคือการสร้างโฟลเดอร์แยกเป็นรูทระบบที่กำหนดเอง (มักจะย่อเป็น "sysroot") และชี้ระบบบิลด์ที่เกี่ยวข้องทั้งหมดไปที่โฟลเดอร์ดังกล่าว เมื่อใช้วิธีนี้ ไลบรารีแต่ละรายการจะค้นหาทรัพยากร Dependency ใน sysroot ที่ระบุระหว่างบิลด์ และจะติดตั้งตัวเองใน sysroot เดียวกันเพื่อให้ผู้ใช้รายอื่นค้นหาได้ง่ายขึ้น

Emscripten มี Syroot อยู่แล้วภายใต้ (path to emscripten cache)/sysroot ซึ่งใช้สำหรับไลบรารีระบบ พอร์ต Emscripten และเครื่องมือต่างๆ เช่น CMake และ pkg-config ฉันเลือกที่จะใช้ sysroot เดิมสำหรับทรัพยากร Dependency ด้วย

# 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 ในแต่ละทรัพยากร Dependency เท่านั้น โดยต้องติดตั้งภายใต้ sysroot จากนั้นไลบรารีก็ค้นหากันและกันโดยอัตโนมัติ

การจัดการกับการโหลดแบบไดนามิก

ดังที่กล่าวไว้ข้างต้น libgphoto2 ใช้ libtool เพื่อแจกแจงและโหลดอะแดปเตอร์พอร์ต I/O และไลบรารีกล้องแบบไดนามิก ตัวอย่างเช่น โค้ดสำหรับการโหลดไลบรารี I/O มีลักษณะดังนี้

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

วิธีการนี้บนเว็บมีปัญหา 2-3 ประการดังนี้

  • ไม่มีการรองรับมาตรฐานสําหรับการลิงก์แบบไดนามิกของโมดูล WebAssembly Emscripten มีการติดตั้งใช้งานแบบกำหนดเองที่สามารถจำลอง dlopen() API ที่ libtool ใช้งาน แต่กำหนดให้คุณต้องสร้างโมดูล "main" และ "side" ที่มีแฟล็กที่แตกต่างกัน และสำหรับ dlopen() โดยเฉพาะ จะต้องโหลดโมดูลด้านข้างล่วงหน้าในระบบไฟล์จำลองในระหว่างการเริ่มต้นแอปพลิเคชันด้วย การผสานรวม Flag และปรับแต่งดังกล่าวลงในระบบบิลด์ Autoconf ที่มีอยู่ซึ่งมีไลบรารีแบบไดนามิกจำนวนมากอาจทําได้ยาก
  • แม้ว่าจะมีการใช้งาน dlopen() เอง แต่ก็ไม่สามารถแจกแจงไลบรารีแบบไดนามิกทั้งหมดในบางโฟลเดอร์บนเว็บได้ เนื่องจากเซิร์ฟเวอร์ HTTP ส่วนใหญ่จะไม่แสดงรายการไดเรกทอรีด้วยเหตุผลด้านความปลอดภัย
  • การลิงก์ไลบรารีแบบไดนามิกในบรรทัดคำสั่งแทนการระบุในรันไทม์อาจทำให้เกิดปัญหาได้ เช่น ปัญหาสัญลักษณ์ซ้ำกัน ซึ่งเกิดจากความแตกต่างระหว่างการนำเสนอไลบรารีที่ใช้ร่วมกันใน Emscripten และในแพลตฟอร์มอื่นๆ

คุณสามารถปรับระบบบิลด์ให้เข้ากับความแตกต่างเหล่านั้นและฮาร์ดโค้ดรายการปลั๊กอินแบบไดนามิกไว้ที่ใดก็ได้ระหว่างการสร้าง แต่วิธีที่ง่ายขึ้นในการแก้ปัญหาเหล่านั้นทั้งหมดคือหลีกเลี่ยงการเริ่มต้นการลิงก์แบบไดนามิก

แล้ว libtool ก็ตัดวิธีลิงก์แบบไดนามิกออกมามากมายในแพลตฟอร์มต่างๆ และรองรับการเขียนตัวโหลดที่กำหนดเองสำหรับผู้อื่น หนึ่งในตัวโหลดในตัวที่รองรับเรียกว่า "Dlpreopening"

"Libtool ให้การสนับสนุนพิเศษสำหรับ dlopening libtool ออบเจ็กต์และไฟล์ไลบรารี libtool ดังนั้น จึงสามารถค้นหาสัญลักษณ์ของสัญลักษณ์ได้แม้ในแพลตฟอร์มที่ไม่มีฟังก์ชัน dlopen และ dlsym
...
Libtool จำลอง -dlopen ในแพลตฟอร์มแบบคงที่โดยลิงก์ออบเจ็กต์เข้ากับโปรแกรมในเวลาที่คอมไพล์ และสร้างโครงสร้างข้อมูลที่แสดงถึงตารางสัญลักษณ์ของโปรแกรม หากต้องการใช้ฟีเจอร์นี้ คุณต้องประกาศออบเจ็กต์ที่ต้องการให้แอปพลิเคชัน dlopen โดยใช้แฟล็ก -dlopen หรือ -dlpreopen เมื่อลิงก์โปรแกรม (ดูโหมดลิงก์)"

กลไกนี้ทำให้สามารถจำลองการโหลดแบบไดนามิกที่ระดับ libtool แทน Emscripten พร้อมกับลิงก์ทุกอย่างแบบคงที่ไว้ในไลบรารีเดียว

ปัญหาเดียวที่แก้ปัญหาไม่ได้คือการนับไลบรารีแบบไดนามิก รายการเหล่านั้นยังคงต้องมีการฮาร์ดโค้ดไว้ที่ใดที่หนึ่ง โชคดีที่ชุดปลั๊กอินที่ฉันต้องการสำหรับแอปนั้นมีปริมาณน้อยที่สุด:

  • ในส่วนของพอร์ต ฉันสนใจเฉพาะการเชื่อมต่อกล้องที่ใช้ Libusb เท่านั้น ไม่ใช่เรื่อง PTP/IP, การเข้าถึงอนุกรม หรือโหมดไดรฟ์ USB
  • ในฝั่ง Camlibs มีปลั๊กอินเฉพาะผู้ให้บริการจำนวนมากที่อาจมีฟังก์ชันพิเศษบางอย่าง แต่สำหรับการควบคุมและการจับภาพการตั้งค่าทั่วไปนั้นก็เพียงพอแล้วที่จะใช้โปรโตคอลการถ่ายโอนภาพซึ่งมีสัญลักษณ์เป็น ptp2 camlib และรองรับโดยกล้องทุกตัวในตลาด

แผนภาพแสดงทรัพยากร Dependency ที่อัปเดตแล้วและทุกอย่างลิงก์กันแบบคงที่

แผนภาพแสดง "แอป" ตาม "libgphoto2 Fork" ซึ่งขึ้นอยู่กับ "libtool" "libtool" ขึ้นอยู่กับ "พอร์ต: 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 โดยมีทั้ง 2 ไฟล์เป็นแฟล็กลิงก์สำหรับไฟล์สั่งการทั้งหมด (เช่น การทดสอบ และแอปเดโมของฉัน) ดังนี้

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

รูปแบบการตั้งชื่อนี้ยังป้องกันไม่ให้ชื่อขัดแย้งกันในกรณีที่ฉันตัดสินใจลิงก์ปลั๊กอินเฉพาะกล้องในแอปเดียวกันในอนาคต

หลังจากนำการเปลี่ยนแปลงทั้งหมดนี้ไปใช้แล้ว ผมก็สามารถสร้างแอปพลิเคชันทดสอบและโหลดปลั๊กอินได้

กำลังสร้าง UI การตั้งค่า

gPhoto2 ช่วยให้ไลบรารีกล้องสามารถกำหนดการตั้งค่าของตนเองในรูปแบบแผนผังวิดเจ็ต ลำดับชั้นของประเภทวิดเจ็ตมีดังนี้

  • หน้าต่าง - คอนเทนเนอร์การกำหนดค่าระดับบนสุด
    • ส่วน - กลุ่มที่มีชื่อของวิดเจ็ตอื่นๆ
    • ช่องของปุ่ม
    • ช่องข้อความ
    • ช่องตัวเลข
    • ช่องวันที่
    • สลับ
    • ปุ่มตัวเลือก

คุณจะค้นหาชื่อ ประเภท ระดับลูก และพร็อพเพอร์ตี้ที่เกี่ยวข้องอื่นๆ ทั้งหมดของวิดเจ็ตแต่ละรายการได้ (และในกรณีของค่า ก็จะแก้ไขได้ด้วย) ผ่าน Exposed C API ซึ่งทั้ง 2 อย่างเป็นพื้นฐานในการสร้าง UI การตั้งค่าโดยอัตโนมัติในภาษาใดๆ ก็ตามที่โต้ตอบกับภาษา C ได้

คุณเปลี่ยนการตั้งค่าได้ทุกเมื่อผ่าน gPhoto2 หรือในกล้องเอง นอกจากนี้ วิดเจ็ตบางอย่างเป็นแบบอ่านอย่างเดียว และแม้แต่สถานะอ่านอย่างเดียวเองก็ขึ้นอยู่กับโหมดกล้องและการตั้งค่าอื่นๆ ด้วย เช่น ความเร็วชัตเตอร์คือช่องตัวเลขที่เขียนได้ใน M (โหมดแมนวล) แต่กลายเป็นช่องให้ข้อมูลแบบอ่านอย่างเดียวใน P (โหมดโปรแกรม) ในโหมด P ค่าความเร็วชัตเตอร์จะเป็นแบบไดนามิกและเปลี่ยนแปลงอย่างต่อเนื่องโดยขึ้นอยู่กับความสว่างของฉากที่กล้องกำลังดูอยู่

โดยสรุปแล้ว คุณจะต้องแสดงข้อมูลที่เป็นปัจจุบันจากกล้องที่เชื่อมต่อใน UI เสมอ พร้อมกับอนุญาตให้ผู้ใช้แก้ไขการตั้งค่าจาก UI เดียวกันไปพร้อมๆ กัน การรับส่งข้อมูลแบบ 2 ทิศทางเช่นนี้มีความซับซ้อนมากกว่า

gPhoto2 ไม่มีกลไกในการเรียกเฉพาะการตั้งค่าที่เปลี่ยนแปลง แต่มีเพียงแผนผังทั้งหมดหรือวิดเจ็ตแต่ละรายการ เราต้องการวิธีที่จะแยกความแตกต่างของแผนผังวิดเจ็ตระหว่างคำขอและอัปเดตเฉพาะพร็อพเพอร์ตี้ UI ที่เปลี่ยนแปลงเพื่อให้ UI เป็นปัจจุบันโดยไม่กะพริบและไม่สะดุด โชคดีที่ปัญหานี้ได้รับการแก้ไขบนเว็บแล้ว และเป็นฟังก์ชันหลักของเฟรมเวิร์ก เช่น 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 ที่ส่งคืนของโครงสร้างการตั้งค่า และสร้าง UI ผ่านฟังก์ชัน 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;
  }
  // …

การเรียกใช้ฟังก์ชันนี้ซ้ำๆ ในลูปเหตุการณ์ที่ไม่สิ้นสุด ทำให้ UI การตั้งค่าแสดงข้อมูลล่าสุดเสมอ ในขณะเดียวกันก็ส่งคำสั่งไปยังกล้องทุกครั้งที่ผู้ใช้แก้ไขช่องใดช่องหนึ่ง

การดำเนินการล่วงหน้าสามารถช่วยกระจายผลลัพธ์และอัปเดต DOM เฉพาะสำหรับส่วนเล็กๆ ที่เปลี่ยนแปลงของ UI โดยไม่ขัดจังหวะการโฟกัสหน้าเว็บหรือสถานะการแก้ไข ปัญหาหนึ่งที่ยังหลงเหลืออยู่คือการรับส่งข้อมูลแบบ 2 ทิศทาง เฟรมเวิร์กอย่าง React และ Preact ออกแบบให้มีการรับส่งข้อมูลแบบไปในทิศทางเดียวเพราะช่วยให้อธิบายข้อมูลและเปรียบเทียบระหว่างการแสดงผลซ้ำได้ง่ายขึ้นมาก แต่ฉันทำลายความคาดหวังดังกล่าวด้วยการอนุญาตให้แหล่งข้อมูลภายนอก ซึ่งก็คือกล้องอัปเดต UI การตั้งค่าได้ทุกเมื่อ

เราแก้ปัญหานี้โดยเลือกไม่ใช้การอัปเดต UI สำหรับช่องป้อนข้อมูลที่ผู้ใช้กำลังแก้ไขอยู่ ดังนี้

/**
 * 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 รองรับการสตรีมวิดีโอจากกล้องไปยังไฟล์ที่จัดเก็บไว้ในเครื่องหรือส่งไปยังเว็บแคมเสมือนโดยตรงเช่นเดียวกับเครื่องมืออย่างเป็นทางการ ฉันต้องการใช้ฟีเจอร์ดังกล่าวเพื่อให้ดูภาพสดในการสาธิต แต่แม้ว่าจะมีอยู่ในยูทิลิตีคอนโซล แต่ฉันไม่พบวิดีโอในไลบรารี API ของ 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 ซึ่งส่งผ่านไปยัง API ของเว็บอื่นๆ ได้ง่ายขึ้น ดังนี้

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

การใช้ API ที่ทันสมัยเหล่านี้จะช่วยให้การถอดรหัสทั้งหมดเสร็จสิ้นในเบื้องหลัง และ Canvas จะได้รับการอัปเดตเฉพาะเมื่อทั้งรูปภาพและเบราว์เซอร์มีความพร้อมในการวาดแล้วเท่านั้น ซึ่งได้ความละเอียด 30+ FPS ในแล็ปท็อปของฉัน ซึ่งสอดคล้องกับประสิทธิภาพในเครื่องของทั้ง gPhoto2 และซอฟต์แวร์ Sony อย่างเป็นทางการ

กำลังซิงค์ข้อมูลการเข้าถึง USB

เมื่อมีการส่งคำขอโอนข้อมูลทาง USB ขณะที่กำลังดำเนินการอื่นอยู่ โดยทั่วไปจะเกิดข้อผิดพลาด "อุปกรณ์ไม่ว่าง" เนื่องจากการแสดงตัวอย่างและ UI การตั้งค่ามีการอัปเดตเป็นประจำ และผู้ใช้อาจกำลังพยายามจับภาพหรือแก้ไขการตั้งค่าในเวลาเดียวกัน ความขัดแย้งระหว่างการดำเนินการที่แตกต่างกันดังกล่าวจึงเกิดขึ้นบ่อยมาก

ฉันจำเป็นต้องซิงค์การเข้าถึงทั้งหมดภายในแอปพลิเคชันเพื่อหลีกเลี่ยงปัญหานี้ ด้วยเหตุนี้ เราจึงได้สร้างคิวแบบไม่พร้อมกันที่อิงตามคำสัญญาไว้ดังนี้

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

การห่วงโซ่การดำเนินการแต่ละรายการในโค้ดเรียกกลับ then() ของสัญญา queue ที่มีอยู่ และการจัดเก็บผลลัพธ์ที่เชื่อมโยงเป็นค่าใหม่ของ queue จะทำให้มั่นใจได้ว่าการดำเนินการทั้งหมดจะดำเนินการทีละรายการตามลำดับโดยไม่มีการซ้อนทับ

ข้อผิดพลาดด้านการดำเนินการใดๆ จะถูกส่งคืนให้กับผู้โทร ขณะที่ข้อผิดพลาดที่สำคัญ (ที่ไม่คาดคิด) จะระบุว่าทั้งห่วงโซ่เป็นสัญญาที่ถูกปฏิเสธ และดูแลให้แน่ใจว่าจะไม่มีการกำหนดเวลาการดำเนินการใหม่ใดๆ อีกหลังจากนั้น

การจัดบริบทของโมดูลไว้ในตัวแปรส่วนตัว (ไม่ได้ส่งออก) จะช่วยลดความเสี่ยงจากการเข้าถึง context โดยไม่ได้ตั้งใจในส่วนอื่นของแอปได้โดยไม่ต้องผ่านการเรียกใช้ schedule()

หากต้องการเชื่อมโยงสิ่งต่างๆ เข้าด้วยกัน ตอนนี้การเข้าถึงบริบทของอุปกรณ์แต่ละรายการต้องรวมอยู่ในการเรียกใช้ schedule() ดังนี้

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

และ

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

หลังจากนั้น การดําเนินการทั้งหมดก็ทํางานสําเร็จโดยไม่มีความขัดแย้ง

บทสรุป

คุณสามารถเรียกดู Codebase ใน GitHub เพื่อดูข้อมูลเชิงลึกเพิ่มเติมเกี่ยวกับการใช้งาน และขอขอบคุณ Marcus Meissner ที่ดูแล gPhoto2 และรีวิวการประชาสัมพันธ์อัปสตรีมของเขา

ตามที่แสดงในโพสต์เหล่านี้ API ของ WebAssembly, Asyncify และ Fugu เป็นเป้าหมายการคอมไพล์ที่มีความสามารถในการรวมได้สำหรับแอปพลิเคชันที่มีความซับซ้อนมากที่สุด โซลูชันเหล่านี้ช่วยให้คุณสามารถนำไลบรารีหรือแอปพลิเคชันที่เคยสร้างไว้บนแพลตฟอร์มเดียว มาถ่ายโอนไปยังเว็บ ซึ่งทำให้พร้อมให้บริการแก่ผู้ใช้ในวงกว้าง ทั้งในเดสก์ท็อปและอุปกรณ์เคลื่อนที่