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

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

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

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

การชี้ระบบบิลด์ไปยัง Fork ที่กําหนดเอง

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

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

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

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

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

แผนภาพความเกี่ยวข้องที่อัปเดตแล้วมีลักษณะดังนี้เมื่อลิงก์ทุกอย่างแบบคงที่ไว้ด้วยกัน

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

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 ต้นทางของเรา

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