@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 +50 -4
- package/dist/index.js +310 -37
- package/package.json +1 -1
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.
|
|
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
|
-
*
|
|
719
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1755
|
-
if (
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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 =
|
|
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
|
|
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
|
|
2006
|
+
function isEncodedFormat2(format) {
|
|
1778
2007
|
const normalized = format.toLowerCase();
|
|
1779
2008
|
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
1780
2009
|
}
|
|
1781
|
-
async function
|
|
1782
|
-
if (!
|
|
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
|
|
1789
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
|
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) &&
|
|
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,
|