@virtualdisplay.io/client 3.5.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,8 @@ const ERROR_CODES = {
4
4
  VALUE_NOT_FOUND: "VALUE_NOT_FOUND",
5
5
  INVALID_MAPPING: "INVALID_MAPPING",
6
6
  PARENT_NOT_FOUND: "PARENT_NOT_FOUND",
7
- INVALID_MUTATIONS: "INVALID_MUTATIONS"
7
+ INVALID_MUTATIONS: "INVALID_MUTATIONS",
8
+ INVALID_PARAMETER: "INVALID_PARAMETER"
8
9
  };
9
10
  class VirtualdisplayError extends Error {
10
11
  code;
@@ -61,7 +62,10 @@ const EVENT_NAMES = {
61
62
  // State flow (Server → Client)
62
63
  MESSAGE_RECEIVED: "message:received",
63
64
  STATE_CHANGED: "state:changed",
64
- INITIAL_STATE_RECEIVED: "state:initial-received"
65
+ INITIAL_STATE_RECEIVED: "state:initial-received",
66
+ // Snapshot flow
67
+ SNAPSHOT_MESSAGE: "snapshot:take",
68
+ SNAPSHOT_DEVELOPED: "snapshot:developed"
65
69
  };
66
70
  const g = Object.freeze({
67
71
  debug: 0,
@@ -6931,7 +6935,8 @@ const MESSAGE_TYPES = {
6931
6935
  MUTATION: "mutation",
6932
6936
  STATE: "state",
6933
6937
  CONFIG: "config",
6934
- CAMERA: "camera"
6938
+ CAMERA: "camera",
6939
+ SNAPSHOT: "snapshot"
6935
6940
  };
6936
6941
  function createMutationMessage(mutations) {
6937
6942
  if (!Array.isArray(mutations)) {
@@ -6943,14 +6948,7 @@ function createMutationMessage(mutations) {
6943
6948
  };
6944
6949
  }
6945
6950
  function isValidMessage(data) {
6946
- return isMutationMessage(data) || isStateMessage(data) || isConfigMessage(data);
6947
- }
6948
- function isMutationMessage(data) {
6949
- if (typeof data !== "object" || data === null) {
6950
- return false;
6951
- }
6952
- const obj = data;
6953
- return obj.type === MESSAGE_TYPES.MUTATION && Array.isArray(obj.mutations);
6951
+ return isStateMessage(data) || isSnapshotDevelopedMessage(data);
6954
6952
  }
6955
6953
  function isStateMessage(data) {
6956
6954
  if (typeof data !== "object" || data === null) {
@@ -6959,12 +6957,12 @@ function isStateMessage(data) {
6959
6957
  const obj = data;
6960
6958
  return obj.type === MESSAGE_TYPES.STATE && Array.isArray(obj.nodes);
6961
6959
  }
6962
- function isConfigMessage(data) {
6960
+ function isSnapshotDevelopedMessage(data) {
6963
6961
  if (typeof data !== "object" || data === null) {
6964
6962
  return false;
6965
6963
  }
6966
6964
  const obj = data;
6967
- return obj.type === MESSAGE_TYPES.CONFIG && typeof obj.config === "object" && obj.config !== null;
6965
+ return obj.type === MESSAGE_TYPES.SNAPSHOT && typeof obj.filename === "string" && typeof obj.data === "string";
6968
6966
  }
6969
6967
  class AttributeService {
6970
6968
  attributes = /* @__PURE__ */ new Map();
@@ -7227,11 +7225,13 @@ class MessageHandler {
7227
7225
  const outgoingEvents = [
7228
7226
  EVENT_NAMES.MUTATION_MESSAGE,
7229
7227
  EVENT_NAMES.CONFIG_MESSAGE,
7230
- EVENT_NAMES.CAMERA_MESSAGE
7228
+ EVENT_NAMES.CAMERA_MESSAGE,
7229
+ EVENT_NAMES.SNAPSHOT_MESSAGE
7231
7230
  ];
7232
7231
  outgoingEvents.forEach((eventName) => {
7233
7232
  this.eventBus.on(eventName, (event) => {
7234
- this.queueOrSend(event.message);
7233
+ const messageEvent = event;
7234
+ this.queueOrSend(messageEvent.message);
7235
7235
  });
7236
7236
  });
7237
7237
  }
@@ -7419,6 +7419,156 @@ class StateService {
7419
7419
  this.hasReceivedInitialSync = false;
7420
7420
  }
7421
7421
  }
7422
+ class Photo {
7423
+ constructor(filename) {
7424
+ this.filename = filename;
7425
+ }
7426
+ callbacks = /* @__PURE__ */ new Set();
7427
+ developed = false;
7428
+ photoData = null;
7429
+ /**
7430
+ * Register a callback for when the photo is developed
7431
+ * If the photo is already developed, the callback is called immediately
7432
+ *
7433
+ * @param callback Function to call with photo data
7434
+ * @returns This Photo instance for chaining
7435
+ */
7436
+ onDeveloped(callback) {
7437
+ if (this.developed && this.photoData !== null) {
7438
+ callback(this.photoData);
7439
+ return this;
7440
+ }
7441
+ this.callbacks.add(callback);
7442
+ return this;
7443
+ }
7444
+ /**
7445
+ * Get the filename for this photo
7446
+ */
7447
+ getFilename() {
7448
+ return this.filename;
7449
+ }
7450
+ /**
7451
+ * Mark the photo as developed and notify all callbacks
7452
+ * This method is called by the SnapshotMessageHandler when the photo data arrives
7453
+ */
7454
+ develop(photoData) {
7455
+ if (this.developed) {
7456
+ return;
7457
+ }
7458
+ this.photoData = photoData;
7459
+ this.developed = true;
7460
+ this.callbacks.forEach((callback) => {
7461
+ callback(this.photoData);
7462
+ });
7463
+ this.callbacks.clear();
7464
+ }
7465
+ }
7466
+ class SnapshotMessageHandler {
7467
+ constructor(eventBus) {
7468
+ this.eventBus = eventBus;
7469
+ logger.debug("SnapshotMessageHandler initialized");
7470
+ this.setupMessageListener();
7471
+ }
7472
+ pendingPhotos = /* @__PURE__ */ new Map();
7473
+ /**
7474
+ * Register a photo to be developed when its data arrives
7475
+ */
7476
+ registerPhoto(photo) {
7477
+ logger.debug("Registering photo for development", { filename: photo.getFilename() });
7478
+ this.pendingPhotos.set(photo.getFilename(), photo);
7479
+ logger.debug("Pending photos count", { count: this.pendingPhotos.size });
7480
+ }
7481
+ setupMessageListener() {
7482
+ logger.debug("Setting up message listener for snapshot messages");
7483
+ this.eventBus.on(EVENT_NAMES.MESSAGE_RECEIVED, (event) => {
7484
+ const { message } = event;
7485
+ if (message.type === MESSAGE_TYPES.SNAPSHOT) {
7486
+ logger.debug("Processing snapshot message");
7487
+ this.handleSnapshotDeveloped(message);
7488
+ }
7489
+ });
7490
+ }
7491
+ handleSnapshotDeveloped(message) {
7492
+ logger.debug("Handling snapshot developed message", {
7493
+ filename: message.filename,
7494
+ dataLength: message.data.length,
7495
+ pendingCount: this.pendingPhotos.size
7496
+ });
7497
+ const photo = this.pendingPhotos.get(message.filename);
7498
+ if (photo !== void 0) {
7499
+ logger.debug("Found pending photo, developing...", { filename: message.filename });
7500
+ const photoData = {
7501
+ filename: message.filename,
7502
+ data: message.data
7503
+ };
7504
+ photo.develop(photoData);
7505
+ this.pendingPhotos.delete(message.filename);
7506
+ logger.debug("Photo developed and removed from pending", {
7507
+ filename: message.filename,
7508
+ remainingCount: this.pendingPhotos.size
7509
+ });
7510
+ return;
7511
+ }
7512
+ logger.warn("No pending photo found for filename", {
7513
+ filename: message.filename,
7514
+ pendingFilenames: Array.from(this.pendingPhotos.keys())
7515
+ });
7516
+ }
7517
+ }
7518
+ class Snapshot {
7519
+ constructor(eventBus) {
7520
+ this.eventBus = eventBus;
7521
+ this.messageHandler = new SnapshotMessageHandler(eventBus);
7522
+ }
7523
+ validExtensions = [".png", ".jpg", ".jpeg", ".webp"];
7524
+ messageHandler;
7525
+ /**
7526
+ * Take a snapshot of the current 3D model view
7527
+ *
7528
+ * @param filename The filename for the snapshot (must include valid extension)
7529
+ * @returns Photo object for registering callbacks
7530
+ * @throws VirtualdisplayError if filename is invalid
7531
+ */
7532
+ take(filename) {
7533
+ this.validateFilename(filename);
7534
+ logger.debug("Taking snapshot", { filename });
7535
+ const photo = new Photo(filename);
7536
+ this.messageHandler.registerPhoto(photo);
7537
+ this.eventBus.emit(EVENT_NAMES.SNAPSHOT_MESSAGE, {
7538
+ message: {
7539
+ type: MESSAGE_TYPES.SNAPSHOT,
7540
+ filename
7541
+ }
7542
+ });
7543
+ return photo;
7544
+ }
7545
+ /**
7546
+ * Validate filename has a valid image extension
7547
+ */
7548
+ validateFilename(filename) {
7549
+ if (filename === "" || typeof filename !== "string") {
7550
+ throw new VirtualdisplayError(
7551
+ "Filename must be a non-empty string",
7552
+ ERROR_CODES.INVALID_PARAMETER
7553
+ );
7554
+ }
7555
+ const hasValidExtension = this.validExtensions.some(
7556
+ (ext) => filename.toLowerCase().endsWith(ext)
7557
+ );
7558
+ if (!hasValidExtension) {
7559
+ throw new VirtualdisplayError(
7560
+ `Invalid filename: "${filename}". Filename must end with one of: ${this.validExtensions.join(", ")}`,
7561
+ ERROR_CODES.INVALID_PARAMETER
7562
+ );
7563
+ }
7564
+ if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
7565
+ throw new VirtualdisplayError(
7566
+ 'Filename cannot contain path separators or ".."',
7567
+ ERROR_CODES.INVALID_PARAMETER
7568
+ );
7569
+ }
7570
+ }
7571
+ }
7422
7572
  class VirtualdisplayViewerService {
7423
7573
  eventBus;
7424
7574
  constructor(eventBus) {
@@ -7512,6 +7662,7 @@ class VirtualdisplayClient {
7512
7662
  messageHandler;
7513
7663
  viewerService;
7514
7664
  cameraApi;
7665
+ snapshotApi;
7515
7666
  options;
7516
7667
  iframe;
7517
7668
  constructor(options) {
@@ -7522,6 +7673,7 @@ class VirtualdisplayClient {
7522
7673
  this.messageHandler = new MessageHandler(this.eventBus);
7523
7674
  this.viewerService = new VirtualdisplayViewerService(this.eventBus);
7524
7675
  this.cameraApi = new Camera(this.eventBus);
7676
+ this.snapshotApi = new Snapshot(this.eventBus);
7525
7677
  this.initializeLogging();
7526
7678
  this.createAndConnectIframe();
7527
7679
  this.setupInitialConfig();
@@ -7621,6 +7773,12 @@ class VirtualdisplayClient {
7621
7773
  get camera() {
7622
7774
  return this.cameraApi;
7623
7775
  }
7776
+ /**
7777
+ * Get the snapshot API for taking screenshots
7778
+ */
7779
+ get snapshot() {
7780
+ return this.snapshotApi;
7781
+ }
7624
7782
  destroy() {
7625
7783
  logger.info("Destroying Virtualdisplay client");
7626
7784
  this.attributeService.clear();
@@ -7645,6 +7803,8 @@ export {
7645
7803
  Mutation,
7646
7804
  NODE_TYPES,
7647
7805
  NodeSelector,
7806
+ Photo,
7807
+ Snapshot,
7648
7808
  VirtualdisplayClient,
7649
7809
  VirtualdisplayError,
7650
7810
  VirtualdisplayViewerService,