@talmolab/sleap-io.js 0.1.7 → 0.1.8

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.
package/dist/index.d.ts CHANGED
@@ -18,13 +18,16 @@ declare class Video {
18
18
  backendMetadata: Record<string, unknown>;
19
19
  sourceVideo: Video | null;
20
20
  openBackend: boolean;
21
+ private _embedded;
21
22
  constructor(options: {
22
23
  filename: string | string[];
23
24
  backend?: VideoBackend | null;
24
25
  backendMetadata?: Record<string, unknown>;
25
26
  sourceVideo?: Video | null;
26
27
  openBackend?: boolean;
28
+ embedded?: boolean;
27
29
  });
30
+ get hasEmbeddedImages(): boolean;
28
31
  get originalVideo(): Video | null;
29
32
  get shape(): [number, number, number, number] | null;
30
33
  get fps(): number | null;
@@ -259,6 +262,10 @@ interface StreamingH5Options {
259
262
  /** Filename hint for the HDF5 file. */
260
263
  filenameHint?: string;
261
264
  }
265
+ /**
266
+ * Source types supported by the streaming HDF5 file.
267
+ */
268
+ type StreamingH5Source = string | ArrayBuffer | Uint8Array | File;
262
269
  /**
263
270
  * A streaming HDF5 file handle that uses a Web Worker for range request access.
264
271
  *
@@ -280,12 +287,33 @@ declare class StreamingH5File {
280
287
  */
281
288
  init(options?: StreamingH5Options): Promise<void>;
282
289
  /**
283
- * Open a remote HDF5 file for streaming access.
290
+ * Open a remote HDF5 file for streaming access via URL.
284
291
  *
285
292
  * @param url - URL to the HDF5 file (must support HTTP range requests)
286
293
  * @param options - Optional settings
287
294
  */
288
295
  open(url: string, options?: StreamingH5Options): Promise<void>;
296
+ /**
297
+ * Open a local File object using WORKERFS (zero-copy).
298
+ *
299
+ * @param file - File object from file input or drag-and-drop
300
+ * @param options - Optional settings
301
+ */
302
+ openLocal(file: File, options?: StreamingH5Options): Promise<void>;
303
+ /**
304
+ * Open an HDF5 file from an ArrayBuffer or Uint8Array.
305
+ *
306
+ * @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
307
+ * @param options - Optional settings
308
+ */
309
+ openBuffer(buffer: ArrayBuffer | Uint8Array, options?: StreamingH5Options): Promise<void>;
310
+ /**
311
+ * Open an HDF5 file from any supported source.
312
+ *
313
+ * @param source - URL string, File, ArrayBuffer, or Uint8Array
314
+ * @param options - Optional settings
315
+ */
316
+ openAny(source: StreamingH5Source, options?: StreamingH5Options): Promise<void>;
289
317
  /**
290
318
  * Whether a file is currently open.
291
319
  */
@@ -341,6 +369,29 @@ declare function isStreamingSupported(): boolean;
341
369
  * @returns A StreamingH5File instance
342
370
  */
343
371
  declare function openStreamingH5(url: string, options?: StreamingH5Options): Promise<StreamingH5File>;
372
+ /**
373
+ * Open an HDF5 file from any supported source using a Web Worker.
374
+ *
375
+ * This is the recommended way to open HDF5 files in the browser as it
376
+ * offloads all h5wasm operations to a Web Worker, avoiding main thread blocking.
377
+ *
378
+ * @param source - URL string, File object, ArrayBuffer, or Uint8Array
379
+ * @param options - Optional settings
380
+ * @returns A StreamingH5File instance
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * // From URL
385
+ * const file = await openH5Worker("https://example.com/data.h5");
386
+ *
387
+ * // From File (file input)
388
+ * const file = await openH5Worker(inputElement.files[0]);
389
+ *
390
+ * // From ArrayBuffer
391
+ * const file = await openH5Worker(arrayBuffer);
392
+ * ```
393
+ */
394
+ declare function openH5Worker(source: StreamingH5Source, options?: StreamingH5Options): Promise<StreamingH5File>;
344
395
 
345
396
  /**
346
397
  * Video backend for embedded images in HDF5 files accessed via streaming.
@@ -360,7 +411,7 @@ declare class StreamingHdf5VideoBackend implements VideoBackend {
360
411
  fps?: number;
361
412
  private h5file;
362
413
  private datasetPath;
363
- private frameNumbers;
414
+ private frameNumberToIndex;
364
415
  private format;
365
416
  private channelOrder;
366
417
  private cachedData;
@@ -396,27 +447,36 @@ type OpenH5Options = {
396
447
  /**
397
448
  * Load an SLP file.
398
449
  *
399
- * When loading from a URL in a browser with `h5.stream` set to 'range' or 'auto',
400
- * this function automatically uses HTTP range requests for efficient streaming.
401
- * Only the annotation data needed is downloaded, not the entire file.
450
+ * In browser environments, this function automatically uses a Web Worker for all
451
+ * HDF5 operations, keeping the main thread responsive. For URLs, it uses HTTP
452
+ * range requests to download only the data needed rather than the entire file.
453
+ *
454
+ * In Node.js, this uses the native h5wasm bindings directly.
402
455
  *
403
456
  * @param source - Path, URL, ArrayBuffer, File, or FileSystemFileHandle
404
457
  * @param options - Loading options
405
- * @param options.openVideos - Whether to open video backends (default: true, but false for streaming)
458
+ * @param options.openVideos - Whether to open video backends (default: true)
406
459
  * @param options.h5 - HDF5 options including streaming mode
407
460
  * @param options.h5.stream - 'auto' | 'range' | 'download' (default: 'auto')
408
461
  *
409
462
  * @example
410
463
  * ```typescript
411
- * // Load from URL with streaming (uses range requests automatically)
412
- * const labels = await loadSlp('https://example.com/labels.slp', {
413
- * h5: { stream: 'range' }
414
- * });
464
+ * // Browser: Load from URL (automatically uses Worker + range requests)
465
+ * const labels = await loadSlp('https://example.com/labels.slp');
466
+ *
467
+ * // Browser: Load from file input (automatically uses Worker)
468
+ * const labels = await loadSlp(fileInput.files[0]);
469
+ *
470
+ * // Browser: Load from ArrayBuffer (automatically uses Worker)
471
+ * const labels = await loadSlp(arrayBuffer);
415
472
  *
416
- * // Force full download
473
+ * // Force full download instead of range requests
417
474
  * const labels = await loadSlp('https://example.com/labels.slp', {
418
475
  * h5: { stream: 'download' }
419
476
  * });
477
+ *
478
+ * // Node.js: Load from file path
479
+ * const labels = await loadSlp('/path/to/file.slp');
420
480
  * ```
421
481
  */
422
482
  declare function loadSlp(source: SlpSource, options?: {
@@ -743,28 +803,38 @@ interface StreamingSlpOptions {
743
803
  openVideos?: boolean;
744
804
  }
745
805
  /**
746
- * Read an SLP file using HTTP range requests for efficient streaming.
806
+ * Read an SLP file using a Web Worker for efficient, non-blocking HDF5 access.
747
807
  *
748
- * This function downloads only the data needed (metadata, frames, instances, points)
749
- * rather than the entire file.
808
+ * This function offloads all h5wasm operations to a Web Worker, keeping the
809
+ * main thread responsive. For URLs, it uses HTTP range requests to download
810
+ * only the data needed rather than the entire file.
750
811
  *
751
812
  * When `openVideos` is true, video backends are created for embedded videos,
752
813
  * allowing frame data to be retrieved. The underlying HDF5 file remains open
753
814
  * until all video backends are closed.
754
815
  *
755
- * @param url - URL to the SLP file (must support HTTP range requests)
816
+ * @param source - URL, File, ArrayBuffer, or Uint8Array containing the SLP file
756
817
  * @param options - Optional settings
757
818
  * @returns Labels object with all annotation data
758
819
  *
759
820
  * @example
760
821
  * ```typescript
761
- * // Load with video backends for embedded images
822
+ * // Load from URL with video backends
762
823
  * const labels = await readSlpStreaming('https://example.com/labels.slp', {
763
824
  * openVideos: true
764
825
  * });
765
- * const frame = await labels.video.getFrame(0);
826
+ *
827
+ * // Load from File object (file input)
828
+ * const labels = await readSlpStreaming(fileInput.files[0], {
829
+ * openVideos: true
830
+ * });
831
+ *
832
+ * // Load from ArrayBuffer
833
+ * const labels = await readSlpStreaming(arrayBuffer, {
834
+ * filenameHint: 'data.slp'
835
+ * });
766
836
  * ```
767
837
  */
768
- declare function readSlpStreaming(url: string, options?: StreamingSlpOptions): Promise<Labels>;
838
+ declare function readSlpStreaming(source: StreamingH5Source, options?: StreamingSlpOptions): Promise<Labels>;
769
839
 
770
- export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
840
+ export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, type StreamingH5Source, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openH5Worker, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
package/dist/index.js CHANGED
@@ -85,12 +85,17 @@ var Video = class {
85
85
  backendMetadata;
86
86
  sourceVideo;
87
87
  openBackend;
88
+ _embedded;
88
89
  constructor(options) {
89
90
  this.filename = options.filename;
90
91
  this.backend = options.backend ?? null;
91
92
  this.backendMetadata = options.backendMetadata ?? {};
92
93
  this.sourceVideo = options.sourceVideo ?? null;
93
94
  this.openBackend = options.openBackend ?? true;
95
+ this._embedded = options.embedded ?? false;
96
+ }
97
+ get hasEmbeddedImages() {
98
+ return this._embedded;
94
99
  }
95
100
  get originalVideo() {
96
101
  if (!this.sourceVideo) return null;
@@ -1217,7 +1222,7 @@ var StreamingHdf5VideoBackend = class {
1217
1222
  fps;
1218
1223
  h5file;
1219
1224
  datasetPath;
1220
- frameNumbers;
1225
+ frameNumberToIndex;
1221
1226
  format;
1222
1227
  channelOrder;
1223
1228
  cachedData;
@@ -1228,7 +1233,8 @@ var StreamingHdf5VideoBackend = class {
1228
1233
  this.h5file = options.h5file;
1229
1234
  this.datasetPath = options.datasetPath;
1230
1235
  this.dataset = options.datasetPath;
1231
- this.frameNumbers = options.frameNumbers ?? [];
1236
+ const frameNumbers = options.frameNumbers ?? [];
1237
+ this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
1232
1238
  this.format = options.format ?? "png";
1233
1239
  this.channelOrder = options.channelOrder ?? "RGB";
1234
1240
  this.shape = options.shape;
@@ -1237,8 +1243,8 @@ var StreamingHdf5VideoBackend = class {
1237
1243
  this.frameOffsets = null;
1238
1244
  }
1239
1245
  async getFrame(frameIndex) {
1240
- const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1241
- if (index < 0) return null;
1246
+ const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
1247
+ if (index === void 0) return null;
1242
1248
  if (!this.cachedData) {
1243
1249
  try {
1244
1250
  const data = await this.h5file.getDatasetValue(this.datasetPath);
@@ -1267,7 +1273,7 @@ var StreamingHdf5VideoBackend = class {
1267
1273
  }
1268
1274
  if (!rawBytes || rawBytes.length === 0) return null;
1269
1275
  if (isEncodedFormat(this.format)) {
1270
- const decoded = await decodeImageBytes(rawBytes, this.format);
1276
+ const decoded = await decodeImageBytes(rawBytes, this.format, this.channelOrder);
1271
1277
  return decoded ?? rawBytes;
1272
1278
  }
1273
1279
  const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
@@ -1338,12 +1344,29 @@ function isEncodedFormat(format) {
1338
1344
  const normalized = format.toLowerCase();
1339
1345
  return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
1340
1346
  }
1341
- async function decodeImageBytes(bytes, format) {
1347
+ async function decodeImageBytes(bytes, format, channelOrder) {
1342
1348
  if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
1343
1349
  const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
1344
1350
  const safeBytes = new Uint8Array(bytes);
1345
1351
  const blob = new Blob([safeBytes.buffer], { type: mime });
1346
- return createImageBitmap(blob);
1352
+ const bitmap = await createImageBitmap(blob);
1353
+ const useBgr = channelOrder.toUpperCase() === "BGR";
1354
+ if (!useBgr) {
1355
+ return bitmap;
1356
+ }
1357
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
1358
+ const ctx = canvas.getContext("2d");
1359
+ if (!ctx) return bitmap;
1360
+ ctx.drawImage(bitmap, 0, 0);
1361
+ const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
1362
+ const data = imageData.data;
1363
+ for (let i = 0; i < data.length; i += 4) {
1364
+ const r = data[i];
1365
+ const b = data[i + 2];
1366
+ data[i] = b;
1367
+ data[i + 2] = r;
1368
+ }
1369
+ return imageData;
1347
1370
  }
1348
1371
  function decodeRawFrame(bytes, shape, channelOrder) {
1349
1372
  if (!isBrowser3 || !shape) return null;
@@ -1371,7 +1394,8 @@ function decodeRawFrame(bytes, shape, channelOrder) {
1371
1394
  // src/codecs/slp/h5-worker.ts
1372
1395
  var H5_WORKER_CODE = `
1373
1396
  // h5wasm streaming worker
1374
- // Uses createLazyFile for HTTP range request streaming
1397
+ // Handles all HDF5 operations in a Web Worker to avoid main thread blocking
1398
+ // Supports: URL streaming (range requests), local files (WORKERFS), and ArrayBuffers
1375
1399
 
1376
1400
  let h5wasmModule = null;
1377
1401
  let FS = null;
@@ -1389,8 +1413,18 @@ self.onmessage = async function(e) {
1389
1413
  break;
1390
1414
 
1391
1415
  case 'openUrl':
1392
- const result = await openRemoteFile(payload.url, payload.filename);
1393
- respond(id, result);
1416
+ const urlResult = await openRemoteFile(payload.url, payload.filename);
1417
+ respond(id, urlResult);
1418
+ break;
1419
+
1420
+ case 'openLocal':
1421
+ const localResult = await openLocalFile(payload.file, payload.filename);
1422
+ respond(id, localResult);
1423
+ break;
1424
+
1425
+ case 'openBuffer':
1426
+ const bufferResult = await openBufferFile(payload.buffer, payload.filename);
1427
+ respond(id, bufferResult);
1394
1428
  break;
1395
1429
 
1396
1430
  case 'getKeys':
@@ -1481,6 +1515,66 @@ async function openRemoteFile(url, filename = 'data.h5') {
1481
1515
  };
1482
1516
  }
1483
1517
 
1518
+ async function openLocalFile(file, filename) {
1519
+ if (!h5wasmModule) {
1520
+ throw new Error('h5wasm not initialized');
1521
+ }
1522
+
1523
+ // Close any existing file
1524
+ closeFile();
1525
+
1526
+ // Use provided filename or file.name
1527
+ const fname = filename || file.name || 'local.h5';
1528
+
1529
+ // Create mount point for WORKERFS
1530
+ mountPath = '/local-' + Date.now();
1531
+ FS.mkdir(mountPath);
1532
+
1533
+ // Mount the file using WORKERFS (zero-copy access)
1534
+ FS.mount(FS.filesystems.WORKERFS, { files: [file] }, mountPath);
1535
+
1536
+ // Open with h5wasm
1537
+ const filePath = mountPath + '/' + fname;
1538
+ currentFile = new h5wasm.File(filePath, 'r');
1539
+
1540
+ return {
1541
+ success: true,
1542
+ path: currentFile.path,
1543
+ filename: currentFile.filename,
1544
+ keys: currentFile.keys()
1545
+ };
1546
+ }
1547
+
1548
+ async function openBufferFile(buffer, filename = 'data.h5') {
1549
+ if (!h5wasmModule) {
1550
+ throw new Error('h5wasm not initialized');
1551
+ }
1552
+
1553
+ // Close any existing file
1554
+ closeFile();
1555
+
1556
+ // Write buffer to virtual filesystem
1557
+ const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
1558
+ mountPath = '/buffer-' + Date.now() + '/' + filename;
1559
+
1560
+ // Create parent directory
1561
+ const dir = mountPath.substring(0, mountPath.lastIndexOf('/'));
1562
+ FS.mkdir(dir);
1563
+
1564
+ // Write file to virtual FS
1565
+ FS.writeFile(mountPath, data);
1566
+
1567
+ // Open with h5wasm
1568
+ currentFile = new h5wasm.File(mountPath, 'r');
1569
+
1570
+ return {
1571
+ success: true,
1572
+ path: currentFile.path,
1573
+ filename: currentFile.filename,
1574
+ keys: currentFile.keys()
1575
+ };
1576
+ }
1577
+
1484
1578
  function getKeys(path) {
1485
1579
  if (!currentFile) throw new Error('No file open');
1486
1580
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
@@ -1488,19 +1582,38 @@ function getKeys(path) {
1488
1582
  return item.keys ? item.keys() : [];
1489
1583
  }
1490
1584
 
1585
+ function serializeAttrValue(attr) {
1586
+ if (!attr) return null;
1587
+ // h5wasm Attribute objects have a .value property
1588
+ const val = attr.value !== undefined ? attr.value : attr;
1589
+ // Convert Uint8Array to string for JSON attributes
1590
+ if (val instanceof Uint8Array) {
1591
+ return { value: new TextDecoder().decode(val) };
1592
+ }
1593
+ // Wrap primitive values to preserve structure
1594
+ return { value: val };
1595
+ }
1596
+
1491
1597
  function getAttr(path, name) {
1492
1598
  if (!currentFile) throw new Error('No file open');
1493
1599
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
1494
1600
  if (!item) throw new Error('Path not found: ' + path);
1495
1601
  const attrs = item.attrs;
1496
- return attrs?.[name] || null;
1602
+ const attr = attrs?.[name];
1603
+ return serializeAttrValue(attr);
1497
1604
  }
1498
1605
 
1499
1606
  function getAttrs(path) {
1500
1607
  if (!currentFile) throw new Error('No file open');
1501
1608
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
1502
1609
  if (!item) throw new Error('Path not found: ' + path);
1503
- return item.attrs || {};
1610
+ const rawAttrs = item.attrs || {};
1611
+ // Serialize all attributes for proper transfer through postMessage
1612
+ const serialized = {};
1613
+ for (const key of Object.keys(rawAttrs)) {
1614
+ serialized[key] = serializeAttrValue(rawAttrs[key]);
1615
+ }
1616
+ return serialized;
1504
1617
  }
1505
1618
 
1506
1619
  function getDatasetMeta(path) {
@@ -1653,7 +1766,7 @@ var StreamingH5File = class {
1653
1766
  await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
1654
1767
  }
1655
1768
  /**
1656
- * Open a remote HDF5 file for streaming access.
1769
+ * Open a remote HDF5 file for streaming access via URL.
1657
1770
  *
1658
1771
  * @param url - URL to the HDF5 file (must support HTTP range requests)
1659
1772
  * @param options - Optional settings
@@ -1665,6 +1778,51 @@ var StreamingH5File = class {
1665
1778
  this._keys = result.keys || [];
1666
1779
  this._isOpen = true;
1667
1780
  }
1781
+ /**
1782
+ * Open a local File object using WORKERFS (zero-copy).
1783
+ *
1784
+ * @param file - File object from file input or drag-and-drop
1785
+ * @param options - Optional settings
1786
+ */
1787
+ async openLocal(file, options) {
1788
+ await this.init(options);
1789
+ const filename = options?.filenameHint || file.name || "data.h5";
1790
+ const result = await this.send("openLocal", { file, filename });
1791
+ this._keys = result.keys || [];
1792
+ this._isOpen = true;
1793
+ }
1794
+ /**
1795
+ * Open an HDF5 file from an ArrayBuffer or Uint8Array.
1796
+ *
1797
+ * @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
1798
+ * @param options - Optional settings
1799
+ */
1800
+ async openBuffer(buffer, options) {
1801
+ await this.init(options);
1802
+ const filename = options?.filenameHint || "data.h5";
1803
+ const data = buffer instanceof Uint8Array ? buffer.buffer : buffer;
1804
+ const result = await this.send("openBuffer", { buffer: data, filename });
1805
+ this._keys = result.keys || [];
1806
+ this._isOpen = true;
1807
+ }
1808
+ /**
1809
+ * Open an HDF5 file from any supported source.
1810
+ *
1811
+ * @param source - URL string, File, ArrayBuffer, or Uint8Array
1812
+ * @param options - Optional settings
1813
+ */
1814
+ async openAny(source, options) {
1815
+ if (typeof source === "string") {
1816
+ return this.open(source, options);
1817
+ }
1818
+ if (typeof File !== "undefined" && source instanceof File) {
1819
+ return this.openLocal(source, options);
1820
+ }
1821
+ if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
1822
+ return this.openBuffer(source, options);
1823
+ }
1824
+ throw new Error("Unsupported source type for StreamingH5File");
1825
+ }
1668
1826
  /**
1669
1827
  * Whether a file is currently open.
1670
1828
  */
@@ -1744,6 +1902,14 @@ async function openStreamingH5(url, options) {
1744
1902
  await file.open(url, options);
1745
1903
  return file;
1746
1904
  }
1905
+ async function openH5Worker(source, options) {
1906
+ if (!isStreamingSupported()) {
1907
+ throw new Error("Web Worker HDF5 access requires Worker/Blob/URL support");
1908
+ }
1909
+ const file = new StreamingH5File();
1910
+ await file.openAny(source, options);
1911
+ return file;
1912
+ }
1747
1913
 
1748
1914
  // src/codecs/slp/h5.ts
1749
1915
  var isNode = typeof process !== "undefined" && !!process.versions?.node;
@@ -1891,7 +2057,7 @@ var Hdf5VideoBackend = class {
1891
2057
  fps;
1892
2058
  file;
1893
2059
  datasetPath;
1894
- frameNumbers;
2060
+ frameNumberToIndex;
1895
2061
  format;
1896
2062
  channelOrder;
1897
2063
  cachedData;
@@ -1901,7 +2067,8 @@ var Hdf5VideoBackend = class {
1901
2067
  this.file = options.file;
1902
2068
  this.datasetPath = options.datasetPath;
1903
2069
  this.dataset = options.datasetPath;
1904
- this.frameNumbers = options.frameNumbers ?? [];
2070
+ const frameNumbers = options.frameNumbers ?? [];
2071
+ this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
1905
2072
  this.format = options.format ?? "png";
1906
2073
  this.channelOrder = options.channelOrder ?? "RGB";
1907
2074
  this.shape = options.shape;
@@ -1912,8 +2079,8 @@ var Hdf5VideoBackend = class {
1912
2079
  async getFrame(frameIndex) {
1913
2080
  const dataset = this.file.get(this.datasetPath);
1914
2081
  if (!dataset) return null;
1915
- const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1916
- if (index < 0) return null;
2082
+ const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
2083
+ if (index === void 0) return null;
1917
2084
  if (!this.cachedData) {
1918
2085
  const value = dataset.value;
1919
2086
  this.cachedData = normalizeVideoData2(value);
@@ -1938,7 +2105,7 @@ var Hdf5VideoBackend = class {
1938
2105
  }
1939
2106
  if (!rawBytes || rawBytes.length === 0) return null;
1940
2107
  if (isEncodedFormat2(this.format)) {
1941
- const decoded = await decodeImageBytes2(rawBytes, this.format);
2108
+ const decoded = await decodeImageBytes2(rawBytes, this.format, this.channelOrder);
1942
2109
  return decoded ?? rawBytes;
1943
2110
  }
1944
2111
  const image = decodeRawFrame2(rawBytes, this.shape, this.channelOrder);
@@ -2007,12 +2174,29 @@ function isEncodedFormat2(format) {
2007
2174
  const normalized = format.toLowerCase();
2008
2175
  return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
2009
2176
  }
2010
- async function decodeImageBytes2(bytes, format) {
2177
+ async function decodeImageBytes2(bytes, format, channelOrder) {
2011
2178
  if (!isBrowser4 || typeof createImageBitmap === "undefined") return null;
2012
2179
  const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
2013
2180
  const safeBytes = new Uint8Array(bytes);
2014
2181
  const blob = new Blob([safeBytes.buffer], { type: mime });
2015
- return createImageBitmap(blob);
2182
+ const bitmap = await createImageBitmap(blob);
2183
+ const useBgr = channelOrder.toUpperCase() === "BGR";
2184
+ if (!useBgr) {
2185
+ return bitmap;
2186
+ }
2187
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
2188
+ const ctx = canvas.getContext("2d");
2189
+ if (!ctx) return bitmap;
2190
+ ctx.drawImage(bitmap, 0, 0);
2191
+ const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
2192
+ const data = imageData.data;
2193
+ for (let i = 0; i < data.length; i += 4) {
2194
+ const r = data[i];
2195
+ const b = data[i + 2];
2196
+ data[i] = b;
2197
+ data[i + 2] = r;
2198
+ }
2199
+ return imageData;
2016
2200
  }
2017
2201
  function decodeRawFrame2(bytes, shape, channelOrder) {
2018
2202
  if (!isBrowser4 || !shape) return null;
@@ -2076,7 +2260,7 @@ async function readSlp(source, options) {
2076
2260
  const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
2077
2261
  const skeletons = parseSkeletons(metadataJson);
2078
2262
  const tracks = readTracks(file.get("tracks_json"));
2079
- const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
2263
+ const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file, formatId);
2080
2264
  const suggestions = readSuggestions(file.get("suggestions_json"), videos);
2081
2265
  const framesData = normalizeStructDataset(file.get("frames"));
2082
2266
  const instancesData = normalizeStructDataset(file.get("instances"));
@@ -2129,11 +2313,12 @@ function readTracks(dataset) {
2129
2313
  }
2130
2314
  return tracks;
2131
2315
  }
2132
- async function readVideos(dataset, labelsPath, openVideos, file) {
2316
+ async function readVideos(dataset, labelsPath, openVideos, file, formatId) {
2133
2317
  if (!dataset) return [];
2134
2318
  const values = dataset.value ?? [];
2135
2319
  const videos = [];
2136
- for (const entry of values) {
2320
+ for (let videoIndex = 0; videoIndex < values.length; videoIndex++) {
2321
+ const entry = values[videoIndex];
2137
2322
  if (!entry) continue;
2138
2323
  const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
2139
2324
  const backendMeta = parsed.backend ?? {};
@@ -2144,14 +2329,26 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
2144
2329
  embedded = true;
2145
2330
  filename = labelsPath;
2146
2331
  }
2332
+ if (embedded && !datasetPath) {
2333
+ datasetPath = findVideoDataset(file, videoIndex);
2334
+ }
2335
+ const channelOrder = backendMeta.channel_order ?? (formatId < 1.4 ? "BGR" : "RGB");
2336
+ let format = backendMeta.format;
2337
+ if (!format && datasetPath) {
2338
+ const videoDs = file.get(datasetPath);
2339
+ if (videoDs) {
2340
+ const attrs = videoDs.attrs ?? {};
2341
+ format = attrs.format?.value ?? attrs.format;
2342
+ }
2343
+ }
2147
2344
  let backend = null;
2148
2345
  if (openVideos) {
2149
2346
  backend = await createVideoBackend(filename, {
2150
2347
  dataset: datasetPath ?? void 0,
2151
2348
  embedded,
2152
2349
  frameNumbers: readFrameNumbers(file, datasetPath),
2153
- format: backendMeta.format,
2154
- channelOrder: backendMeta.channel_order,
2350
+ format,
2351
+ channelOrder,
2155
2352
  shape: backendMeta.shape,
2156
2353
  fps: backendMeta.fps
2157
2354
  });
@@ -2163,7 +2360,8 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
2163
2360
  backend,
2164
2361
  backendMetadata: backendMeta,
2165
2362
  sourceVideo,
2166
- openBackend: openVideos
2363
+ openBackend: openVideos,
2364
+ embedded
2167
2365
  })
2168
2366
  );
2169
2367
  }
@@ -2177,6 +2375,28 @@ function readFrameNumbers(file, datasetPath) {
2177
2375
  const values = frameDataset.value ?? [];
2178
2376
  return Array.from(values).map((v) => Number(v));
2179
2377
  }
2378
+ function findVideoDataset(file, videoIndex) {
2379
+ const explicitPath = `video${videoIndex}/video`;
2380
+ if (file.get(explicitPath)) {
2381
+ return explicitPath;
2382
+ }
2383
+ const keys = file.keys?.() ?? [];
2384
+ for (const key of keys) {
2385
+ if (key.startsWith("video")) {
2386
+ const candidatePath = `${key}/video`;
2387
+ if (file.get(candidatePath)) {
2388
+ if (videoIndex === 0) {
2389
+ return candidatePath;
2390
+ }
2391
+ const keyIndex = parseInt(key.slice(5), 10);
2392
+ if (!isNaN(keyIndex) && keyIndex === videoIndex) {
2393
+ return candidatePath;
2394
+ }
2395
+ }
2396
+ }
2397
+ }
2398
+ return null;
2399
+ }
2180
2400
  function readSuggestions(dataset, videos) {
2181
2401
  if (!dataset) return [];
2182
2402
  const values = dataset.value ?? [];
@@ -2456,17 +2676,18 @@ function slicePoints(data, start, end, predicted = false) {
2456
2676
  }
2457
2677
 
2458
2678
  // src/codecs/slp/read-streaming.ts
2459
- async function readSlpStreaming(url, options) {
2679
+ async function readSlpStreaming(source, options) {
2460
2680
  if (!isStreamingSupported()) {
2461
2681
  throw new Error("Streaming HDF5 requires Web Worker support (browser environment)");
2462
2682
  }
2463
- const file = await openStreamingH5(url, {
2683
+ const file = await openH5Worker(source, {
2464
2684
  h5wasmUrl: options?.h5wasmUrl,
2465
2685
  filenameHint: options?.filenameHint
2466
2686
  });
2467
2687
  const openVideos = options?.openVideos ?? false;
2688
+ const sourcePath = typeof source === "string" ? source : typeof File !== "undefined" && source instanceof File ? source.name : options?.filenameHint ?? "slp-data.slp";
2468
2689
  try {
2469
- return await readFromStreamingFile(file, url, options?.filenameHint, openVideos);
2690
+ return await readFromStreamingFile(file, sourcePath, options?.filenameHint, openVideos);
2470
2691
  } finally {
2471
2692
  if (!openVideos) {
2472
2693
  await file.close();
@@ -2482,7 +2703,7 @@ async function readFromStreamingFile(file, url, filenameHint, openVideos = false
2482
2703
  const labelsPath = filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
2483
2704
  const skeletons = parseSkeletons(metadataJson);
2484
2705
  const tracks = await readTracksStreaming(file);
2485
- const videos = await readVideosStreaming(file, labelsPath, openVideos);
2706
+ const videos = await readVideosStreaming(file, labelsPath, openVideos, formatId);
2486
2707
  const suggestions = await readSuggestionsStreaming(file, videos);
2487
2708
  const framesData = await readStructDatasetStreaming(file, "frames");
2488
2709
  const instancesData = await readStructDatasetStreaming(file, "instances");
@@ -2520,7 +2741,7 @@ async function readTracksStreaming(file) {
2520
2741
  return [];
2521
2742
  }
2522
2743
  }
2523
- async function readVideosStreaming(file, labelsPath, openVideos = false) {
2744
+ async function readVideosStreaming(file, labelsPath, openVideos = false, formatId = 1) {
2524
2745
  try {
2525
2746
  const keys = file.keys();
2526
2747
  if (!keys.includes("videos_json")) return [];
@@ -2528,18 +2749,35 @@ async function readVideosStreaming(file, labelsPath, openVideos = false) {
2528
2749
  const values = normalizeDatasetArray(data.value);
2529
2750
  const metadataList = parseVideosMetadata(values, labelsPath);
2530
2751
  const videos = [];
2531
- for (const meta of metadataList) {
2752
+ for (let videoIndex = 0; videoIndex < metadataList.length; videoIndex++) {
2753
+ const meta = metadataList[videoIndex];
2532
2754
  const shape = meta.frameCount && meta.height && meta.width && meta.channels ? [meta.frameCount, meta.height, meta.width, meta.channels] : void 0;
2755
+ let datasetPath = meta.dataset;
2756
+ if (meta.embedded && !datasetPath) {
2757
+ datasetPath = await findVideoDatasetStreaming(file, videoIndex) ?? void 0;
2758
+ }
2759
+ const channelOrder = meta.channelOrder ?? (formatId < 1.4 ? "BGR" : "RGB");
2760
+ let format = meta.format;
2761
+ if (!format && datasetPath) {
2762
+ try {
2763
+ const attrs = await file.getAttrs(datasetPath);
2764
+ const formatAttr = attrs.format;
2765
+ if (formatAttr) {
2766
+ format = typeof formatAttr === "string" ? formatAttr : formatAttr?.value;
2767
+ }
2768
+ } catch {
2769
+ }
2770
+ }
2533
2771
  let backend = null;
2534
- if (openVideos && meta.embedded && meta.dataset) {
2535
- const frameNumbers = await readFrameNumbersStreaming(file, meta.dataset);
2772
+ if (openVideos && meta.embedded && datasetPath) {
2773
+ const frameNumbers = await readFrameNumbersStreaming(file, datasetPath);
2536
2774
  backend = new StreamingHdf5VideoBackend({
2537
2775
  filename: meta.filename,
2538
2776
  h5file: file,
2539
- datasetPath: meta.dataset,
2777
+ datasetPath,
2540
2778
  frameNumbers,
2541
- format: meta.format ?? "png",
2542
- channelOrder: meta.channelOrder ?? "RGB",
2779
+ format: format ?? "png",
2780
+ channelOrder,
2543
2781
  shape,
2544
2782
  fps: meta.fps
2545
2783
  });
@@ -2548,14 +2786,15 @@ async function readVideosStreaming(file, labelsPath, openVideos = false) {
2548
2786
  filename: meta.filename,
2549
2787
  backend,
2550
2788
  backendMetadata: {
2551
- dataset: meta.dataset,
2552
- format: meta.format,
2789
+ dataset: datasetPath,
2790
+ format,
2553
2791
  shape,
2554
2792
  fps: meta.fps,
2555
- channel_order: meta.channelOrder
2793
+ channel_order: channelOrder
2556
2794
  },
2557
2795
  sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
2558
- openBackend: openVideos && meta.embedded
2796
+ openBackend: openVideos && meta.embedded,
2797
+ embedded: meta.embedded
2559
2798
  }));
2560
2799
  }
2561
2800
  return videos;
@@ -2584,6 +2823,41 @@ async function readFrameNumbersStreaming(file, datasetPath) {
2584
2823
  return [];
2585
2824
  }
2586
2825
  }
2826
+ async function findVideoDatasetStreaming(file, videoIndex) {
2827
+ try {
2828
+ const explicitPath = `video${videoIndex}/video`;
2829
+ const explicitGroupPath = `video${videoIndex}`;
2830
+ try {
2831
+ const groupKeys = await file.getKeys(explicitGroupPath);
2832
+ if (groupKeys.includes("video")) {
2833
+ return explicitPath;
2834
+ }
2835
+ } catch {
2836
+ }
2837
+ const rootKeys = file.keys();
2838
+ for (const key of rootKeys) {
2839
+ if (key.startsWith("video")) {
2840
+ try {
2841
+ const groupKeys = await file.getKeys(key);
2842
+ if (groupKeys.includes("video")) {
2843
+ const candidatePath = `${key}/video`;
2844
+ if (videoIndex === 0) {
2845
+ return candidatePath;
2846
+ }
2847
+ const keyIndex = parseInt(key.slice(5), 10);
2848
+ if (!isNaN(keyIndex) && keyIndex === videoIndex) {
2849
+ return candidatePath;
2850
+ }
2851
+ }
2852
+ } catch {
2853
+ }
2854
+ }
2855
+ }
2856
+ return null;
2857
+ } catch {
2858
+ return null;
2859
+ }
2860
+ }
2587
2861
  async function readSuggestionsStreaming(file, videos) {
2588
2862
  try {
2589
2863
  const keys = file.keys();
@@ -3095,29 +3369,44 @@ function createMatrixDataset(file, name, rows, fieldNames, dtype) {
3095
3369
  }
3096
3370
 
3097
3371
  // src/io/main.ts
3098
- function isProbablyUrl2(source) {
3099
- return typeof source === "string" && /^https?:\/\//i.test(source);
3372
+ function isNode3() {
3373
+ return typeof process !== "undefined" && !!process.versions?.node;
3100
3374
  }
3101
- function isBrowser5() {
3102
- return typeof window !== "undefined" && typeof Worker !== "undefined";
3375
+ function isBrowserWithWorkerSupport() {
3376
+ return typeof window !== "undefined" && isStreamingSupported();
3103
3377
  }
3104
3378
  async function loadSlp(source, options) {
3105
3379
  const streamMode = options?.h5?.stream ?? "auto";
3106
- if (isProbablyUrl2(source) && isBrowser5() && isStreamingSupported() && (streamMode === "range" || streamMode === "auto")) {
3107
- try {
3108
- return await readSlpStreaming(source, {
3109
- filenameHint: options?.h5?.filenameHint,
3110
- openVideos: options?.openVideos ?? true
3111
- });
3112
- } catch (e) {
3113
- if (streamMode === "auto") {
3114
- console.warn("Streaming failed, falling back to full download:", e);
3115
- } else {
3116
- throw e;
3380
+ const openVideos = options?.openVideos ?? true;
3381
+ if (isBrowserWithWorkerSupport() && !isNode3() && streamMode !== "download") {
3382
+ let streamingSource;
3383
+ if (typeof source === "string") {
3384
+ streamingSource = source;
3385
+ } else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
3386
+ streamingSource = source;
3387
+ } else if (typeof File !== "undefined" && source instanceof File) {
3388
+ streamingSource = source;
3389
+ } else if (typeof FileSystemFileHandle !== "undefined" && "getFile" in source) {
3390
+ streamingSource = await source.getFile();
3391
+ } else {
3392
+ streamingSource = null;
3393
+ }
3394
+ if (streamingSource !== null) {
3395
+ try {
3396
+ return await readSlpStreaming(streamingSource, {
3397
+ filenameHint: options?.h5?.filenameHint,
3398
+ openVideos
3399
+ });
3400
+ } catch (e) {
3401
+ if (streamMode === "auto") {
3402
+ console.warn("[sleap-io] Worker-based loading failed, falling back to main thread:", e);
3403
+ } else {
3404
+ throw e;
3405
+ }
3117
3406
  }
3118
3407
  }
3119
3408
  }
3120
- return readSlp(source, { openVideos: options?.openVideos ?? true, h5: options?.h5 });
3409
+ return readSlp(source, { openVideos, h5: options?.h5 });
3121
3410
  }
3122
3411
  async function saveSlp(labels, filename, options) {
3123
3412
  await writeSlp(filename, labels, {
@@ -4009,6 +4298,7 @@ export {
4009
4298
  loadSlp,
4010
4299
  loadVideo,
4011
4300
  makeCameraFromDict,
4301
+ openH5Worker,
4012
4302
  openStreamingH5,
4013
4303
  pointsEmpty,
4014
4304
  pointsFromArray,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talmolab/sleap-io.js",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {