retold-remote 0.0.13 → 0.0.17
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 +75 -0
- package/docs/_sidebar.md +2 -0
- package/docs/ultravisor-configuration.md +212 -0
- package/docs/ultravisor-integration.md +140 -0
- package/package.json +121 -96
- package/source/cli/RetoldRemote-Server-Setup.js +26 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +11 -3
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +5 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +16 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +101 -23
- package/source/server/RetoldRemote-EbookService.js +119 -6
- package/source/server/RetoldRemote-ImageService.js +254 -2
- package/source/server/RetoldRemote-MediaService.js +274 -37
- package/source/server/RetoldRemote-ToolDetector.js +27 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +599 -0
- package/source/server/RetoldRemote-VideoFrameService.js +309 -77
- package/source/views/PictView-Remote-AudioExplorer.js +28 -14
- package/source/views/PictView-Remote-ImageExplorer.js +31 -11
- package/source/views/PictView-Remote-VLCSetup.js +22 -12
- package/source/views/PictView-Remote-VideoExplorer.js +29 -14
- package/web-application/css/retold-remote.css +75 -0
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/ultravisor-configuration.md +212 -0
- package/web-application/docs/ultravisor-integration.md +140 -0
- 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 +4763 -4238
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +16 -16
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -51,6 +51,9 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
51
51
|
|
|
52
52
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
53
53
|
|
|
54
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
55
|
+
this._dispatcher = null;
|
|
56
|
+
|
|
54
57
|
// Apply explorer state persistence mixin (initializeState,
|
|
55
58
|
// _buildExplorerStateKey, loadExplorerState, saveExplorerState)
|
|
56
59
|
libExplorerStateMixin.apply(this, EXPLORER_STATE_SOURCE, 'video-explorer');
|
|
@@ -62,6 +65,16 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
62
65
|
this.fable.log.info('Video Frame Service: frames in ParimeBinaryStorage, state in Bibliograph');
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
72
|
+
*/
|
|
73
|
+
setDispatcher(pDispatcher)
|
|
74
|
+
{
|
|
75
|
+
this._dispatcher = pDispatcher;
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Install video-specific explorer state methods that need read-merge-write
|
|
67
80
|
* behavior (merging custom frames, preserving selection across saves).
|
|
@@ -218,43 +231,76 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
218
231
|
|
|
219
232
|
/**
|
|
220
233
|
* Probe a video file with ffprobe to get its duration.
|
|
234
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
221
235
|
*
|
|
222
236
|
* @param {string} pAbsPath - Absolute path to the video
|
|
223
237
|
* @param {Function} fCallback - Callback(pError, { duration, width, height, codec })
|
|
224
238
|
*/
|
|
225
239
|
_probeVideo(pAbsPath, fCallback)
|
|
226
240
|
{
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
|
|
230
|
-
let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
|
|
231
|
-
let tmpData = JSON.parse(tmpOutput.toString());
|
|
232
|
-
|
|
233
|
-
let tmpResult = {};
|
|
241
|
+
let tmpSelf = this;
|
|
234
242
|
|
|
235
|
-
|
|
243
|
+
// Try Ultravisor dispatch first
|
|
244
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
245
|
+
{
|
|
246
|
+
let tmpRelPath;
|
|
247
|
+
try
|
|
248
|
+
{
|
|
249
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
250
|
+
}
|
|
251
|
+
catch (pErr)
|
|
236
252
|
{
|
|
237
|
-
|
|
238
|
-
tmpResult.bitrate = parseInt(tmpData.format.bit_rate, 10) || null;
|
|
239
|
-
tmpResult.size = parseInt(tmpData.format.size, 10) || null;
|
|
253
|
+
tmpRelPath = null;
|
|
240
254
|
}
|
|
241
255
|
|
|
242
|
-
if (
|
|
256
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
243
257
|
{
|
|
244
|
-
|
|
258
|
+
let tmpCommand = `ffprobe -v quiet -print_format json -show_format -show_streams "{SourcePath}"`;
|
|
259
|
+
|
|
260
|
+
this._dispatcher.dispatchMediaCommand(
|
|
261
|
+
{
|
|
262
|
+
Command: tmpCommand,
|
|
263
|
+
InputPath: tmpRelPath,
|
|
264
|
+
AffinityKey: tmpRelPath,
|
|
265
|
+
TimeoutMs: 30000
|
|
266
|
+
},
|
|
267
|
+
(pDispatchError, pResult) =>
|
|
245
268
|
{
|
|
246
|
-
|
|
247
|
-
if (tmpStream.codec_type === 'video')
|
|
269
|
+
if (!pDispatchError && pResult && pResult.Outputs && pResult.Outputs.StdOut)
|
|
248
270
|
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
271
|
+
try
|
|
272
|
+
{
|
|
273
|
+
let tmpData = JSON.parse(pResult.Outputs.StdOut);
|
|
274
|
+
let tmpParsed = tmpSelf._parseProbeData(tmpData);
|
|
275
|
+
tmpSelf.fable.log.info(`ffprobe via Ultravisor for ${tmpRelPath}`);
|
|
276
|
+
return fCallback(null, tmpParsed);
|
|
277
|
+
}
|
|
278
|
+
catch (pParseError)
|
|
279
|
+
{
|
|
280
|
+
// Fall through to local
|
|
281
|
+
}
|
|
253
282
|
}
|
|
254
|
-
|
|
283
|
+
|
|
284
|
+
tmpSelf._probeVideoLocal(pAbsPath, fCallback);
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
255
287
|
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return this._probeVideoLocal(pAbsPath, fCallback);
|
|
291
|
+
}
|
|
256
292
|
|
|
257
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Probe a video file locally with ffprobe.
|
|
295
|
+
*/
|
|
296
|
+
_probeVideoLocal(pAbsPath, fCallback)
|
|
297
|
+
{
|
|
298
|
+
try
|
|
299
|
+
{
|
|
300
|
+
let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
|
|
301
|
+
let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
|
|
302
|
+
let tmpData = JSON.parse(tmpOutput.toString());
|
|
303
|
+
return fCallback(null, this._parseProbeData(tmpData));
|
|
258
304
|
}
|
|
259
305
|
catch (pError)
|
|
260
306
|
{
|
|
@@ -262,8 +308,41 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
262
308
|
}
|
|
263
309
|
}
|
|
264
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Parse ffprobe JSON output into a normalized result object.
|
|
313
|
+
*/
|
|
314
|
+
_parseProbeData(pData)
|
|
315
|
+
{
|
|
316
|
+
let tmpResult = {};
|
|
317
|
+
|
|
318
|
+
if (pData.format)
|
|
319
|
+
{
|
|
320
|
+
tmpResult.duration = parseFloat(pData.format.duration) || null;
|
|
321
|
+
tmpResult.bitrate = parseInt(pData.format.bit_rate, 10) || null;
|
|
322
|
+
tmpResult.size = parseInt(pData.format.size, 10) || null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (pData.streams)
|
|
326
|
+
{
|
|
327
|
+
for (let i = 0; i < pData.streams.length; i++)
|
|
328
|
+
{
|
|
329
|
+
let tmpStream = pData.streams[i];
|
|
330
|
+
if (tmpStream.codec_type === 'video')
|
|
331
|
+
{
|
|
332
|
+
tmpResult.width = tmpStream.width;
|
|
333
|
+
tmpResult.height = tmpStream.height;
|
|
334
|
+
tmpResult.codec = tmpStream.codec_name;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return tmpResult;
|
|
341
|
+
}
|
|
342
|
+
|
|
265
343
|
/**
|
|
266
344
|
* Extract a single frame from a video at a given timestamp.
|
|
345
|
+
* Synchronous version for backward compatibility.
|
|
267
346
|
*
|
|
268
347
|
* @param {string} pAbsPath - Absolute path to the video
|
|
269
348
|
* @param {number} pTimestamp - Timestamp in seconds
|
|
@@ -274,17 +353,18 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
274
353
|
* @returns {boolean} True if extraction succeeded
|
|
275
354
|
*/
|
|
276
355
|
_extractFrame(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat)
|
|
356
|
+
{
|
|
357
|
+
return this._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Extract a single frame locally using ffmpeg (synchronous).
|
|
362
|
+
*/
|
|
363
|
+
_extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat)
|
|
277
364
|
{
|
|
278
365
|
try
|
|
279
366
|
{
|
|
280
|
-
|
|
281
|
-
let tmpHours = Math.floor(pTimestamp / 3600);
|
|
282
|
-
let tmpMinutes = Math.floor((pTimestamp % 3600) / 60);
|
|
283
|
-
let tmpSeconds = pTimestamp % 60;
|
|
284
|
-
let tmpTimeStr = `${String(tmpHours).padStart(2, '0')}:${String(tmpMinutes).padStart(2, '0')}:${tmpSeconds.toFixed(3).padStart(6, '0')}`;
|
|
285
|
-
|
|
286
|
-
let tmpCodec = (pFormat === 'png') ? 'png' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
|
|
287
|
-
|
|
367
|
+
let tmpTimeStr = this._formatFfmpegTimestamp(pTimestamp);
|
|
288
368
|
let tmpMuxer = (pFormat === 'png') ? 'image2' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
|
|
289
369
|
let tmpCmd = `ffmpeg -ss ${tmpTimeStr} -i "${pAbsPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpMuxer} -y "${pOutputPath}"`;
|
|
290
370
|
libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 30000 });
|
|
@@ -297,6 +377,97 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
297
377
|
}
|
|
298
378
|
}
|
|
299
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Extract a single frame asynchronously, trying Ultravisor dispatch first.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} pAbsPath - Absolute path to the video
|
|
384
|
+
* @param {number} pTimestamp - Timestamp in seconds
|
|
385
|
+
* @param {string} pOutputPath - Absolute path for the output image
|
|
386
|
+
* @param {number} pWidth - Target width
|
|
387
|
+
* @param {number} pHeight - Target height
|
|
388
|
+
* @param {string} pFormat - Output format (jpg, png, webp)
|
|
389
|
+
* @param {Function} fCallback - Callback(pError, pSuccess)
|
|
390
|
+
*/
|
|
391
|
+
_extractFrameAsync(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat, fCallback)
|
|
392
|
+
{
|
|
393
|
+
let tmpSelf = this;
|
|
394
|
+
|
|
395
|
+
// Try Ultravisor dispatch first
|
|
396
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
397
|
+
{
|
|
398
|
+
let tmpRelPath;
|
|
399
|
+
try
|
|
400
|
+
{
|
|
401
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
402
|
+
}
|
|
403
|
+
catch (pErr)
|
|
404
|
+
{
|
|
405
|
+
tmpRelPath = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
409
|
+
{
|
|
410
|
+
let tmpTimeStr = this._formatFfmpegTimestamp(pTimestamp);
|
|
411
|
+
let tmpMuxer = (pFormat === 'png') ? 'image2' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
|
|
412
|
+
let tmpOutputFilename = libPath.basename(pOutputPath);
|
|
413
|
+
let tmpCommand = `ffmpeg -ss ${tmpTimeStr} -i "{SourcePath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpMuxer} -y "{OutputPath}"`;
|
|
414
|
+
|
|
415
|
+
this._dispatcher.dispatchMediaCommand(
|
|
416
|
+
{
|
|
417
|
+
Command: tmpCommand,
|
|
418
|
+
InputPath: tmpRelPath,
|
|
419
|
+
OutputFilename: tmpOutputFilename,
|
|
420
|
+
AffinityKey: tmpRelPath,
|
|
421
|
+
TimeoutMs: 60000
|
|
422
|
+
},
|
|
423
|
+
(pDispatchError, pResult) =>
|
|
424
|
+
{
|
|
425
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
426
|
+
{
|
|
427
|
+
try
|
|
428
|
+
{
|
|
429
|
+
// Write the output buffer to the expected path
|
|
430
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
431
|
+
if (!libFs.existsSync(tmpDir))
|
|
432
|
+
{
|
|
433
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
434
|
+
}
|
|
435
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
436
|
+
return fCallback(null, true);
|
|
437
|
+
}
|
|
438
|
+
catch (pWriteError)
|
|
439
|
+
{
|
|
440
|
+
// Fall through to local
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fall through to local processing
|
|
445
|
+
let tmpSuccess = tmpSelf._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
|
|
446
|
+
return fCallback(null, tmpSuccess);
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Local processing
|
|
453
|
+
let tmpSuccess = this._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
|
|
454
|
+
return fCallback(null, tmpSuccess);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Format a timestamp in seconds to ffmpeg's HH:MM:SS.mmm format.
|
|
459
|
+
*
|
|
460
|
+
* @param {number} pTimestamp - Timestamp in seconds
|
|
461
|
+
* @returns {string}
|
|
462
|
+
*/
|
|
463
|
+
_formatFfmpegTimestamp(pTimestamp)
|
|
464
|
+
{
|
|
465
|
+
let tmpHours = Math.floor(pTimestamp / 3600);
|
|
466
|
+
let tmpMinutes = Math.floor((pTimestamp % 3600) / 60);
|
|
467
|
+
let tmpSeconds = pTimestamp % 60;
|
|
468
|
+
return `${String(tmpHours).padStart(2, '0')}:${String(tmpMinutes).padStart(2, '0')}:${tmpSeconds.toFixed(3).padStart(6, '0')}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
300
471
|
/**
|
|
301
472
|
* Format a timestamp in seconds to a human-readable string.
|
|
302
473
|
*
|
|
@@ -443,66 +614,127 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
443
614
|
|
|
444
615
|
tmpSelf.fable.log.info(`Extracting ${tmpTimestamps.length} frames from ${pRelPath} (${tmpDuration.toFixed(1)}s)`);
|
|
445
616
|
|
|
446
|
-
|
|
617
|
+
// Use async serial extraction when dispatcher is available,
|
|
618
|
+
// otherwise use synchronous loop for backward compatibility
|
|
619
|
+
let tmpUseAsync = !!(tmpSelf._dispatcher && tmpSelf._dispatcher.isAvailable());
|
|
620
|
+
|
|
621
|
+
let _finishExtraction = () =>
|
|
447
622
|
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
623
|
+
if (tmpExtractedCount === 0)
|
|
624
|
+
{
|
|
625
|
+
return fCallback(new Error('Failed to extract any frames from the video.'));
|
|
626
|
+
}
|
|
451
627
|
|
|
452
|
-
let
|
|
453
|
-
|
|
628
|
+
let tmpResult =
|
|
629
|
+
{
|
|
630
|
+
Success: true,
|
|
631
|
+
Path: pRelPath,
|
|
632
|
+
Duration: tmpDuration,
|
|
633
|
+
DurationFormatted: tmpSelf._formatTimestamp(tmpDuration),
|
|
634
|
+
VideoWidth: pVideoInfo.width,
|
|
635
|
+
VideoHeight: pVideoInfo.height,
|
|
636
|
+
Codec: pVideoInfo.codec,
|
|
637
|
+
Bitrate: pVideoInfo.bitrate,
|
|
638
|
+
FileSize: pVideoInfo.size || tmpStat.size,
|
|
639
|
+
FrameCount: tmpExtractedCount,
|
|
640
|
+
FrameWidth: tmpWidth,
|
|
641
|
+
FrameHeight: tmpHeight,
|
|
642
|
+
FrameFormat: tmpFormat,
|
|
643
|
+
CacheKey: libPath.basename(tmpCacheDir),
|
|
644
|
+
Frames: tmpFrames
|
|
645
|
+
};
|
|
454
646
|
|
|
455
|
-
|
|
647
|
+
// Write manifest to cache
|
|
648
|
+
try
|
|
456
649
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
|
|
463
|
-
Filename: tmpFrameFilename,
|
|
464
|
-
Size: tmpFrameStat.size
|
|
465
|
-
});
|
|
466
|
-
tmpExtractedCount++;
|
|
650
|
+
libFs.writeFileSync(tmpManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
651
|
+
}
|
|
652
|
+
catch (pWriteError)
|
|
653
|
+
{
|
|
654
|
+
tmpSelf.fable.log.warn(`Could not write frame manifest: ${pWriteError.message}`);
|
|
467
655
|
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (tmpExtractedCount === 0)
|
|
471
|
-
{
|
|
472
|
-
return fCallback(new Error('Failed to extract any frames from the video.'));
|
|
473
|
-
}
|
|
474
656
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
Success: true,
|
|
478
|
-
Path: pRelPath,
|
|
479
|
-
Duration: tmpDuration,
|
|
480
|
-
DurationFormatted: tmpSelf._formatTimestamp(tmpDuration),
|
|
481
|
-
VideoWidth: pVideoInfo.width,
|
|
482
|
-
VideoHeight: pVideoInfo.height,
|
|
483
|
-
Codec: pVideoInfo.codec,
|
|
484
|
-
Bitrate: pVideoInfo.bitrate,
|
|
485
|
-
FileSize: pVideoInfo.size || tmpStat.size,
|
|
486
|
-
FrameCount: tmpExtractedCount,
|
|
487
|
-
FrameWidth: tmpWidth,
|
|
488
|
-
FrameHeight: tmpHeight,
|
|
489
|
-
FrameFormat: tmpFormat,
|
|
490
|
-
CacheKey: libPath.basename(tmpCacheDir),
|
|
491
|
-
Frames: tmpFrames
|
|
657
|
+
tmpSelf.fable.log.info(`Extracted ${tmpExtractedCount} frames for ${pRelPath}`);
|
|
658
|
+
return fCallback(null, tmpResult);
|
|
492
659
|
};
|
|
493
660
|
|
|
494
|
-
|
|
495
|
-
try
|
|
661
|
+
if (tmpUseAsync)
|
|
496
662
|
{
|
|
497
|
-
|
|
663
|
+
// Serial async extraction
|
|
664
|
+
let tmpFrameIndex = 0;
|
|
665
|
+
|
|
666
|
+
let _extractNext = () =>
|
|
667
|
+
{
|
|
668
|
+
if (tmpFrameIndex >= tmpTimestamps.length)
|
|
669
|
+
{
|
|
670
|
+
return _finishExtraction();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let tmpI = tmpFrameIndex;
|
|
674
|
+
let tmpTimestamp = tmpTimestamps[tmpI];
|
|
675
|
+
let tmpFrameFilename = `frame_${String(tmpI).padStart(4, '0')}.${tmpFormat}`;
|
|
676
|
+
let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
|
|
677
|
+
tmpFrameIndex++;
|
|
678
|
+
|
|
679
|
+
tmpSelf._extractFrameAsync(
|
|
680
|
+
pAbsPath, tmpTimestamp, tmpFramePath, tmpWidth, tmpHeight, tmpFormat,
|
|
681
|
+
(pExtractError, pSuccess) =>
|
|
682
|
+
{
|
|
683
|
+
if (pSuccess)
|
|
684
|
+
{
|
|
685
|
+
try
|
|
686
|
+
{
|
|
687
|
+
let tmpFrameStat = libFs.statSync(tmpFramePath);
|
|
688
|
+
tmpFrames.push(
|
|
689
|
+
{
|
|
690
|
+
Index: tmpI,
|
|
691
|
+
Timestamp: tmpTimestamp,
|
|
692
|
+
TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
|
|
693
|
+
Filename: tmpFrameFilename,
|
|
694
|
+
Size: tmpFrameStat.size
|
|
695
|
+
});
|
|
696
|
+
tmpExtractedCount++;
|
|
697
|
+
}
|
|
698
|
+
catch (pStatErr)
|
|
699
|
+
{
|
|
700
|
+
// Frame file disappeared — skip
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
_extractNext();
|
|
704
|
+
});
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
_extractNext();
|
|
498
708
|
}
|
|
499
|
-
|
|
709
|
+
else
|
|
500
710
|
{
|
|
501
|
-
|
|
502
|
-
|
|
711
|
+
// Synchronous extraction (original behavior)
|
|
712
|
+
for (let i = 0; i < tmpTimestamps.length; i++)
|
|
713
|
+
{
|
|
714
|
+
let tmpTimestamp = tmpTimestamps[i];
|
|
715
|
+
let tmpFrameFilename = `frame_${String(i).padStart(4, '0')}.${tmpFormat}`;
|
|
716
|
+
let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
|
|
717
|
+
|
|
718
|
+
let tmpSuccess = tmpSelf._extractFrame(
|
|
719
|
+
pAbsPath, tmpTimestamp, tmpFramePath, tmpWidth, tmpHeight, tmpFormat);
|
|
720
|
+
|
|
721
|
+
if (tmpSuccess)
|
|
722
|
+
{
|
|
723
|
+
let tmpFrameStat = libFs.statSync(tmpFramePath);
|
|
724
|
+
tmpFrames.push(
|
|
725
|
+
{
|
|
726
|
+
Index: i,
|
|
727
|
+
Timestamp: tmpTimestamp,
|
|
728
|
+
TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
|
|
729
|
+
Filename: tmpFrameFilename,
|
|
730
|
+
Size: tmpFrameStat.size
|
|
731
|
+
});
|
|
732
|
+
tmpExtractedCount++;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
503
735
|
|
|
504
|
-
|
|
505
|
-
|
|
736
|
+
_finishExtraction();
|
|
737
|
+
}
|
|
506
738
|
});
|
|
507
739
|
}
|
|
508
740
|
|
|
@@ -59,6 +59,8 @@ class RetoldRemoteAudioExplorerView extends libPictView
|
|
|
59
59
|
{
|
|
60
60
|
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
61
61
|
tmpRemote.ActiveMode = 'audio-explorer';
|
|
62
|
+
tmpRemote.CurrentViewerFile = pFilePath;
|
|
63
|
+
tmpRemote.CurrentViewerMediaType = 'audio';
|
|
62
64
|
this._currentPath = pFilePath;
|
|
63
65
|
this._waveformData = null;
|
|
64
66
|
this._peaks = [];
|
|
@@ -102,8 +104,12 @@ class RetoldRemoteAudioExplorerView extends libPictView
|
|
|
102
104
|
|
|
103
105
|
// Header
|
|
104
106
|
tmpHTML += '<div class="retold-remote-aex-header">';
|
|
105
|
-
tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back
|
|
107
|
+
tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back (Esc)">← Back</button>';
|
|
106
108
|
tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer — ' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFileName) + '</div>';
|
|
109
|
+
tmpHTML += '<div class="retold-remote-aex-actions">';
|
|
110
|
+
tmpHTML += '<button class="retold-remote-aex-action-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].playInBrowser()" title="Play in browser">▶ Play</button>';
|
|
111
|
+
tmpHTML += '<button class="retold-remote-aex-action-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\']._streamWithVLC()" title="Stream with VLC (v)">▶ VLC</button>';
|
|
112
|
+
tmpHTML += '</div>';
|
|
107
113
|
tmpHTML += '</div>';
|
|
108
114
|
|
|
109
115
|
// Info bar (populated after waveform loads)
|
|
@@ -971,7 +977,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
|
|
|
971
977
|
}
|
|
972
978
|
|
|
973
979
|
/**
|
|
974
|
-
* Navigate back to the
|
|
980
|
+
* Navigate back to the gallery / file listing.
|
|
975
981
|
*/
|
|
976
982
|
goBack()
|
|
977
983
|
{
|
|
@@ -982,21 +988,29 @@ class RetoldRemoteAudioExplorerView extends libPictView
|
|
|
982
988
|
this._resizeObserver = null;
|
|
983
989
|
}
|
|
984
990
|
|
|
985
|
-
|
|
991
|
+
let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
992
|
+
if (tmpNav)
|
|
986
993
|
{
|
|
987
|
-
|
|
988
|
-
if (tmpViewer)
|
|
989
|
-
{
|
|
990
|
-
tmpViewer.showMedia(this._currentPath, 'audio');
|
|
991
|
-
}
|
|
994
|
+
tmpNav.closeViewer();
|
|
992
995
|
}
|
|
993
|
-
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Leave the audio explorer and play the file in the browser viewer.
|
|
1000
|
+
*/
|
|
1001
|
+
playInBrowser()
|
|
1002
|
+
{
|
|
1003
|
+
// Clean up resize observer
|
|
1004
|
+
if (this._resizeObserver)
|
|
994
1005
|
{
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1006
|
+
this._resizeObserver.disconnect();
|
|
1007
|
+
this._resizeObserver = null;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
1011
|
+
if (tmpViewer)
|
|
1012
|
+
{
|
|
1013
|
+
tmpViewer.showMedia(this._currentPath, 'audio');
|
|
1000
1014
|
}
|
|
1001
1015
|
}
|
|
1002
1016
|
|
|
@@ -34,6 +34,8 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
34
34
|
{
|
|
35
35
|
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
36
36
|
tmpRemote.ActiveMode = 'image-explorer';
|
|
37
|
+
tmpRemote.CurrentViewerFile = pFilePath;
|
|
38
|
+
tmpRemote.CurrentViewerMediaType = 'image';
|
|
37
39
|
this._currentPath = pFilePath;
|
|
38
40
|
this._dziData = null;
|
|
39
41
|
this._loading = false;
|
|
@@ -86,6 +88,9 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
86
88
|
tmpHTML += '<div class="retold-remote-iex-header">';
|
|
87
89
|
tmpHTML += '<button class="retold-remote-iex-nav-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].goBack()" title="Back (Esc)">← Back</button>';
|
|
88
90
|
tmpHTML += '<div class="retold-remote-iex-title">Image Explorer — ' + tmpFmt.escapeHTML(tmpFileName) + '</div>';
|
|
91
|
+
tmpHTML += '<div class="retold-remote-iex-actions">';
|
|
92
|
+
tmpHTML += '<button class="retold-remote-iex-action-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].viewInBrowser()" title="View in standard viewer">🖼 View</button>';
|
|
93
|
+
tmpHTML += '</div>';
|
|
89
94
|
tmpHTML += '</div>';
|
|
90
95
|
|
|
91
96
|
// Info bar
|
|
@@ -732,7 +737,7 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
732
737
|
}
|
|
733
738
|
|
|
734
739
|
/**
|
|
735
|
-
* Navigate back to the
|
|
740
|
+
* Navigate back to the gallery / file listing.
|
|
736
741
|
*/
|
|
737
742
|
goBack()
|
|
738
743
|
{
|
|
@@ -750,21 +755,36 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
750
755
|
this._osdViewer = null;
|
|
751
756
|
}
|
|
752
757
|
|
|
753
|
-
|
|
758
|
+
let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
759
|
+
if (tmpNav)
|
|
754
760
|
{
|
|
755
|
-
|
|
756
|
-
if (tmpViewer)
|
|
757
|
-
{
|
|
758
|
-
tmpViewer.showMedia(this._currentPath, 'image');
|
|
759
|
-
}
|
|
761
|
+
tmpNav.closeViewer();
|
|
760
762
|
}
|
|
761
|
-
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Leave the image explorer and view the image in the standard viewer.
|
|
767
|
+
*/
|
|
768
|
+
viewInBrowser()
|
|
769
|
+
{
|
|
770
|
+
// Destroy the OSD viewer
|
|
771
|
+
if (this._osdViewer)
|
|
762
772
|
{
|
|
763
|
-
|
|
764
|
-
|
|
773
|
+
try
|
|
774
|
+
{
|
|
775
|
+
this._osdViewer.destroy();
|
|
776
|
+
}
|
|
777
|
+
catch (pErr)
|
|
765
778
|
{
|
|
766
|
-
|
|
779
|
+
// ignore
|
|
767
780
|
}
|
|
781
|
+
this._osdViewer = null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
785
|
+
if (tmpViewer)
|
|
786
|
+
{
|
|
787
|
+
tmpViewer.showMedia(this._currentPath, 'image');
|
|
768
788
|
}
|
|
769
789
|
}
|
|
770
790
|
|