@talmolab/sleap-io.js 0.1.5 → 0.1.7

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
@@ -342,6 +342,43 @@ declare function isStreamingSupported(): boolean;
342
342
  */
343
343
  declare function openStreamingH5(url: string, options?: StreamingH5Options): Promise<StreamingH5File>;
344
344
 
345
+ /**
346
+ * Video backend for embedded images in HDF5 files accessed via streaming.
347
+ *
348
+ * This backend uses StreamingH5File (Web Worker + range requests) instead of
349
+ * a synchronous h5wasm File object, making it suitable for browser environments
350
+ * where the SLP file is loaded via HTTP range requests.
351
+ *
352
+ * Supports two data storage formats:
353
+ * 1. vlen-encoded: Array of individual frame blobs (each index = one frame)
354
+ * 2. Contiguous buffer: Single buffer with all frames concatenated
355
+ */
356
+ declare class StreamingHdf5VideoBackend implements VideoBackend {
357
+ filename: string;
358
+ dataset?: string | null;
359
+ shape?: [number, number, number, number];
360
+ fps?: number;
361
+ private h5file;
362
+ private datasetPath;
363
+ private frameNumbers;
364
+ private format;
365
+ private channelOrder;
366
+ private cachedData;
367
+ private frameOffsets;
368
+ constructor(options: {
369
+ filename: string;
370
+ h5file: StreamingH5File;
371
+ datasetPath: string;
372
+ frameNumbers?: number[];
373
+ format?: string;
374
+ channelOrder?: string;
375
+ shape?: [number, number, number, number];
376
+ fps?: number;
377
+ });
378
+ getFrame(frameIndex: number): Promise<VideoFrame | null>;
379
+ close(): void;
380
+ }
381
+
345
382
  type SlpSource = string | ArrayBuffer | Uint8Array | File | FileSystemFileHandle;
346
383
  type StreamMode = "auto" | "range" | "download";
347
384
  type OpenH5Options = {
@@ -702,12 +739,18 @@ interface StreamingSlpOptions {
702
739
  h5wasmUrl?: string;
703
740
  /** Filename hint for the HDF5 file */
704
741
  filenameHint?: string;
742
+ /** Whether to open video backends for embedded videos (default: false) */
743
+ openVideos?: boolean;
705
744
  }
706
745
  /**
707
746
  * Read an SLP file using HTTP range requests for efficient streaming.
708
747
  *
709
748
  * This function downloads only the data needed (metadata, frames, instances, points)
710
- * rather than the entire file. Embedded videos are NOT loaded - only metadata.
749
+ * rather than the entire file.
750
+ *
751
+ * When `openVideos` is true, video backends are created for embedded videos,
752
+ * allowing frame data to be retrieved. The underlying HDF5 file remains open
753
+ * until all video backends are closed.
711
754
  *
712
755
  * @param url - URL to the SLP file (must support HTTP range requests)
713
756
  * @param options - Optional settings
@@ -715,10 +758,13 @@ interface StreamingSlpOptions {
715
758
  *
716
759
  * @example
717
760
  * ```typescript
718
- * const labels = await readSlpStreaming('https://example.com/labels.slp');
719
- * console.log(`Loaded ${labels.labeledFrames.length} frames`);
761
+ * // Load with video backends for embedded images
762
+ * const labels = await readSlpStreaming('https://example.com/labels.slp', {
763
+ * openVideos: true
764
+ * });
765
+ * const frame = await labels.video.getFrame(0);
720
766
  * ```
721
767
  */
722
768
  declare function readSlpStreaming(url: string, options?: StreamingSlpOptions): Promise<Labels>;
723
769
 
724
- 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, 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 };
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 };
package/dist/index.js CHANGED
@@ -1206,6 +1206,168 @@ var Mp4BoxVideoBackend = class {
1206
1206
  }
1207
1207
  };
1208
1208
 
1209
+ // src/video/streaming-hdf5-video.ts
1210
+ var isBrowser3 = typeof window !== "undefined" && typeof document !== "undefined";
1211
+ var PNG_MAGIC = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
1212
+ var JPEG_MAGIC = new Uint8Array([255, 216, 255]);
1213
+ var StreamingHdf5VideoBackend = class {
1214
+ filename;
1215
+ dataset;
1216
+ shape;
1217
+ fps;
1218
+ h5file;
1219
+ datasetPath;
1220
+ frameNumbers;
1221
+ format;
1222
+ channelOrder;
1223
+ cachedData;
1224
+ frameOffsets;
1225
+ // For contiguous buffer: byte offsets of each frame
1226
+ constructor(options) {
1227
+ this.filename = options.filename;
1228
+ this.h5file = options.h5file;
1229
+ this.datasetPath = options.datasetPath;
1230
+ this.dataset = options.datasetPath;
1231
+ this.frameNumbers = options.frameNumbers ?? [];
1232
+ this.format = options.format ?? "png";
1233
+ this.channelOrder = options.channelOrder ?? "RGB";
1234
+ this.shape = options.shape;
1235
+ this.fps = options.fps;
1236
+ this.cachedData = null;
1237
+ this.frameOffsets = null;
1238
+ }
1239
+ async getFrame(frameIndex) {
1240
+ const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1241
+ if (index < 0) return null;
1242
+ if (!this.cachedData) {
1243
+ try {
1244
+ const data = await this.h5file.getDatasetValue(this.datasetPath);
1245
+ this.cachedData = normalizeVideoData(data.value, data.shape);
1246
+ if (isContiguousEncodedBuffer(this.cachedData, this.format, this.shape)) {
1247
+ this.frameOffsets = findEncodedFrameOffsets(
1248
+ this.cachedData,
1249
+ this.format,
1250
+ this.shape?.[0] ?? 0
1251
+ );
1252
+ }
1253
+ } catch {
1254
+ return null;
1255
+ }
1256
+ }
1257
+ let rawBytes;
1258
+ if (this.frameOffsets && this.frameOffsets.length > index) {
1259
+ const buffer = this.cachedData;
1260
+ const start = this.frameOffsets[index];
1261
+ const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
1262
+ rawBytes = buffer.slice(start, end);
1263
+ } else {
1264
+ const entry = this.cachedData[index];
1265
+ if (entry == null) return null;
1266
+ rawBytes = toUint8Array(entry);
1267
+ }
1268
+ if (!rawBytes || rawBytes.length === 0) return null;
1269
+ if (isEncodedFormat(this.format)) {
1270
+ const decoded = await decodeImageBytes(rawBytes, this.format);
1271
+ return decoded ?? rawBytes;
1272
+ }
1273
+ const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
1274
+ return image ?? rawBytes;
1275
+ }
1276
+ close() {
1277
+ this.cachedData = null;
1278
+ this.frameOffsets = null;
1279
+ }
1280
+ };
1281
+ function normalizeVideoData(value, _shape) {
1282
+ if (Array.isArray(value)) {
1283
+ return value;
1284
+ }
1285
+ if (ArrayBuffer.isView(value)) {
1286
+ const arr = value;
1287
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
1288
+ }
1289
+ return [];
1290
+ }
1291
+ function isContiguousEncodedBuffer(data, format, shape) {
1292
+ if (!isEncodedFormat(format)) return false;
1293
+ if (!(data instanceof Uint8Array)) return false;
1294
+ if (data.length < 8) return false;
1295
+ const isPng = matchesMagic(data, PNG_MAGIC);
1296
+ const isJpeg = matchesMagic(data, JPEG_MAGIC);
1297
+ if (!isPng && !isJpeg) return false;
1298
+ if (shape) {
1299
+ const frameCount = shape[0];
1300
+ if (frameCount > 1 && data.length > 1e4) {
1301
+ return true;
1302
+ }
1303
+ }
1304
+ return true;
1305
+ }
1306
+ function matchesMagic(buffer, magic) {
1307
+ if (buffer.length < magic.length) return false;
1308
+ for (let i = 0; i < magic.length; i++) {
1309
+ if (buffer[i] !== magic[i]) return false;
1310
+ }
1311
+ return true;
1312
+ }
1313
+ function findEncodedFrameOffsets(buffer, format, expectedFrameCount) {
1314
+ const offsets = [];
1315
+ const magic = format.toLowerCase() === "png" ? PNG_MAGIC : JPEG_MAGIC;
1316
+ for (let i = 0; i <= buffer.length - magic.length; i++) {
1317
+ if (matchesMagic(buffer.subarray(i), magic)) {
1318
+ offsets.push(i);
1319
+ i += magic.length - 1;
1320
+ if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
1321
+ break;
1322
+ }
1323
+ }
1324
+ }
1325
+ return offsets;
1326
+ }
1327
+ function toUint8Array(entry) {
1328
+ if (entry instanceof Uint8Array) return entry;
1329
+ if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
1330
+ if (ArrayBuffer.isView(entry)) return new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
1331
+ if (Array.isArray(entry)) return new Uint8Array(entry.flat());
1332
+ if (entry && typeof entry === "object" && "buffer" in entry) {
1333
+ return new Uint8Array(entry.buffer);
1334
+ }
1335
+ return null;
1336
+ }
1337
+ function isEncodedFormat(format) {
1338
+ const normalized = format.toLowerCase();
1339
+ return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
1340
+ }
1341
+ async function decodeImageBytes(bytes, format) {
1342
+ if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
1343
+ const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
1344
+ const safeBytes = new Uint8Array(bytes);
1345
+ const blob = new Blob([safeBytes.buffer], { type: mime });
1346
+ return createImageBitmap(blob);
1347
+ }
1348
+ function decodeRawFrame(bytes, shape, channelOrder) {
1349
+ if (!isBrowser3 || !shape) return null;
1350
+ const [, height, width, channels] = shape;
1351
+ if (!height || !width || !channels) return null;
1352
+ const expectedLength = height * width * channels;
1353
+ if (bytes.length < expectedLength) return null;
1354
+ const rgba = new Uint8ClampedArray(width * height * 4);
1355
+ const useBgr = channelOrder.toUpperCase() === "BGR";
1356
+ for (let i = 0; i < width * height; i += 1) {
1357
+ const base = i * channels;
1358
+ const r = bytes[base + (useBgr ? 2 : 0)] ?? 0;
1359
+ const g = bytes[base + 1] ?? 0;
1360
+ const b = bytes[base + (useBgr ? 0 : 2)] ?? 0;
1361
+ const a = channels === 4 ? bytes[base + 3] ?? 255 : 255;
1362
+ const out = i * 4;
1363
+ rgba[out] = r;
1364
+ rgba[out + 1] = g;
1365
+ rgba[out + 2] = b;
1366
+ rgba[out + 3] = a;
1367
+ }
1368
+ return new ImageData(rgba, width, height);
1369
+ }
1370
+
1209
1371
  // src/codecs/slp/h5-worker.ts
1210
1372
  var H5_WORKER_CODE = `
1211
1373
  // h5wasm streaming worker
@@ -1719,7 +1881,9 @@ function getH5FileSystem(module) {
1719
1881
  }
1720
1882
 
1721
1883
  // src/video/hdf5-video.ts
1722
- var isBrowser3 = typeof window !== "undefined" && typeof document !== "undefined";
1884
+ var isBrowser4 = typeof window !== "undefined" && typeof document !== "undefined";
1885
+ var PNG_MAGIC2 = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
1886
+ var JPEG_MAGIC2 = new Uint8Array([255, 216, 255]);
1723
1887
  var Hdf5VideoBackend = class {
1724
1888
  filename;
1725
1889
  dataset;
@@ -1731,6 +1895,7 @@ var Hdf5VideoBackend = class {
1731
1895
  format;
1732
1896
  channelOrder;
1733
1897
  cachedData;
1898
+ frameOffsets;
1734
1899
  constructor(options) {
1735
1900
  this.filename = options.filename;
1736
1901
  this.file = options.file;
@@ -1742,6 +1907,7 @@ var Hdf5VideoBackend = class {
1742
1907
  this.shape = options.shape;
1743
1908
  this.fps = options.fps;
1744
1909
  this.cachedData = null;
1910
+ this.frameOffsets = null;
1745
1911
  }
1746
1912
  async getFrame(frameIndex) {
1747
1913
  const dataset = this.file.get(this.datasetPath);
@@ -1749,24 +1915,87 @@ var Hdf5VideoBackend = class {
1749
1915
  const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
1750
1916
  if (index < 0) return null;
1751
1917
  if (!this.cachedData) {
1752
- this.cachedData = dataset.value;
1918
+ const value = dataset.value;
1919
+ this.cachedData = normalizeVideoData2(value);
1920
+ if (isContiguousEncodedBuffer2(this.cachedData, this.format, this.shape)) {
1921
+ this.frameOffsets = findEncodedFrameOffsets2(
1922
+ this.cachedData,
1923
+ this.format,
1924
+ this.shape?.[0] ?? 0
1925
+ );
1926
+ }
1753
1927
  }
1754
- const entry = this.cachedData[index];
1755
- if (entry == null) return null;
1756
- const rawBytes = toUint8Array(entry);
1757
- if (!rawBytes) return null;
1758
- if (isEncodedFormat(this.format)) {
1759
- const decoded = await decodeImageBytes(rawBytes, this.format);
1928
+ let rawBytes;
1929
+ if (this.frameOffsets && this.frameOffsets.length > index) {
1930
+ const buffer = this.cachedData;
1931
+ const start = this.frameOffsets[index];
1932
+ const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
1933
+ rawBytes = buffer.slice(start, end);
1934
+ } else {
1935
+ const entry = this.cachedData[index];
1936
+ if (entry == null) return null;
1937
+ rawBytes = toUint8Array2(entry);
1938
+ }
1939
+ if (!rawBytes || rawBytes.length === 0) return null;
1940
+ if (isEncodedFormat2(this.format)) {
1941
+ const decoded = await decodeImageBytes2(rawBytes, this.format);
1760
1942
  return decoded ?? rawBytes;
1761
1943
  }
1762
- const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
1944
+ const image = decodeRawFrame2(rawBytes, this.shape, this.channelOrder);
1763
1945
  return image ?? rawBytes;
1764
1946
  }
1765
1947
  close() {
1766
1948
  this.cachedData = null;
1949
+ this.frameOffsets = null;
1767
1950
  }
1768
1951
  };
1769
- function toUint8Array(entry) {
1952
+ function normalizeVideoData2(value) {
1953
+ if (Array.isArray(value)) {
1954
+ return value;
1955
+ }
1956
+ if (ArrayBuffer.isView(value)) {
1957
+ const arr = value;
1958
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
1959
+ }
1960
+ return [];
1961
+ }
1962
+ function isContiguousEncodedBuffer2(data, format, shape) {
1963
+ if (!isEncodedFormat2(format)) return false;
1964
+ if (!(data instanceof Uint8Array)) return false;
1965
+ if (data.length < 8) return false;
1966
+ const isPng = matchesMagic2(data, PNG_MAGIC2);
1967
+ const isJpeg = matchesMagic2(data, JPEG_MAGIC2);
1968
+ if (!isPng && !isJpeg) return false;
1969
+ if (shape) {
1970
+ const frameCount = shape[0];
1971
+ if (frameCount > 1 && data.length > 1e4) {
1972
+ return true;
1973
+ }
1974
+ }
1975
+ return true;
1976
+ }
1977
+ function matchesMagic2(buffer, magic) {
1978
+ if (buffer.length < magic.length) return false;
1979
+ for (let i = 0; i < magic.length; i++) {
1980
+ if (buffer[i] !== magic[i]) return false;
1981
+ }
1982
+ return true;
1983
+ }
1984
+ function findEncodedFrameOffsets2(buffer, format, expectedFrameCount) {
1985
+ const offsets = [];
1986
+ const magic = format.toLowerCase() === "png" ? PNG_MAGIC2 : JPEG_MAGIC2;
1987
+ for (let i = 0; i <= buffer.length - magic.length; i++) {
1988
+ if (matchesMagic2(buffer.subarray(i), magic)) {
1989
+ offsets.push(i);
1990
+ i += magic.length - 1;
1991
+ if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
1992
+ break;
1993
+ }
1994
+ }
1995
+ }
1996
+ return offsets;
1997
+ }
1998
+ function toUint8Array2(entry) {
1770
1999
  if (entry instanceof Uint8Array) return entry;
1771
2000
  if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
1772
2001
  if (ArrayBuffer.isView(entry)) return new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
@@ -1774,19 +2003,19 @@ function toUint8Array(entry) {
1774
2003
  if (entry?.buffer) return new Uint8Array(entry.buffer);
1775
2004
  return null;
1776
2005
  }
1777
- function isEncodedFormat(format) {
2006
+ function isEncodedFormat2(format) {
1778
2007
  const normalized = format.toLowerCase();
1779
2008
  return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
1780
2009
  }
1781
- async function decodeImageBytes(bytes, format) {
1782
- if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
2010
+ async function decodeImageBytes2(bytes, format) {
2011
+ if (!isBrowser4 || typeof createImageBitmap === "undefined") return null;
1783
2012
  const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
1784
2013
  const safeBytes = new Uint8Array(bytes);
1785
2014
  const blob = new Blob([safeBytes.buffer], { type: mime });
1786
2015
  return createImageBitmap(blob);
1787
2016
  }
1788
- function decodeRawFrame(bytes, shape, channelOrder) {
1789
- if (!isBrowser3 || !shape) return null;
2017
+ function decodeRawFrame2(bytes, shape, channelOrder) {
2018
+ if (!isBrowser4 || !shape) return null;
1790
2019
  const [, height, width, channels] = shape;
1791
2020
  if (!height || !width || !channels) return null;
1792
2021
  const expectedLength = height * width * channels;
@@ -2235,13 +2464,16 @@ async function readSlpStreaming(url, options) {
2235
2464
  h5wasmUrl: options?.h5wasmUrl,
2236
2465
  filenameHint: options?.filenameHint
2237
2466
  });
2467
+ const openVideos = options?.openVideos ?? false;
2238
2468
  try {
2239
- return await readFromStreamingFile(file, url, options?.filenameHint);
2469
+ return await readFromStreamingFile(file, url, options?.filenameHint, openVideos);
2240
2470
  } finally {
2241
- await file.close();
2471
+ if (!openVideos) {
2472
+ await file.close();
2473
+ }
2242
2474
  }
2243
2475
  }
2244
- async function readFromStreamingFile(file, url, filenameHint) {
2476
+ async function readFromStreamingFile(file, url, filenameHint, openVideos = false) {
2245
2477
  const metadataAttrs = await file.getAttrs("metadata");
2246
2478
  const formatId = Number(
2247
2479
  metadataAttrs["format_id"]?.value ?? metadataAttrs["format_id"] ?? 1
@@ -2250,7 +2482,7 @@ async function readFromStreamingFile(file, url, filenameHint) {
2250
2482
  const labelsPath = filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
2251
2483
  const skeletons = parseSkeletons(metadataJson);
2252
2484
  const tracks = await readTracksStreaming(file);
2253
- const videos = await readVideosStreaming(file, labelsPath);
2485
+ const videos = await readVideosStreaming(file, labelsPath, openVideos);
2254
2486
  const suggestions = await readSuggestionsStreaming(file, videos);
2255
2487
  const framesData = await readStructDatasetStreaming(file, "frames");
2256
2488
  const instancesData = await readStructDatasetStreaming(file, "instances");
@@ -2288,27 +2520,66 @@ async function readTracksStreaming(file) {
2288
2520
  return [];
2289
2521
  }
2290
2522
  }
2291
- async function readVideosStreaming(file, labelsPath) {
2523
+ async function readVideosStreaming(file, labelsPath, openVideos = false) {
2292
2524
  try {
2293
2525
  const keys = file.keys();
2294
2526
  if (!keys.includes("videos_json")) return [];
2295
2527
  const data = await file.getDatasetValue("videos_json");
2296
2528
  const values = normalizeDatasetArray(data.value);
2297
2529
  const metadataList = parseVideosMetadata(values, labelsPath);
2298
- return metadataList.map((meta) => new Video({
2299
- filename: meta.filename,
2300
- backend: null,
2301
- // No backend in streaming mode
2302
- backendMetadata: {
2303
- dataset: meta.dataset,
2304
- format: meta.format,
2305
- shape: meta.frameCount && meta.height && meta.width && meta.channels ? [meta.frameCount, meta.height, meta.width, meta.channels] : void 0,
2306
- fps: meta.fps,
2307
- channel_order: meta.channelOrder
2308
- },
2309
- sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
2310
- openBackend: false
2311
- }));
2530
+ const videos = [];
2531
+ for (const meta of metadataList) {
2532
+ const shape = meta.frameCount && meta.height && meta.width && meta.channels ? [meta.frameCount, meta.height, meta.width, meta.channels] : void 0;
2533
+ let backend = null;
2534
+ if (openVideos && meta.embedded && meta.dataset) {
2535
+ const frameNumbers = await readFrameNumbersStreaming(file, meta.dataset);
2536
+ backend = new StreamingHdf5VideoBackend({
2537
+ filename: meta.filename,
2538
+ h5file: file,
2539
+ datasetPath: meta.dataset,
2540
+ frameNumbers,
2541
+ format: meta.format ?? "png",
2542
+ channelOrder: meta.channelOrder ?? "RGB",
2543
+ shape,
2544
+ fps: meta.fps
2545
+ });
2546
+ }
2547
+ videos.push(new Video({
2548
+ filename: meta.filename,
2549
+ backend,
2550
+ backendMetadata: {
2551
+ dataset: meta.dataset,
2552
+ format: meta.format,
2553
+ shape,
2554
+ fps: meta.fps,
2555
+ channel_order: meta.channelOrder
2556
+ },
2557
+ sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
2558
+ openBackend: openVideos && meta.embedded
2559
+ }));
2560
+ }
2561
+ return videos;
2562
+ } catch {
2563
+ return [];
2564
+ }
2565
+ }
2566
+ async function readFrameNumbersStreaming(file, datasetPath) {
2567
+ try {
2568
+ const groupPath = datasetPath.endsWith("/video") ? datasetPath.slice(0, -6) : datasetPath;
2569
+ const frameNumbersPath = `${groupPath}/frame_numbers`;
2570
+ const groupKeys = await file.getKeys(groupPath);
2571
+ if (!groupKeys.includes("frame_numbers")) {
2572
+ return [];
2573
+ }
2574
+ const data = await file.getDatasetValue(frameNumbersPath);
2575
+ const values = data.value;
2576
+ if (Array.isArray(values)) {
2577
+ return values.map((v) => Number(v));
2578
+ }
2579
+ if (ArrayBuffer.isView(values)) {
2580
+ return Array.from(values).map(Number);
2581
+ }
2582
+ return [];
2312
2583
  } catch {
2313
2584
  return [];
2314
2585
  }
@@ -2827,15 +3098,16 @@ function createMatrixDataset(file, name, rows, fieldNames, dtype) {
2827
3098
  function isProbablyUrl2(source) {
2828
3099
  return typeof source === "string" && /^https?:\/\//i.test(source);
2829
3100
  }
2830
- function isBrowser4() {
3101
+ function isBrowser5() {
2831
3102
  return typeof window !== "undefined" && typeof Worker !== "undefined";
2832
3103
  }
2833
3104
  async function loadSlp(source, options) {
2834
3105
  const streamMode = options?.h5?.stream ?? "auto";
2835
- if (isProbablyUrl2(source) && isBrowser4() && isStreamingSupported() && (streamMode === "range" || streamMode === "auto")) {
3106
+ if (isProbablyUrl2(source) && isBrowser5() && isStreamingSupported() && (streamMode === "range" || streamMode === "auto")) {
2836
3107
  try {
2837
3108
  return await readSlpStreaming(source, {
2838
- filenameHint: options?.h5?.filenameHint
3109
+ filenameHint: options?.h5?.filenameHint,
3110
+ openVideos: options?.openVideos ?? true
2839
3111
  });
2840
3112
  } catch (e) {
2841
3113
  if (streamMode === "auto") {
@@ -3714,6 +3986,7 @@ export {
3714
3986
  RenderContext,
3715
3987
  Skeleton,
3716
3988
  StreamingH5File,
3989
+ StreamingHdf5VideoBackend,
3717
3990
  SuggestionFrame,
3718
3991
  Symmetry,
3719
3992
  Track,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talmolab/sleap-io.js",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {