การพอร์ตแอปพลิเคชัน 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 ports" และ "libgphoto2 camlibs" แบบไดนามิก สุดท้าย "พอร์ต libgphoto2" จะขึ้นอยู่กับ "libusb Fork" ในเชิงสถิติ

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

แต่วิธีที่ง่ายกว่าคือการสร้างโฟลเดอร์แยกต่างหากเป็นรากของระบบที่กำหนดเอง (มักจะย่อลงเป็น "sysroot") และชี้ระบบบิลด์ที่เกี่ยวข้องทั้งหมดไปที่โฟลเดอร์นั้น วิธีนี้จะทำให้ไลบรารีแต่ละรายการทั้งค้นหาไลบรารีที่ต้องพึ่งพาใน 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 ();

แนวทางนี้บนเว็บมีปัญหาอยู่ 2-3 อย่าง ดังนี้

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

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

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

"Libtool ให้การรองรับพิเศษสําหรับการ dlopen ออบเจ็กต์ libtool และไฟล์ไลบรารี libtool เพื่อให้สามารถแก้ไขสัญลักษณ์ได้แม้ในแพลตฟอร์มที่ไม่มีฟังก์ชัน dlopen และ dlsym

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

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

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

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

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

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

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