@talmolab/sleap-io.js 0.1.6 → 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.
@@ -348,6 +399,10 @@ declare function openStreamingH5(url: string, options?: StreamingH5Options): Pro
348
399
  * This backend uses StreamingH5File (Web Worker + range requests) instead of
349
400
  * a synchronous h5wasm File object, making it suitable for browser environments
350
401
  * where the SLP file is loaded via HTTP range requests.
402
+ *
403
+ * Supports two data storage formats:
404
+ * 1. vlen-encoded: Array of individual frame blobs (each index = one frame)
405
+ * 2. Contiguous buffer: Single buffer with all frames concatenated
351
406
  */
352
407
  declare class StreamingHdf5VideoBackend implements VideoBackend {
353
408
  filename: string;
@@ -356,10 +411,11 @@ declare class StreamingHdf5VideoBackend implements VideoBackend {
356
411
  fps?: number;
357
412
  private h5file;
358
413
  private datasetPath;
359
- private frameNumbers;
414
+ private frameNumberToIndex;
360
415
  private format;
361
416
  private channelOrder;
362
417
  private cachedData;
418
+ private frameOffsets;
363
419
  constructor(options: {
364
420
  filename: string;
365
421
  h5file: StreamingH5File;
@@ -391,27 +447,36 @@ type OpenH5Options = {
391
447
  /**
392
448
  * Load an SLP file.
393
449
  *
394
- * When loading from a URL in a browser with `h5.stream` set to 'range' or 'auto',
395
- * this function automatically uses HTTP range requests for efficient streaming.
396
- * 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.
397
455
  *
398
456
  * @param source - Path, URL, ArrayBuffer, File, or FileSystemFileHandle
399
457
  * @param options - Loading options
400
- * @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)
401
459
  * @param options.h5 - HDF5 options including streaming mode
402
460
  * @param options.h5.stream - 'auto' | 'range' | 'download' (default: 'auto')
403
461
  *
404
462
  * @example
405
463
  * ```typescript
406
- * // Load from URL with streaming (uses range requests automatically)
407
- * const labels = await loadSlp('https://example.com/labels.slp', {
408
- * h5: { stream: 'range' }
409
- * });
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);
410
472
  *
411
- * // Force full download
473
+ * // Force full download instead of range requests
412
474
  * const labels = await loadSlp('https://example.com/labels.slp', {
413
475
  * h5: { stream: 'download' }
414
476
  * });
477
+ *
478
+ * // Node.js: Load from file path
479
+ * const labels = await loadSlp('/path/to/file.slp');
415
480
  * ```
416
481
  */
417
482
  declare function loadSlp(source: SlpSource, options?: {
@@ -738,28 +803,38 @@ interface StreamingSlpOptions {
738
803
  openVideos?: boolean;
739
804
  }
740
805
  /**
741
- * 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.
742
807
  *
743
- * This function downloads only the data needed (metadata, frames, instances, points)
744
- * 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.
745
811
  *
746
812
  * When `openVideos` is true, video backends are created for embedded videos,
747
813
  * allowing frame data to be retrieved. The underlying HDF5 file remains open
748
814
  * until all video backends are closed.
749
815
  *
750
- * @param url - URL to the SLP file (must support HTTP range requests)
816
+ * @param source - URL, File, ArrayBuffer, or Uint8Array containing the SLP file
751
817
  * @param options - Optional settings
752
818
  * @returns Labels object with all annotation data
753
819
  *
754
820
  * @example
755
821
  * ```typescript
756
- * // Load with video backends for embedded images
822
+ * // Load from URL with video backends
757
823
  * const labels = await readSlpStreaming('https://example.com/labels.slp', {
758
824
  * openVideos: true
759
825
  * });
760
- * 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
+ * });
761
836
  * ```
762
837
  */
763
- declare function readSlpStreaming(url: string, options?: StreamingSlpOptions): Promise<Labels>;
838
+ declare function readSlpStreaming(source: StreamingH5Source, options?: StreamingSlpOptions): Promise<Labels>;
764
839
 
765
- 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;
@@ -1208,6 +1213,8 @@ var Mp4BoxVideoBackend = class {
1208
1213
 
1209
1214
  // src/video/streaming-hdf5-video.ts
1210
1215
  var isBrowser3 = typeof window !== "undefined" && typeof document !== "undefined";
1216
+ var PNG_MAGIC = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
1217
+ var JPEG_MAGIC = new Uint8Array([255, 216, 255]);
1211
1218
  var StreamingHdf5VideoBackend = class {
1212
1219
  filename;
1213
1220
  dataset;
@@ -1215,39 +1222,58 @@ var StreamingHdf5VideoBackend = class {
1215
1222
  fps;
1216
1223
  h5file;
1217
1224
  datasetPath;
1218
- frameNumbers;
1225
+ frameNumberToIndex;
1219
1226
  format;
1220
1227
  channelOrder;
1221
1228
  cachedData;
1229
+ frameOffsets;
1230
+ // For contiguous buffer: byte offsets of each frame
1222
1231
  constructor(options) {
1223
1232
  this.filename = options.filename;
1224
1233
  this.h5file = options.h5file;
1225
1234
  this.datasetPath = options.datasetPath;
1226
1235
  this.dataset = options.datasetPath;
1227
- this.frameNumbers = options.frameNumbers ?? [];
1236
+ const frameNumbers = options.frameNumbers ?? [];
1237
+ this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
1228
1238
  this.format = options.format ?? "png";
1229
1239
  this.channelOrder = options.channelOrder ?? "RGB";
1230
1240
  this.shape = options.shape;
1231
1241
  this.fps = options.fps;
1232
1242
  this.cachedData = null;
1243
+ this.frameOffsets = null;
1233
1244
  }
1234
1245
  async getFrame(frameIndex) {
1235
- const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1236
- if (index < 0) return null;
1246
+ const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
1247
+ if (index === void 0) return null;
1237
1248
  if (!this.cachedData) {
1238
1249
  try {
1239
1250
  const data = await this.h5file.getDatasetValue(this.datasetPath);
1240
- this.cachedData = data.value;
1251
+ this.cachedData = normalizeVideoData(data.value, data.shape);
1252
+ if (isContiguousEncodedBuffer(this.cachedData, this.format, this.shape)) {
1253
+ this.frameOffsets = findEncodedFrameOffsets(
1254
+ this.cachedData,
1255
+ this.format,
1256
+ this.shape?.[0] ?? 0
1257
+ );
1258
+ }
1241
1259
  } catch {
1242
1260
  return null;
1243
1261
  }
1244
1262
  }
1245
- const entry = this.cachedData[index];
1246
- if (entry == null) return null;
1247
- const rawBytes = toUint8Array(entry);
1248
- if (!rawBytes) return null;
1263
+ let rawBytes;
1264
+ if (this.frameOffsets && this.frameOffsets.length > index) {
1265
+ const buffer = this.cachedData;
1266
+ const start = this.frameOffsets[index];
1267
+ const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
1268
+ rawBytes = buffer.slice(start, end);
1269
+ } else {
1270
+ const entry = this.cachedData[index];
1271
+ if (entry == null) return null;
1272
+ rawBytes = toUint8Array(entry);
1273
+ }
1274
+ if (!rawBytes || rawBytes.length === 0) return null;
1249
1275
  if (isEncodedFormat(this.format)) {
1250
- const decoded = await decodeImageBytes(rawBytes, this.format);
1276
+ const decoded = await decodeImageBytes(rawBytes, this.format, this.channelOrder);
1251
1277
  return decoded ?? rawBytes;
1252
1278
  }
1253
1279
  const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
@@ -1255,8 +1281,55 @@ var StreamingHdf5VideoBackend = class {
1255
1281
  }
1256
1282
  close() {
1257
1283
  this.cachedData = null;
1284
+ this.frameOffsets = null;
1258
1285
  }
1259
1286
  };
1287
+ function normalizeVideoData(value, _shape) {
1288
+ if (Array.isArray(value)) {
1289
+ return value;
1290
+ }
1291
+ if (ArrayBuffer.isView(value)) {
1292
+ const arr = value;
1293
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
1294
+ }
1295
+ return [];
1296
+ }
1297
+ function isContiguousEncodedBuffer(data, format, shape) {
1298
+ if (!isEncodedFormat(format)) return false;
1299
+ if (!(data instanceof Uint8Array)) return false;
1300
+ if (data.length < 8) return false;
1301
+ const isPng = matchesMagic(data, PNG_MAGIC);
1302
+ const isJpeg = matchesMagic(data, JPEG_MAGIC);
1303
+ if (!isPng && !isJpeg) return false;
1304
+ if (shape) {
1305
+ const frameCount = shape[0];
1306
+ if (frameCount > 1 && data.length > 1e4) {
1307
+ return true;
1308
+ }
1309
+ }
1310
+ return true;
1311
+ }
1312
+ function matchesMagic(buffer, magic) {
1313
+ if (buffer.length < magic.length) return false;
1314
+ for (let i = 0; i < magic.length; i++) {
1315
+ if (buffer[i] !== magic[i]) return false;
1316
+ }
1317
+ return true;
1318
+ }
1319
+ function findEncodedFrameOffsets(buffer, format, expectedFrameCount) {
1320
+ const offsets = [];
1321
+ const magic = format.toLowerCase() === "png" ? PNG_MAGIC : JPEG_MAGIC;
1322
+ for (let i = 0; i <= buffer.length - magic.length; i++) {
1323
+ if (matchesMagic(buffer.subarray(i), magic)) {
1324
+ offsets.push(i);
1325
+ i += magic.length - 1;
1326
+ if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
1327
+ break;
1328
+ }
1329
+ }
1330
+ }
1331
+ return offsets;
1332
+ }
1260
1333
  function toUint8Array(entry) {
1261
1334
  if (entry instanceof Uint8Array) return entry;
1262
1335
  if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
@@ -1271,12 +1344,29 @@ function isEncodedFormat(format) {
1271
1344
  const normalized = format.toLowerCase();
1272
1345
  return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
1273
1346
  }
1274
- async function decodeImageBytes(bytes, format) {
1347
+ async function decodeImageBytes(bytes, format, channelOrder) {
1275
1348
  if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
1276
1349
  const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
1277
1350
  const safeBytes = new Uint8Array(bytes);
1278
1351
  const blob = new Blob([safeBytes.buffer], { type: mime });
1279
- 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;
1280
1370
  }
1281
1371
  function decodeRawFrame(bytes, shape, channelOrder) {
1282
1372
  if (!isBrowser3 || !shape) return null;
@@ -1304,7 +1394,8 @@ function decodeRawFrame(bytes, shape, channelOrder) {
1304
1394
  // src/codecs/slp/h5-worker.ts
1305
1395
  var H5_WORKER_CODE = `
1306
1396
  // h5wasm streaming worker
1307
- // 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
1308
1399
 
1309
1400
  let h5wasmModule = null;
1310
1401
  let FS = null;
@@ -1322,8 +1413,18 @@ self.onmessage = async function(e) {
1322
1413
  break;
1323
1414
 
1324
1415
  case 'openUrl':
1325
- const result = await openRemoteFile(payload.url, payload.filename);
1326
- 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);
1327
1428
  break;
1328
1429
 
1329
1430
  case 'getKeys':
@@ -1414,6 +1515,66 @@ async function openRemoteFile(url, filename = 'data.h5') {
1414
1515
  };
1415
1516
  }
1416
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
+
1417
1578
  function getKeys(path) {
1418
1579
  if (!currentFile) throw new Error('No file open');
1419
1580
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
@@ -1421,19 +1582,38 @@ function getKeys(path) {
1421
1582
  return item.keys ? item.keys() : [];
1422
1583
  }
1423
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
+
1424
1597
  function getAttr(path, name) {
1425
1598
  if (!currentFile) throw new Error('No file open');
1426
1599
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
1427
1600
  if (!item) throw new Error('Path not found: ' + path);
1428
1601
  const attrs = item.attrs;
1429
- return attrs?.[name] || null;
1602
+ const attr = attrs?.[name];
1603
+ return serializeAttrValue(attr);
1430
1604
  }
1431
1605
 
1432
1606
  function getAttrs(path) {
1433
1607
  if (!currentFile) throw new Error('No file open');
1434
1608
  const item = path === '/' || !path ? currentFile : currentFile.get(path);
1435
1609
  if (!item) throw new Error('Path not found: ' + path);
1436
- 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;
1437
1617
  }
1438
1618
 
1439
1619
  function getDatasetMeta(path) {
@@ -1586,7 +1766,7 @@ var StreamingH5File = class {
1586
1766
  await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
1587
1767
  }
1588
1768
  /**
1589
- * Open a remote HDF5 file for streaming access.
1769
+ * Open a remote HDF5 file for streaming access via URL.
1590
1770
  *
1591
1771
  * @param url - URL to the HDF5 file (must support HTTP range requests)
1592
1772
  * @param options - Optional settings
@@ -1598,6 +1778,51 @@ var StreamingH5File = class {
1598
1778
  this._keys = result.keys || [];
1599
1779
  this._isOpen = true;
1600
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
+ }
1601
1826
  /**
1602
1827
  * Whether a file is currently open.
1603
1828
  */
@@ -1677,6 +1902,14 @@ async function openStreamingH5(url, options) {
1677
1902
  await file.open(url, options);
1678
1903
  return file;
1679
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
+ }
1680
1913
 
1681
1914
  // src/codecs/slp/h5.ts
1682
1915
  var isNode = typeof process !== "undefined" && !!process.versions?.node;
@@ -1815,6 +2048,8 @@ function getH5FileSystem(module) {
1815
2048
 
1816
2049
  // src/video/hdf5-video.ts
1817
2050
  var isBrowser4 = typeof window !== "undefined" && typeof document !== "undefined";
2051
+ var PNG_MAGIC2 = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
2052
+ var JPEG_MAGIC2 = new Uint8Array([255, 216, 255]);
1818
2053
  var Hdf5VideoBackend = class {
1819
2054
  filename;
1820
2055
  dataset;
@@ -1822,36 +2057,55 @@ var Hdf5VideoBackend = class {
1822
2057
  fps;
1823
2058
  file;
1824
2059
  datasetPath;
1825
- frameNumbers;
2060
+ frameNumberToIndex;
1826
2061
  format;
1827
2062
  channelOrder;
1828
2063
  cachedData;
2064
+ frameOffsets;
1829
2065
  constructor(options) {
1830
2066
  this.filename = options.filename;
1831
2067
  this.file = options.file;
1832
2068
  this.datasetPath = options.datasetPath;
1833
2069
  this.dataset = options.datasetPath;
1834
- this.frameNumbers = options.frameNumbers ?? [];
2070
+ const frameNumbers = options.frameNumbers ?? [];
2071
+ this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
1835
2072
  this.format = options.format ?? "png";
1836
2073
  this.channelOrder = options.channelOrder ?? "RGB";
1837
2074
  this.shape = options.shape;
1838
2075
  this.fps = options.fps;
1839
2076
  this.cachedData = null;
2077
+ this.frameOffsets = null;
1840
2078
  }
1841
2079
  async getFrame(frameIndex) {
1842
2080
  const dataset = this.file.get(this.datasetPath);
1843
2081
  if (!dataset) return null;
1844
- const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1845
- if (index < 0) return null;
2082
+ const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
2083
+ if (index === void 0) return null;
1846
2084
  if (!this.cachedData) {
1847
- this.cachedData = dataset.value;
2085
+ const value = dataset.value;
2086
+ this.cachedData = normalizeVideoData2(value);
2087
+ if (isContiguousEncodedBuffer2(this.cachedData, this.format, this.shape)) {
2088
+ this.frameOffsets = findEncodedFrameOffsets2(
2089
+ this.cachedData,
2090
+ this.format,
2091
+ this.shape?.[0] ?? 0
2092
+ );
2093
+ }
2094
+ }
2095
+ let rawBytes;
2096
+ if (this.frameOffsets && this.frameOffsets.length > index) {
2097
+ const buffer = this.cachedData;
2098
+ const start = this.frameOffsets[index];
2099
+ const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
2100
+ rawBytes = buffer.slice(start, end);
2101
+ } else {
2102
+ const entry = this.cachedData[index];
2103
+ if (entry == null) return null;
2104
+ rawBytes = toUint8Array2(entry);
1848
2105
  }
1849
- const entry = this.cachedData[index];
1850
- if (entry == null) return null;
1851
- const rawBytes = toUint8Array2(entry);
1852
- if (!rawBytes) return null;
2106
+ if (!rawBytes || rawBytes.length === 0) return null;
1853
2107
  if (isEncodedFormat2(this.format)) {
1854
- const decoded = await decodeImageBytes2(rawBytes, this.format);
2108
+ const decoded = await decodeImageBytes2(rawBytes, this.format, this.channelOrder);
1855
2109
  return decoded ?? rawBytes;
1856
2110
  }
1857
2111
  const image = decodeRawFrame2(rawBytes, this.shape, this.channelOrder);
@@ -1859,8 +2113,55 @@ var Hdf5VideoBackend = class {
1859
2113
  }
1860
2114
  close() {
1861
2115
  this.cachedData = null;
2116
+ this.frameOffsets = null;
1862
2117
  }
1863
2118
  };
2119
+ function normalizeVideoData2(value) {
2120
+ if (Array.isArray(value)) {
2121
+ return value;
2122
+ }
2123
+ if (ArrayBuffer.isView(value)) {
2124
+ const arr = value;
2125
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
2126
+ }
2127
+ return [];
2128
+ }
2129
+ function isContiguousEncodedBuffer2(data, format, shape) {
2130
+ if (!isEncodedFormat2(format)) return false;
2131
+ if (!(data instanceof Uint8Array)) return false;
2132
+ if (data.length < 8) return false;
2133
+ const isPng = matchesMagic2(data, PNG_MAGIC2);
2134
+ const isJpeg = matchesMagic2(data, JPEG_MAGIC2);
2135
+ if (!isPng && !isJpeg) return false;
2136
+ if (shape) {
2137
+ const frameCount = shape[0];
2138
+ if (frameCount > 1 && data.length > 1e4) {
2139
+ return true;
2140
+ }
2141
+ }
2142
+ return true;
2143
+ }
2144
+ function matchesMagic2(buffer, magic) {
2145
+ if (buffer.length < magic.length) return false;
2146
+ for (let i = 0; i < magic.length; i++) {
2147
+ if (buffer[i] !== magic[i]) return false;
2148
+ }
2149
+ return true;
2150
+ }
2151
+ function findEncodedFrameOffsets2(buffer, format, expectedFrameCount) {
2152
+ const offsets = [];
2153
+ const magic = format.toLowerCase() === "png" ? PNG_MAGIC2 : JPEG_MAGIC2;
2154
+ for (let i = 0; i <= buffer.length - magic.length; i++) {
2155
+ if (matchesMagic2(buffer.subarray(i), magic)) {
2156
+ offsets.push(i);
2157
+ i += magic.length - 1;
2158
+ if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
2159
+ break;
2160
+ }
2161
+ }
2162
+ }
2163
+ return offsets;
2164
+ }
1864
2165
  function toUint8Array2(entry) {
1865
2166
  if (entry instanceof Uint8Array) return entry;
1866
2167
  if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
@@ -1873,12 +2174,29 @@ function isEncodedFormat2(format) {
1873
2174
  const normalized = format.toLowerCase();
1874
2175
  return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
1875
2176
  }
1876
- async function decodeImageBytes2(bytes, format) {
2177
+ async function decodeImageBytes2(bytes, format, channelOrder) {
1877
2178
  if (!isBrowser4 || typeof createImageBitmap === "undefined") return null;
1878
2179
  const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
1879
2180
  const safeBytes = new Uint8Array(bytes);
1880
2181
  const blob = new Blob([safeBytes.buffer], { type: mime });
1881
- 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;
1882
2200
  }
1883
2201
  function decodeRawFrame2(bytes, shape, channelOrder) {
1884
2202
  if (!isBrowser4 || !shape) return null;
@@ -1942,7 +2260,7 @@ async function readSlp(source, options) {
1942
2260
  const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
1943
2261
  const skeletons = parseSkeletons(metadataJson);
1944
2262
  const tracks = readTracks(file.get("tracks_json"));
1945
- 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);
1946
2264
  const suggestions = readSuggestions(file.get("suggestions_json"), videos);
1947
2265
  const framesData = normalizeStructDataset(file.get("frames"));
1948
2266
  const instancesData = normalizeStructDataset(file.get("instances"));
@@ -1995,11 +2313,12 @@ function readTracks(dataset) {
1995
2313
  }
1996
2314
  return tracks;
1997
2315
  }
1998
- async function readVideos(dataset, labelsPath, openVideos, file) {
2316
+ async function readVideos(dataset, labelsPath, openVideos, file, formatId) {
1999
2317
  if (!dataset) return [];
2000
2318
  const values = dataset.value ?? [];
2001
2319
  const videos = [];
2002
- for (const entry of values) {
2320
+ for (let videoIndex = 0; videoIndex < values.length; videoIndex++) {
2321
+ const entry = values[videoIndex];
2003
2322
  if (!entry) continue;
2004
2323
  const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
2005
2324
  const backendMeta = parsed.backend ?? {};
@@ -2010,14 +2329,26 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
2010
2329
  embedded = true;
2011
2330
  filename = labelsPath;
2012
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
+ }
2013
2344
  let backend = null;
2014
2345
  if (openVideos) {
2015
2346
  backend = await createVideoBackend(filename, {
2016
2347
  dataset: datasetPath ?? void 0,
2017
2348
  embedded,
2018
2349
  frameNumbers: readFrameNumbers(file, datasetPath),
2019
- format: backendMeta.format,
2020
- channelOrder: backendMeta.channel_order,
2350
+ format,
2351
+ channelOrder,
2021
2352
  shape: backendMeta.shape,
2022
2353
  fps: backendMeta.fps
2023
2354
  });
@@ -2029,7 +2360,8 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
2029
2360
  backend,
2030
2361
  backendMetadata: backendMeta,
2031
2362
  sourceVideo,
2032
- openBackend: openVideos
2363
+ openBackend: openVideos,
2364
+ embedded
2033
2365
  })
2034
2366
  );
2035
2367
  }
@@ -2043,6 +2375,28 @@ function readFrameNumbers(file, datasetPath) {
2043
2375
  const values = frameDataset.value ?? [];
2044
2376
  return Array.from(values).map((v) => Number(v));
2045
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
+ }
2046
2400
  function readSuggestions(dataset, videos) {
2047
2401
  if (!dataset) return [];
2048
2402
  const values = dataset.value ?? [];
@@ -2322,17 +2676,18 @@ function slicePoints(data, start, end, predicted = false) {
2322
2676
  }
2323
2677
 
2324
2678
  // src/codecs/slp/read-streaming.ts
2325
- async function readSlpStreaming(url, options) {
2679
+ async function readSlpStreaming(source, options) {
2326
2680
  if (!isStreamingSupported()) {
2327
2681
  throw new Error("Streaming HDF5 requires Web Worker support (browser environment)");
2328
2682
  }
2329
- const file = await openStreamingH5(url, {
2683
+ const file = await openH5Worker(source, {
2330
2684
  h5wasmUrl: options?.h5wasmUrl,
2331
2685
  filenameHint: options?.filenameHint
2332
2686
  });
2333
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";
2334
2689
  try {
2335
- return await readFromStreamingFile(file, url, options?.filenameHint, openVideos);
2690
+ return await readFromStreamingFile(file, sourcePath, options?.filenameHint, openVideos);
2336
2691
  } finally {
2337
2692
  if (!openVideos) {
2338
2693
  await file.close();
@@ -2348,7 +2703,7 @@ async function readFromStreamingFile(file, url, filenameHint, openVideos = false
2348
2703
  const labelsPath = filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
2349
2704
  const skeletons = parseSkeletons(metadataJson);
2350
2705
  const tracks = await readTracksStreaming(file);
2351
- const videos = await readVideosStreaming(file, labelsPath, openVideos);
2706
+ const videos = await readVideosStreaming(file, labelsPath, openVideos, formatId);
2352
2707
  const suggestions = await readSuggestionsStreaming(file, videos);
2353
2708
  const framesData = await readStructDatasetStreaming(file, "frames");
2354
2709
  const instancesData = await readStructDatasetStreaming(file, "instances");
@@ -2386,45 +2741,123 @@ async function readTracksStreaming(file) {
2386
2741
  return [];
2387
2742
  }
2388
2743
  }
2389
- async function readVideosStreaming(file, labelsPath, openVideos = false) {
2744
+ async function readVideosStreaming(file, labelsPath, openVideos = false, formatId = 1) {
2390
2745
  try {
2391
2746
  const keys = file.keys();
2392
2747
  if (!keys.includes("videos_json")) return [];
2393
2748
  const data = await file.getDatasetValue("videos_json");
2394
2749
  const values = normalizeDatasetArray(data.value);
2395
2750
  const metadataList = parseVideosMetadata(values, labelsPath);
2396
- return metadataList.map((meta) => {
2751
+ const videos = [];
2752
+ for (let videoIndex = 0; videoIndex < metadataList.length; videoIndex++) {
2753
+ const meta = metadataList[videoIndex];
2397
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
+ }
2398
2771
  let backend = null;
2399
- if (openVideos && meta.embedded && meta.dataset) {
2772
+ if (openVideos && meta.embedded && datasetPath) {
2773
+ const frameNumbers = await readFrameNumbersStreaming(file, datasetPath);
2400
2774
  backend = new StreamingHdf5VideoBackend({
2401
2775
  filename: meta.filename,
2402
2776
  h5file: file,
2403
- datasetPath: meta.dataset,
2404
- format: meta.format ?? "png",
2405
- channelOrder: meta.channelOrder ?? "RGB",
2777
+ datasetPath,
2778
+ frameNumbers,
2779
+ format: format ?? "png",
2780
+ channelOrder,
2406
2781
  shape,
2407
2782
  fps: meta.fps
2408
2783
  });
2409
2784
  }
2410
- return new Video({
2785
+ videos.push(new Video({
2411
2786
  filename: meta.filename,
2412
2787
  backend,
2413
2788
  backendMetadata: {
2414
- dataset: meta.dataset,
2415
- format: meta.format,
2789
+ dataset: datasetPath,
2790
+ format,
2416
2791
  shape,
2417
2792
  fps: meta.fps,
2418
- channel_order: meta.channelOrder
2793
+ channel_order: channelOrder
2419
2794
  },
2420
2795
  sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
2421
- openBackend: openVideos && meta.embedded
2422
- });
2423
- });
2796
+ openBackend: openVideos && meta.embedded,
2797
+ embedded: meta.embedded
2798
+ }));
2799
+ }
2800
+ return videos;
2424
2801
  } catch {
2425
2802
  return [];
2426
2803
  }
2427
2804
  }
2805
+ async function readFrameNumbersStreaming(file, datasetPath) {
2806
+ try {
2807
+ const groupPath = datasetPath.endsWith("/video") ? datasetPath.slice(0, -6) : datasetPath;
2808
+ const frameNumbersPath = `${groupPath}/frame_numbers`;
2809
+ const groupKeys = await file.getKeys(groupPath);
2810
+ if (!groupKeys.includes("frame_numbers")) {
2811
+ return [];
2812
+ }
2813
+ const data = await file.getDatasetValue(frameNumbersPath);
2814
+ const values = data.value;
2815
+ if (Array.isArray(values)) {
2816
+ return values.map((v) => Number(v));
2817
+ }
2818
+ if (ArrayBuffer.isView(values)) {
2819
+ return Array.from(values).map(Number);
2820
+ }
2821
+ return [];
2822
+ } catch {
2823
+ return [];
2824
+ }
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
+ }
2428
2861
  async function readSuggestionsStreaming(file, videos) {
2429
2862
  try {
2430
2863
  const keys = file.keys();
@@ -2936,29 +3369,44 @@ function createMatrixDataset(file, name, rows, fieldNames, dtype) {
2936
3369
  }
2937
3370
 
2938
3371
  // src/io/main.ts
2939
- function isProbablyUrl2(source) {
2940
- return typeof source === "string" && /^https?:\/\//i.test(source);
3372
+ function isNode3() {
3373
+ return typeof process !== "undefined" && !!process.versions?.node;
2941
3374
  }
2942
- function isBrowser5() {
2943
- return typeof window !== "undefined" && typeof Worker !== "undefined";
3375
+ function isBrowserWithWorkerSupport() {
3376
+ return typeof window !== "undefined" && isStreamingSupported();
2944
3377
  }
2945
3378
  async function loadSlp(source, options) {
2946
3379
  const streamMode = options?.h5?.stream ?? "auto";
2947
- if (isProbablyUrl2(source) && isBrowser5() && isStreamingSupported() && (streamMode === "range" || streamMode === "auto")) {
2948
- try {
2949
- return await readSlpStreaming(source, {
2950
- filenameHint: options?.h5?.filenameHint,
2951
- openVideos: options?.openVideos ?? true
2952
- });
2953
- } catch (e) {
2954
- if (streamMode === "auto") {
2955
- console.warn("Streaming failed, falling back to full download:", e);
2956
- } else {
2957
- 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
+ }
2958
3406
  }
2959
3407
  }
2960
3408
  }
2961
- return readSlp(source, { openVideos: options?.openVideos ?? true, h5: options?.h5 });
3409
+ return readSlp(source, { openVideos, h5: options?.h5 });
2962
3410
  }
2963
3411
  async function saveSlp(labels, filename, options) {
2964
3412
  await writeSlp(filename, labels, {
@@ -3850,6 +4298,7 @@ export {
3850
4298
  loadSlp,
3851
4299
  loadVideo,
3852
4300
  makeCameraFromDict,
4301
+ openH5Worker,
3853
4302
  openStreamingH5,
3854
4303
  pointsEmpty,
3855
4304
  pointsFromArray,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talmolab/sleap-io.js",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {