retold-remote 0.0.23 → 0.0.26
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/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -159,16 +159,17 @@ function _buildPipelineOperation(pConfig)
|
|
|
159
159
|
Address: '{~D:Record.Operation.' + pConfig.AddressParam + '~}'
|
|
160
160
|
},
|
|
161
161
|
['Address'],
|
|
162
|
-
['URL', 'Filename', 'BeaconName']),
|
|
162
|
+
['URL', 'Filename', 'BeaconName', 'LocalPath', 'Strategy']),
|
|
163
163
|
|
|
164
164
|
_taskNode(tmpTransferHash, 'file-transfer', 'File Transfer', 490, 180,
|
|
165
165
|
{
|
|
166
166
|
SourceURL: '{~D:Record.TaskOutputs.' + tmpResolveHash + '.URL~}',
|
|
167
|
+
SourceLocalPath: '{~D:Record.TaskOutputs.' + tmpResolveHash + '.LocalPath~}',
|
|
167
168
|
Filename: '{~D:Record.TaskOutputs.' + tmpResolveHash + '.Filename~}',
|
|
168
169
|
TimeoutMs: pConfig.TransferTimeoutMs || 300000
|
|
169
170
|
},
|
|
170
|
-
['SourceURL', 'Filename', 'TimeoutMs'],
|
|
171
|
-
['LocalPath', 'BytesTransferred', 'DurationMs']),
|
|
171
|
+
['SourceURL', 'SourceLocalPath', 'Filename', 'TimeoutMs'],
|
|
172
|
+
['LocalPath', 'BytesTransferred', 'DurationMs', 'Strategy']),
|
|
172
173
|
|
|
173
174
|
_taskNode(tmpProcessHash, pConfig.ProcessType, pConfig.ProcessTitle, 760, 180,
|
|
174
175
|
pConfig.ProcessData,
|
|
@@ -207,6 +208,16 @@ function _buildPipelineOperation(pConfig)
|
|
|
207
208
|
Data: {}
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
// Resolve LocalPath → transfer SourceLocalPath (shared-fs zero-copy fast path)
|
|
212
|
+
tmpConnections.push({
|
|
213
|
+
Hash: tmpHash + '-s4',
|
|
214
|
+
SourceNodeHash: tmpResolveHash,
|
|
215
|
+
SourcePortHash: tmpResolveHash + '-so-LocalPath',
|
|
216
|
+
TargetNodeHash: tmpTransferHash,
|
|
217
|
+
TargetPortHash: tmpTransferHash + '-si-SourceLocalPath',
|
|
218
|
+
Data: {}
|
|
219
|
+
});
|
|
220
|
+
|
|
210
221
|
// Transfer LocalPath → process InputFile
|
|
211
222
|
tmpConnections.push({
|
|
212
223
|
Hash: tmpHash + '-s3',
|
|
@@ -299,14 +310,15 @@ function getOperations()
|
|
|
299
310
|
_startNode(tmpVfe, 50, 200),
|
|
300
311
|
_taskNode(tmpVfe + '-resolve', 'resolve-address', 'Resolve Address', 220, 180,
|
|
301
312
|
{ Address: '{~D:Record.Operation.VideoAddress~}' },
|
|
302
|
-
['Address'], ['URL', 'Filename', 'BeaconName']),
|
|
313
|
+
['Address'], ['URL', 'Filename', 'BeaconName', 'LocalPath', 'Strategy']),
|
|
303
314
|
_taskNode(tmpVfe + '-transfer', 'file-transfer', 'File Transfer', 440, 180,
|
|
304
315
|
{
|
|
305
316
|
SourceURL: '{~D:Record.TaskOutputs.' + tmpVfe + '-resolve.URL~}',
|
|
317
|
+
SourceLocalPath: '{~D:Record.TaskOutputs.' + tmpVfe + '-resolve.LocalPath~}',
|
|
306
318
|
Filename: '{~D:Record.TaskOutputs.' + tmpVfe + '-resolve.Filename~}',
|
|
307
319
|
TimeoutMs: 1800000
|
|
308
320
|
},
|
|
309
|
-
['SourceURL', 'Filename', 'TimeoutMs'], ['LocalPath', 'BytesTransferred', 'DurationMs']),
|
|
321
|
+
['SourceURL', 'SourceLocalPath', 'Filename', 'TimeoutMs'], ['LocalPath', 'BytesTransferred', 'DurationMs', 'Strategy']),
|
|
310
322
|
_taskNode(tmpVfe + '-probe', 'beacon-mediaconversion-mediaprobe', 'Probe Video', 660, 180,
|
|
311
323
|
{
|
|
312
324
|
AffinityKey: '{~D:Record.Operation.VideoAddress~}',
|
|
@@ -335,6 +347,8 @@ function getOperations()
|
|
|
335
347
|
tmpVfeConns.push({ Hash: tmpVfe + '-s2', SourceNodeHash: tmpVfe + '-resolve', SourcePortHash: tmpVfe + '-resolve-so-Filename', TargetNodeHash: tmpVfe + '-transfer', TargetPortHash: tmpVfe + '-transfer-si-Filename', Data: {} });
|
|
336
348
|
tmpVfeConns.push({ Hash: tmpVfe + '-s3', SourceNodeHash: tmpVfe + '-transfer', SourcePortHash: tmpVfe + '-transfer-so-LocalPath', TargetNodeHash: tmpVfe + '-probe', TargetPortHash: tmpVfe + '-probe-si-InputFile', Data: {} });
|
|
337
349
|
tmpVfeConns.push({ Hash: tmpVfe + '-s4', SourceNodeHash: tmpVfe + '-transfer', SourcePortHash: tmpVfe + '-transfer-so-LocalPath', TargetNodeHash: tmpVfe + '-extract', TargetPortHash: tmpVfe + '-extract-si-InputFile', Data: {} });
|
|
350
|
+
// Resolve LocalPath → transfer SourceLocalPath (shared-fs zero-copy fast path)
|
|
351
|
+
tmpVfeConns.push({ Hash: tmpVfe + '-s5', SourceNodeHash: tmpVfe + '-resolve', SourcePortHash: tmpVfe + '-resolve-so-LocalPath', TargetNodeHash: tmpVfe + '-transfer', TargetPortHash: tmpVfe + '-transfer-si-SourceLocalPath', Data: {} });
|
|
338
352
|
tmpOperations.push({
|
|
339
353
|
Hash: tmpVfe,
|
|
340
354
|
Name: 'Video Frame Extraction',
|
|
@@ -348,6 +362,72 @@ function getOperations()
|
|
|
348
362
|
InitialOperationState: {}
|
|
349
363
|
});
|
|
350
364
|
|
|
365
|
+
// ── 3b. Video Frames (BATCH) ────────────────────────────────
|
|
366
|
+
// Single dispatch that resolves the address once, transfers the file
|
|
367
|
+
// once (with shared-fs zero-copy when available), and extracts ALL N
|
|
368
|
+
// frames in a single beacon work item. Used by the video explorer to
|
|
369
|
+
// avoid the previous N×operation-graph dispatch storm where N frames
|
|
370
|
+
// produced 21 separate file transfers (1 probe + N extracts).
|
|
371
|
+
//
|
|
372
|
+
// Trigger Parameters:
|
|
373
|
+
// { VideoAddress, OutputDir, Frames, Width }
|
|
374
|
+
// OutputDir — absolute writable directory where frames will land. Set
|
|
375
|
+
// to retold-remote's per-video cache directory; works in
|
|
376
|
+
// stack mode (shared filesystem) and on multi-host setups
|
|
377
|
+
// that bind-mount the same content tree.
|
|
378
|
+
// Frames — JSON-encoded [{ Timestamp, Filename }, ...]
|
|
379
|
+
//
|
|
380
|
+
// Returns (no send-result; read TaskOutputs[<extract-node>].Result):
|
|
381
|
+
// JSON string with { FrameCount, SuccessCount, OutputDir, Frames: [...] }
|
|
382
|
+
let tmpVfb = 'rr-video-frames-batch';
|
|
383
|
+
let tmpVfbNodes = [
|
|
384
|
+
_startNode(tmpVfb, 50, 200),
|
|
385
|
+
_taskNode(tmpVfb + '-resolve', 'resolve-address', 'Resolve Address', 220, 180,
|
|
386
|
+
{ Address: '{~D:Record.Operation.VideoAddress~}' },
|
|
387
|
+
['Address'], ['URL', 'Filename', 'BeaconName', 'LocalPath', 'Strategy']),
|
|
388
|
+
_taskNode(tmpVfb + '-transfer', 'file-transfer', 'File Transfer', 440, 180,
|
|
389
|
+
{
|
|
390
|
+
SourceURL: '{~D:Record.TaskOutputs.' + tmpVfb + '-resolve.URL~}',
|
|
391
|
+
SourceLocalPath: '{~D:Record.TaskOutputs.' + tmpVfb + '-resolve.LocalPath~}',
|
|
392
|
+
Filename: '{~D:Record.TaskOutputs.' + tmpVfb + '-resolve.Filename~}',
|
|
393
|
+
TimeoutMs: 1800000
|
|
394
|
+
},
|
|
395
|
+
['SourceURL', 'SourceLocalPath', 'Filename', 'TimeoutMs'],
|
|
396
|
+
['LocalPath', 'BytesTransferred', 'DurationMs', 'Strategy']),
|
|
397
|
+
_taskNode(tmpVfb + '-extract', 'beacon-mediaconversion-videoextractframes', 'Extract Frames Batch', 660, 180,
|
|
398
|
+
{
|
|
399
|
+
OutputDir: '{~D:Record.Operation.OutputDir~}',
|
|
400
|
+
Frames: '{~D:Record.Operation.Frames~}',
|
|
401
|
+
Width: '{~D:Record.Operation.Width~}',
|
|
402
|
+
AffinityKey: '{~D:Record.Operation.VideoAddress~}',
|
|
403
|
+
TimeoutMs: 1800000
|
|
404
|
+
},
|
|
405
|
+
['InputFile', 'OutputDir', 'Frames', 'Width'],
|
|
406
|
+
['Result', 'StdOut']),
|
|
407
|
+
_endNode(tmpVfb, 880, 260)
|
|
408
|
+
];
|
|
409
|
+
let tmpVfbConns = _chainConnections(tmpVfb,
|
|
410
|
+
[tmpVfb + '-start', tmpVfb + '-resolve', tmpVfb + '-transfer', tmpVfb + '-extract'],
|
|
411
|
+
tmpVfb + '-end');
|
|
412
|
+
// State wires
|
|
413
|
+
tmpVfbConns.push({ Hash: tmpVfb + '-s1', SourceNodeHash: tmpVfb + '-resolve', SourcePortHash: tmpVfb + '-resolve-so-URL', TargetNodeHash: tmpVfb + '-transfer', TargetPortHash: tmpVfb + '-transfer-si-SourceURL', Data: {} });
|
|
414
|
+
tmpVfbConns.push({ Hash: tmpVfb + '-s2', SourceNodeHash: tmpVfb + '-resolve', SourcePortHash: tmpVfb + '-resolve-so-Filename', TargetNodeHash: tmpVfb + '-transfer', TargetPortHash: tmpVfb + '-transfer-si-Filename', Data: {} });
|
|
415
|
+
tmpVfbConns.push({ Hash: tmpVfb + '-s3', SourceNodeHash: tmpVfb + '-transfer', SourcePortHash: tmpVfb + '-transfer-so-LocalPath', TargetNodeHash: tmpVfb + '-extract', TargetPortHash: tmpVfb + '-extract-si-InputFile', Data: {} });
|
|
416
|
+
// Resolve LocalPath → transfer SourceLocalPath (shared-fs zero-copy fast path)
|
|
417
|
+
tmpVfbConns.push({ Hash: tmpVfb + '-s4', SourceNodeHash: tmpVfb + '-resolve', SourcePortHash: tmpVfb + '-resolve-so-LocalPath', TargetNodeHash: tmpVfb + '-transfer', TargetPortHash: tmpVfb + '-transfer-si-SourceLocalPath', Data: {} });
|
|
418
|
+
tmpOperations.push({
|
|
419
|
+
Hash: tmpVfb,
|
|
420
|
+
Name: 'Video Frames Batch',
|
|
421
|
+
Description: 'Single-dispatch batch frame extraction. resolve → transfer → extract-many. Trigger with Parameters: { VideoAddress, OutputDir, Frames (JSON), Width }. Result is JSON metadata only — frames are written to OutputDir and read directly by the dispatcher.',
|
|
422
|
+
Tags: ['retold-remote', 'media', 'video', 'frames', 'batch'],
|
|
423
|
+
Author: 'retold-remote',
|
|
424
|
+
Version: '3.0.0',
|
|
425
|
+
Graph: { Nodes: tmpVfbNodes, Connections: tmpVfbConns, ViewState: { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null } },
|
|
426
|
+
SavedLayouts: [],
|
|
427
|
+
InitialGlobalState: {},
|
|
428
|
+
InitialOperationState: {}
|
|
429
|
+
});
|
|
430
|
+
|
|
351
431
|
// ── 4. Audio Waveform ───────────────────────────────────────
|
|
352
432
|
tmpOperations.push(_buildPipelineOperation(
|
|
353
433
|
{
|
|
@@ -461,23 +541,56 @@ function getOperations()
|
|
|
461
541
|
}));
|
|
462
542
|
|
|
463
543
|
// ── 9. Media Probe ─────────────────────────────────────────
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
544
|
+
// Custom graph (NOT _buildPipelineOperation) because MediaProbe returns
|
|
545
|
+
// its result as a JSON string in the work item's Outputs.Result field —
|
|
546
|
+
// it does NOT write a file to staging. The pipeline helper would tack on
|
|
547
|
+
// a send-result node looking for `probe.json` and log a noisy error every
|
|
548
|
+
// time the operation runs. Skip send-result entirely; the dispatcher
|
|
549
|
+
// reads the JSON directly from `pResult.TaskOutputs['rr-media-probe-process'].Result`.
|
|
550
|
+
let tmpMp = 'rr-media-probe';
|
|
551
|
+
let tmpMpNodes = [
|
|
552
|
+
_startNode(tmpMp, 50, 200),
|
|
553
|
+
_taskNode(tmpMp + '-resolve', 'resolve-address', 'Resolve Address', 220, 180,
|
|
554
|
+
{ Address: '{~D:Record.Operation.MediaAddress~}' },
|
|
555
|
+
['Address'], ['URL', 'Filename', 'BeaconName', 'LocalPath', 'Strategy']),
|
|
556
|
+
_taskNode(tmpMp + '-transfer', 'file-transfer', 'File Transfer', 440, 180,
|
|
557
|
+
{
|
|
558
|
+
SourceURL: '{~D:Record.TaskOutputs.' + tmpMp + '-resolve.URL~}',
|
|
559
|
+
SourceLocalPath: '{~D:Record.TaskOutputs.' + tmpMp + '-resolve.LocalPath~}',
|
|
560
|
+
Filename: '{~D:Record.TaskOutputs.' + tmpMp + '-resolve.Filename~}',
|
|
561
|
+
TimeoutMs: 300000
|
|
562
|
+
},
|
|
563
|
+
['SourceURL', 'SourceLocalPath', 'Filename', 'TimeoutMs'],
|
|
564
|
+
['LocalPath', 'BytesTransferred', 'DurationMs', 'Strategy']),
|
|
565
|
+
_taskNode(tmpMp + '-process', 'beacon-mediaconversion-mediaprobe', 'Probe Metadata', 660, 180,
|
|
566
|
+
{
|
|
567
|
+
AffinityKey: '{~D:Record.Operation.MediaAddress~}',
|
|
568
|
+
TimeoutMs: 300000
|
|
569
|
+
},
|
|
570
|
+
['InputFile'],
|
|
571
|
+
['Result', 'StdOut']),
|
|
572
|
+
_endNode(tmpMp, 880, 260)
|
|
573
|
+
];
|
|
574
|
+
let tmpMpConns = _chainConnections(tmpMp,
|
|
575
|
+
[tmpMp + '-start', tmpMp + '-resolve', tmpMp + '-transfer', tmpMp + '-process'],
|
|
576
|
+
tmpMp + '-end');
|
|
577
|
+
tmpMpConns.push({ Hash: tmpMp + '-s1', SourceNodeHash: tmpMp + '-resolve', SourcePortHash: tmpMp + '-resolve-so-URL', TargetNodeHash: tmpMp + '-transfer', TargetPortHash: tmpMp + '-transfer-si-SourceURL', Data: {} });
|
|
578
|
+
tmpMpConns.push({ Hash: tmpMp + '-s2', SourceNodeHash: tmpMp + '-resolve', SourcePortHash: tmpMp + '-resolve-so-Filename', TargetNodeHash: tmpMp + '-transfer', TargetPortHash: tmpMp + '-transfer-si-Filename', Data: {} });
|
|
579
|
+
tmpMpConns.push({ Hash: tmpMp + '-s3', SourceNodeHash: tmpMp + '-transfer', SourcePortHash: tmpMp + '-transfer-so-LocalPath', TargetNodeHash: tmpMp + '-process', TargetPortHash: tmpMp + '-process-si-InputFile', Data: {} });
|
|
580
|
+
// Resolve LocalPath → transfer SourceLocalPath (shared-fs zero-copy fast path)
|
|
581
|
+
tmpMpConns.push({ Hash: tmpMp + '-s4', SourceNodeHash: tmpMp + '-resolve', SourcePortHash: tmpMp + '-resolve-so-LocalPath', TargetNodeHash: tmpMp + '-transfer', TargetPortHash: tmpMp + '-transfer-si-SourceLocalPath', Data: {} });
|
|
582
|
+
tmpOperations.push({
|
|
583
|
+
Hash: tmpMp,
|
|
467
584
|
Name: 'Media Probe',
|
|
468
|
-
Description: 'Extract metadata from a media file via ffprobe.
|
|
585
|
+
Description: 'Extract metadata from a media file via ffprobe. Custom graph: resolve → transfer → probe → end. Result is JSON metadata only (no send-result; read TaskOutputs[<process>].Result). Trigger with Parameters: { MediaAddress }.',
|
|
469
586
|
Tags: ['media', 'probe', 'metadata'],
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
{
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
ProcessSettings: ['InputFile'],
|
|
478
|
-
ProcessOutputs: ['Result', 'StdOut'],
|
|
479
|
-
OutputFile: 'probe.json'
|
|
480
|
-
}));
|
|
587
|
+
Author: 'retold-remote',
|
|
588
|
+
Version: '3.0.0',
|
|
589
|
+
Graph: { Nodes: tmpMpNodes, Connections: tmpMpConns, ViewState: { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null } },
|
|
590
|
+
SavedLayouts: [],
|
|
591
|
+
InitialGlobalState: {},
|
|
592
|
+
InitialOperationState: {}
|
|
593
|
+
});
|
|
481
594
|
|
|
482
595
|
return tmpOperations;
|
|
483
596
|
}
|
|
@@ -54,6 +54,25 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
54
54
|
// Ultravisor dispatcher — set via setDispatcher()
|
|
55
55
|
this._dispatcher = null;
|
|
56
56
|
|
|
57
|
+
// Operation broadcaster — set via setBroadcaster()
|
|
58
|
+
this._broadcaster = null;
|
|
59
|
+
|
|
60
|
+
// One-time local ffprobe detection. If ffprobe is on this machine we
|
|
61
|
+
// always prefer to use it directly — running ffprobe locally is a
|
|
62
|
+
// metadata-only call (microseconds for an indexed file) and avoids the
|
|
63
|
+
// entire resolve→transfer→probe→result Ultravisor pipeline. Only when
|
|
64
|
+
// ffprobe is missing locally do we fall back to dispatching the probe
|
|
65
|
+
// to a beacon.
|
|
66
|
+
this._ffprobeLocalAvailable = this._detectLocalFfprobe();
|
|
67
|
+
if (this._ffprobeLocalAvailable)
|
|
68
|
+
{
|
|
69
|
+
this.fable.log.info('Video Frame Service: local ffprobe detected — probes will run in-process (no dispatch).');
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
{
|
|
73
|
+
this.fable.log.info('Video Frame Service: local ffprobe NOT found — will dispatch probes to a beacon.');
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
// Apply explorer state persistence mixin (initializeState,
|
|
58
77
|
// _buildExplorerStateKey, loadExplorerState, saveExplorerState)
|
|
59
78
|
libExplorerStateMixin.apply(this, EXPLORER_STATE_SOURCE, 'video-explorer');
|
|
@@ -75,6 +94,42 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
75
94
|
this._dispatcher = pDispatcher;
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Set the operation broadcaster for progress events and cancellation.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} pBroadcaster - RetoldRemoteOperationBroadcaster instance
|
|
101
|
+
*/
|
|
102
|
+
setBroadcaster(pBroadcaster)
|
|
103
|
+
{
|
|
104
|
+
this._broadcaster = pBroadcaster;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Helper: emit a progress event if a broadcaster is attached and the
|
|
109
|
+
* caller supplied an operation id. Safe to call without either.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} pOperationId - Opaque client-supplied operation id
|
|
112
|
+
* @param {object} pPayload - { Phase, Current, Total, Message }
|
|
113
|
+
*/
|
|
114
|
+
_emitProgress(pOperationId, pPayload)
|
|
115
|
+
{
|
|
116
|
+
if (this._broadcaster && pOperationId)
|
|
117
|
+
{
|
|
118
|
+
this._broadcaster.broadcastProgress(pOperationId, pPayload);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Helper: check whether the current operation has been cancelled.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} pOperationId
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
_isCancelled(pOperationId)
|
|
129
|
+
{
|
|
130
|
+
return !!(this._broadcaster && pOperationId && this._broadcaster.isCancelled(pOperationId));
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
/**
|
|
79
134
|
* Install video-specific explorer state methods that need read-merge-write
|
|
80
135
|
* behavior (merging custom frames, preserving selection across saves).
|
|
@@ -229,9 +284,40 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
229
284
|
return this.fable.ParimeBinaryStorage.resolvePath('video-frames', tmpHash);
|
|
230
285
|
}
|
|
231
286
|
|
|
287
|
+
/**
|
|
288
|
+
* One-time check for local ffprobe at constructor time. Synchronous because
|
|
289
|
+
* the constructor is, and we want the cached answer ready before the first
|
|
290
|
+
* probe call. The check itself is cheap (~50ms) and only runs once per
|
|
291
|
+
* service lifetime.
|
|
292
|
+
*
|
|
293
|
+
* @returns {boolean} true if `ffprobe -version` returns 0 on this host
|
|
294
|
+
*/
|
|
295
|
+
_detectLocalFfprobe()
|
|
296
|
+
{
|
|
297
|
+
try
|
|
298
|
+
{
|
|
299
|
+
libChildProcess.execSync('ffprobe -version', { stdio: 'ignore', timeout: 5000 });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
catch (pError)
|
|
303
|
+
{
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
232
308
|
/**
|
|
233
309
|
* Probe a video file with ffprobe to get its duration.
|
|
234
|
-
*
|
|
310
|
+
*
|
|
311
|
+
* Strategy ordering (most efficient first):
|
|
312
|
+
* 1. LOCAL ffprobe — when this process has the binary on PATH. ffprobe
|
|
313
|
+
* reads the container index and returns metadata in milliseconds; no
|
|
314
|
+
* Ultravisor pipeline involvement, no file copies, no shared-fs
|
|
315
|
+
* negotiation. This is the right call for stack-mode deployments
|
|
316
|
+
* where retold-remote and orator-conversion live in the same image.
|
|
317
|
+
* 2. DISPATCHED probe — only when the local binary is missing. Goes
|
|
318
|
+
* through the rr-media-probe operation graph (resolve → transfer →
|
|
319
|
+
* probe → result). With shared-fs the file isn't actually copied,
|
|
320
|
+
* but the operation graph still runs.
|
|
235
321
|
*
|
|
236
322
|
* @param {string} pAbsPath - Absolute path to the video
|
|
237
323
|
* @param {Function} fCallback - Callback(pError, { duration, width, height, codec })
|
|
@@ -240,7 +326,18 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
240
326
|
{
|
|
241
327
|
let tmpSelf = this;
|
|
242
328
|
|
|
243
|
-
//
|
|
329
|
+
// Local-first: if ffprobe is on this host, just run it. The previous
|
|
330
|
+
// version did this BACKWARDS and dispatched even when local was
|
|
331
|
+
// available, which made every video frame extraction pay an extra
|
|
332
|
+
// Ultravisor round trip just to read the duration.
|
|
333
|
+
if (this._ffprobeLocalAvailable)
|
|
334
|
+
{
|
|
335
|
+
return this._probeVideoLocal(pAbsPath, fCallback);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// No local ffprobe — fall back to dispatching the probe through the
|
|
339
|
+
// Ultravisor mesh. This requires the dispatcher to be available and
|
|
340
|
+
// the file to be addressable through the File context.
|
|
244
341
|
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
245
342
|
{
|
|
246
343
|
let tmpRelPath;
|
|
@@ -270,13 +367,15 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
270
367
|
{
|
|
271
368
|
let tmpData = JSON.parse(tmpProcessOutput.Result);
|
|
272
369
|
let tmpParsed = tmpSelf._parseProbeData(tmpData);
|
|
273
|
-
tmpSelf.fable.log.info(`ffprobe via operation trigger for ${tmpRelPath}`);
|
|
370
|
+
tmpSelf.fable.log.info(`ffprobe via operation trigger for ${tmpRelPath} (no local ffprobe)`);
|
|
274
371
|
return fCallback(null, tmpParsed);
|
|
275
372
|
}
|
|
276
373
|
}
|
|
277
374
|
catch (pParseError)
|
|
278
375
|
{
|
|
279
|
-
// Fall through to local
|
|
376
|
+
// Fall through to a final local attempt — even though
|
|
377
|
+
// the constructor said ffprobe was missing, the binary
|
|
378
|
+
// could have been installed since startup.
|
|
280
379
|
}
|
|
281
380
|
}
|
|
282
381
|
|
|
@@ -286,6 +385,8 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
286
385
|
}
|
|
287
386
|
}
|
|
288
387
|
|
|
388
|
+
// Last resort: try local even though detection said it was missing.
|
|
389
|
+
// Returns the original ENOENT or similar if ffprobe really isn't there.
|
|
289
390
|
return this._probeVideoLocal(pAbsPath, fCallback);
|
|
290
391
|
}
|
|
291
392
|
|
|
@@ -543,6 +644,9 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
543
644
|
let tmpHeight = parseInt(pOptions.height, 10) || this.options.DefaultFrameHeight;
|
|
544
645
|
let tmpFormat = pOptions.format || this.options.DefaultFrameFormat;
|
|
545
646
|
|
|
647
|
+
// Operation tracking (optional — passed by caller via X-Op-Id)
|
|
648
|
+
let tmpOpId = pOptions.OperationId || null;
|
|
649
|
+
|
|
546
650
|
// Clamp values
|
|
547
651
|
tmpCount = Math.min(Math.max(tmpCount, 1), 100);
|
|
548
652
|
tmpWidth = Math.min(Math.max(tmpWidth, 64), 1920);
|
|
@@ -575,6 +679,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
575
679
|
{
|
|
576
680
|
let tmpManifest = JSON.parse(libFs.readFileSync(tmpManifestPath, 'utf8'));
|
|
577
681
|
this.fable.log.info(`Video frames cache hit for ${pRelPath}`);
|
|
682
|
+
this._emitProgress(tmpOpId, { Phase: 'cached', Message: 'Cached frames available' });
|
|
578
683
|
return fCallback(null, tmpManifest);
|
|
579
684
|
}
|
|
580
685
|
catch (pError)
|
|
@@ -584,6 +689,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
584
689
|
}
|
|
585
690
|
|
|
586
691
|
// Probe the video for duration
|
|
692
|
+
this._emitProgress(tmpOpId, { Phase: 'probing', Message: 'Probing video metadata' });
|
|
587
693
|
this._probeVideo(pAbsPath,
|
|
588
694
|
(pError, pVideoInfo) =>
|
|
589
695
|
{
|
|
@@ -592,6 +698,11 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
592
698
|
return fCallback(new Error('Could not probe video. ffprobe may not be available.'));
|
|
593
699
|
}
|
|
594
700
|
|
|
701
|
+
if (tmpSelf._isCancelled(tmpOpId))
|
|
702
|
+
{
|
|
703
|
+
return fCallback(new Error('Cancelled'));
|
|
704
|
+
}
|
|
705
|
+
|
|
595
706
|
let tmpDuration = pVideoInfo.duration;
|
|
596
707
|
|
|
597
708
|
// Calculate timestamps
|
|
@@ -609,9 +720,15 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
609
720
|
|
|
610
721
|
tmpSelf.fable.log.info(`Extracting ${tmpTimestamps.length} frames from ${pRelPath} (${tmpDuration.toFixed(1)}s)`);
|
|
611
722
|
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
723
|
+
// Three execution paths, in order of preference:
|
|
724
|
+
// 1. BATCH dispatch (dispatcher available) — single Ultravisor
|
|
725
|
+
// operation that resolves the address once, transfers/short-
|
|
726
|
+
// circuits the file once, and extracts all N frames in one
|
|
727
|
+
// beacon work item. Replaces the previous N-trigger storm.
|
|
728
|
+
// 2. Local async loop (dispatcher unavailable, ffmpeg local) —
|
|
729
|
+
// sequential extracts in-process, no Ultravisor.
|
|
730
|
+
// 3. Local sync loop (final fallback for ancient code paths)
|
|
731
|
+
let tmpUseBatch = !!(tmpSelf._dispatcher && tmpSelf._dispatcher.isAvailable());
|
|
615
732
|
|
|
616
733
|
let _finishExtraction = () =>
|
|
617
734
|
{
|
|
@@ -653,9 +770,20 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
653
770
|
return fCallback(null, tmpResult);
|
|
654
771
|
};
|
|
655
772
|
|
|
656
|
-
|
|
773
|
+
// Emit initial progress before starting extraction
|
|
774
|
+
tmpSelf._emitProgress(tmpOpId,
|
|
775
|
+
{
|
|
776
|
+
Phase: 'extracting',
|
|
777
|
+
Current: 0,
|
|
778
|
+
Total: tmpTimestamps.length,
|
|
779
|
+
Message: 'Extracting 0 of ' + tmpTimestamps.length + ' frames',
|
|
780
|
+
Cancelable: true
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Local serial-async fallback used by both the "no batch" path
|
|
784
|
+
// and the "batch failed" recovery path.
|
|
785
|
+
let _runLocalAsync = () =>
|
|
657
786
|
{
|
|
658
|
-
// Serial async extraction
|
|
659
787
|
let tmpFrameIndex = 0;
|
|
660
788
|
|
|
661
789
|
let _extractNext = () =>
|
|
@@ -665,6 +793,13 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
665
793
|
return _finishExtraction();
|
|
666
794
|
}
|
|
667
795
|
|
|
796
|
+
// Cooperative cancellation check before each frame
|
|
797
|
+
if (tmpSelf._isCancelled(tmpOpId))
|
|
798
|
+
{
|
|
799
|
+
tmpSelf.fable.log.info('[VideoFrameService] cancelled mid-extraction for ' + pRelPath);
|
|
800
|
+
return fCallback(new Error('Cancelled'));
|
|
801
|
+
}
|
|
802
|
+
|
|
668
803
|
let tmpI = tmpFrameIndex;
|
|
669
804
|
let tmpTimestamp = tmpTimestamps[tmpI];
|
|
670
805
|
let tmpFrameFilename = `frame_${String(tmpI).padStart(4, '0')}.${tmpFormat}`;
|
|
@@ -695,17 +830,166 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
695
830
|
// Frame file disappeared — skip
|
|
696
831
|
}
|
|
697
832
|
}
|
|
833
|
+
tmpSelf._emitProgress(tmpOpId,
|
|
834
|
+
{
|
|
835
|
+
Phase: 'extracting',
|
|
836
|
+
Current: tmpExtractedCount,
|
|
837
|
+
Total: tmpTimestamps.length,
|
|
838
|
+
Message: 'Extracting ' + tmpExtractedCount + ' of ' + tmpTimestamps.length + ' frames',
|
|
839
|
+
Cancelable: true
|
|
840
|
+
});
|
|
698
841
|
_extractNext();
|
|
699
842
|
});
|
|
700
843
|
};
|
|
701
844
|
|
|
702
845
|
_extractNext();
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
if (tmpUseBatch)
|
|
849
|
+
{
|
|
850
|
+
// ── BATCH path ──────────────────────────────────────────
|
|
851
|
+
// Build the per-frame spec list and dispatch a single batch
|
|
852
|
+
// operation. The orator-conversion beacon writes all frame
|
|
853
|
+
// files directly into our cache directory because we share
|
|
854
|
+
// the filesystem. After the operation completes we just stat
|
|
855
|
+
// each frame file and build our normal manifest.
|
|
856
|
+
let tmpRelPath;
|
|
857
|
+
try
|
|
858
|
+
{
|
|
859
|
+
tmpRelPath = libPath.relative(tmpSelf.contentPath, pAbsPath);
|
|
860
|
+
}
|
|
861
|
+
catch (pRelErr)
|
|
862
|
+
{
|
|
863
|
+
tmpRelPath = null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (!tmpRelPath || tmpRelPath.startsWith('..'))
|
|
867
|
+
{
|
|
868
|
+
tmpSelf.fable.log.warn(`[VideoFrameService] could not derive content-relative path for ${pAbsPath}; falling back to local extraction`);
|
|
869
|
+
return _runLocalAsync();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
let tmpFrameSpecs = [];
|
|
873
|
+
let tmpFilenameToMeta = {};
|
|
874
|
+
for (let i = 0; i < tmpTimestamps.length; i++)
|
|
875
|
+
{
|
|
876
|
+
let tmpTimestamp = tmpTimestamps[i];
|
|
877
|
+
let tmpFrameFilename = `frame_${String(i).padStart(4, '0')}.${tmpFormat}`;
|
|
878
|
+
let tmpTimeStr = tmpSelf._formatFfmpegTimestamp(tmpTimestamp);
|
|
879
|
+
tmpFrameSpecs.push({ Timestamp: tmpTimeStr, Filename: tmpFrameFilename });
|
|
880
|
+
tmpFilenameToMeta[tmpFrameFilename] =
|
|
881
|
+
{
|
|
882
|
+
Index: i,
|
|
883
|
+
Timestamp: tmpTimestamp,
|
|
884
|
+
TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp)
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (tmpSelf._isCancelled(tmpOpId))
|
|
889
|
+
{
|
|
890
|
+
return fCallback(new Error('Cancelled'));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
tmpSelf.fable.log.info(`[VideoFrameService] dispatching batch extraction (${tmpFrameSpecs.length} frames) for ${pRelPath}`);
|
|
894
|
+
|
|
895
|
+
tmpSelf._dispatcher.triggerOperation('rr-video-frames-batch',
|
|
896
|
+
{
|
|
897
|
+
VideoAddress: '>retold-remote/File/' + tmpRelPath,
|
|
898
|
+
OutputDir: tmpCacheDir,
|
|
899
|
+
Frames: JSON.stringify(tmpFrameSpecs),
|
|
900
|
+
Width: tmpWidth,
|
|
901
|
+
TimeoutMs: 1800000
|
|
902
|
+
},
|
|
903
|
+
(pBatchError, pBatchResult) =>
|
|
904
|
+
{
|
|
905
|
+
if (pBatchError || !pBatchResult)
|
|
906
|
+
{
|
|
907
|
+
tmpSelf.fable.log.warn(`[VideoFrameService] batch dispatch failed: ${pBatchError ? pBatchError.message : 'no result'}; falling back to local`);
|
|
908
|
+
return _runLocalAsync();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Pull the manifest out of the extract task's Result
|
|
912
|
+
let tmpManifest = null;
|
|
913
|
+
try
|
|
914
|
+
{
|
|
915
|
+
let tmpExtractOutputs = pBatchResult.TaskOutputs && pBatchResult.TaskOutputs['rr-video-frames-batch-extract'];
|
|
916
|
+
if (tmpExtractOutputs && tmpExtractOutputs.Result)
|
|
917
|
+
{
|
|
918
|
+
tmpManifest = JSON.parse(tmpExtractOutputs.Result);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
catch (pParseError)
|
|
922
|
+
{
|
|
923
|
+
tmpSelf.fable.log.warn(`[VideoFrameService] could not parse batch result manifest: ${pParseError.message}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (!tmpManifest || !Array.isArray(tmpManifest.Frames))
|
|
927
|
+
{
|
|
928
|
+
tmpSelf.fable.log.warn('[VideoFrameService] batch result missing manifest; falling back to local');
|
|
929
|
+
return _runLocalAsync();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Walk the manifest, stat each output file, build our
|
|
933
|
+
// frames array. Skip any that didn't actually land on
|
|
934
|
+
// disk (filesystem races, partial failures, etc).
|
|
935
|
+
for (let i = 0; i < tmpManifest.Frames.length; i++)
|
|
936
|
+
{
|
|
937
|
+
let tmpEntry = tmpManifest.Frames[i];
|
|
938
|
+
if (!tmpEntry || !tmpEntry.Success || !tmpEntry.Filename)
|
|
939
|
+
{
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
let tmpFramePath = libPath.join(tmpCacheDir, tmpEntry.Filename);
|
|
943
|
+
try
|
|
944
|
+
{
|
|
945
|
+
let tmpFrameStat = libFs.statSync(tmpFramePath);
|
|
946
|
+
let tmpMeta = tmpFilenameToMeta[tmpEntry.Filename] || {};
|
|
947
|
+
tmpFrames.push(
|
|
948
|
+
{
|
|
949
|
+
Index: tmpMeta.Index !== undefined ? tmpMeta.Index : i,
|
|
950
|
+
Timestamp: tmpMeta.Timestamp,
|
|
951
|
+
TimestampFormatted: tmpMeta.TimestampFormatted,
|
|
952
|
+
Filename: tmpEntry.Filename,
|
|
953
|
+
Size: tmpFrameStat.size
|
|
954
|
+
});
|
|
955
|
+
tmpExtractedCount++;
|
|
956
|
+
}
|
|
957
|
+
catch (pStatErr)
|
|
958
|
+
{
|
|
959
|
+
// File missing — skip
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
tmpSelf._emitProgress(tmpOpId,
|
|
964
|
+
{
|
|
965
|
+
Phase: 'extracting',
|
|
966
|
+
Current: tmpExtractedCount,
|
|
967
|
+
Total: tmpTimestamps.length,
|
|
968
|
+
Message: 'Extracting ' + tmpExtractedCount + ' of ' + tmpTimestamps.length + ' frames',
|
|
969
|
+
Cancelable: true
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
if (tmpExtractedCount === 0)
|
|
973
|
+
{
|
|
974
|
+
tmpSelf.fable.log.warn('[VideoFrameService] batch returned 0 successful frames; falling back to local');
|
|
975
|
+
return _runLocalAsync();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return _finishExtraction();
|
|
979
|
+
});
|
|
703
980
|
}
|
|
704
981
|
else
|
|
705
982
|
{
|
|
706
983
|
// Synchronous extraction (original behavior)
|
|
707
984
|
for (let i = 0; i < tmpTimestamps.length; i++)
|
|
708
985
|
{
|
|
986
|
+
// Cooperative cancellation check before each frame
|
|
987
|
+
if (tmpSelf._isCancelled(tmpOpId))
|
|
988
|
+
{
|
|
989
|
+
tmpSelf.fable.log.info('[VideoFrameService] cancelled mid-extraction for ' + pRelPath);
|
|
990
|
+
return fCallback(new Error('Cancelled'));
|
|
991
|
+
}
|
|
992
|
+
|
|
709
993
|
let tmpTimestamp = tmpTimestamps[i];
|
|
710
994
|
let tmpFrameFilename = `frame_${String(i).padStart(4, '0')}.${tmpFormat}`;
|
|
711
995
|
let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
|
|
@@ -726,6 +1010,15 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
726
1010
|
});
|
|
727
1011
|
tmpExtractedCount++;
|
|
728
1012
|
}
|
|
1013
|
+
|
|
1014
|
+
tmpSelf._emitProgress(tmpOpId,
|
|
1015
|
+
{
|
|
1016
|
+
Phase: 'extracting',
|
|
1017
|
+
Current: tmpExtractedCount,
|
|
1018
|
+
Total: tmpTimestamps.length,
|
|
1019
|
+
Message: 'Extracting ' + tmpExtractedCount + ' of ' + tmpTimestamps.length + ' frames',
|
|
1020
|
+
Cancelable: true
|
|
1021
|
+
});
|
|
729
1022
|
}
|
|
730
1023
|
|
|
731
1024
|
_finishExtraction();
|