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

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

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

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

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

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

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

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

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

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

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

Emscripten มี sysroot ของตัวเองอยู่แล้วภายใต้ (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 ();

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

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

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

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

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

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

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

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

แผนภาพทรัพยากร Dependency ที่อัปเดตใหม่มีลักษณะดังนี้ และทุกอย่างเชื่อมโยงกันแบบคงที่

แผนภาพแสดง "แอป" ขึ้นอยู่กับ "libgphoto2 fork" ซึ่งขึ้นอยู่กับ "libtool" "libtool" ขึ้นอยู่กับ "พอร์ต: libusb1" และ "camlibs: libptp2" "พอร์ต: 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>
// …

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

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

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

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

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

คุณจะค้นหาชื่อ ประเภท องค์ประกอบย่อย และพร็อพเพอร์ตี้อื่นๆ ที่เกี่ยวข้องทั้งหมดของวิดเจ็ตแต่ละรายการได้ (และในกรณีที่มีการแก้ไขค่า ก็จะค้นหาได้ด้วย) ผ่าน 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 การตั้งค่าแสดงข้อมูลล่าสุดเสมอ และขณะเดียวกันก็ส่งคําสั่งไปยังกล้องเมื่อใดก็ตามที่ผู้ใช้แก้ไขช่องใดช่องหนึ่ง

Preact จัดการความแตกต่างของผลลัพธ์และอัปเดต 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 แล้วถ่ายโอนไปยัง Canvas ในเฟรมภาพเคลื่อนไหวถัดไป

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 ที่ทันสมัยเหล่านี้ทำให้มั่นใจได้ว่าการถอดรหัสทั้งหมดจะดำเนินการในเบื้องหลัง และผืนผ้าใบจะได้รับการอัปเดตเฉพาะเมื่อทั้งรูปภาพและเบราว์เซอร์มีการเตรียมการสำหรับการวาดภาพอย่างสมบูรณ์แล้วเท่านั้น ทำให้แล็ปท็อปของฉันได้ 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());

หลังจากนั้น การดำเนินการทั้งหมดก็ประสบผลสำเร็จโดยไม่มีข้อขัดแย้ง

บทสรุป

เรียกดูฐานของโค้ดใน GitHub เพื่อดูข้อมูลเชิงลึกเกี่ยวกับการใช้งานเพิ่มเติม และผมต้องขอขอบคุณ Marcus Meissner สำหรับการบำรุงรักษา gPhoto2 และสำหรับรีวิวเกี่ยวกับ PR ต้นทางของฉัน

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