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
|
@@ -78,6 +78,19 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
78
78
|
|
|
79
79
|
// Tool capabilities — set by MediaService after ToolDetector runs
|
|
80
80
|
this._capabilities = {};
|
|
81
|
+
|
|
82
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
83
|
+
this._dispatcher = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
90
|
+
*/
|
|
91
|
+
setDispatcher(pDispatcher)
|
|
92
|
+
{
|
|
93
|
+
this._dispatcher = pDispatcher;
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
/**
|
|
@@ -336,6 +349,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
336
349
|
|
|
337
350
|
/**
|
|
338
351
|
* Convert a raw image to JPEG using dcraw piped through Sharp.
|
|
352
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
339
353
|
* dcraw outputs PPM to stdout; Sharp converts to JPEG.
|
|
340
354
|
*
|
|
341
355
|
* @param {string} pAbsPath - Raw file path
|
|
@@ -344,6 +358,71 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
344
358
|
* @param {Function} fCallback - Callback(pError)
|
|
345
359
|
*/
|
|
346
360
|
_convertWithDcraw(pAbsPath, pOutputPath, pFullResolution, fCallback)
|
|
361
|
+
{
|
|
362
|
+
let tmpSelf = this;
|
|
363
|
+
|
|
364
|
+
// Try Ultravisor dispatch first
|
|
365
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
366
|
+
{
|
|
367
|
+
let tmpRelPath;
|
|
368
|
+
try
|
|
369
|
+
{
|
|
370
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
371
|
+
}
|
|
372
|
+
catch (pErr)
|
|
373
|
+
{
|
|
374
|
+
tmpRelPath = null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
378
|
+
{
|
|
379
|
+
let tmpHalfFlag = pFullResolution ? '' : ' -h';
|
|
380
|
+
let tmpOutputFilename = libPath.basename(pOutputPath);
|
|
381
|
+
let tmpCommand = `dcraw -c -w${tmpHalfFlag} "{SourcePath}" | convert ppm:- jpeg:"{OutputPath}"`;
|
|
382
|
+
|
|
383
|
+
this._dispatcher.dispatchMediaCommand(
|
|
384
|
+
{
|
|
385
|
+
Command: tmpCommand,
|
|
386
|
+
InputPath: tmpRelPath,
|
|
387
|
+
OutputFilename: tmpOutputFilename,
|
|
388
|
+
AffinityKey: tmpRelPath,
|
|
389
|
+
TimeoutMs: 180000
|
|
390
|
+
},
|
|
391
|
+
(pDispatchError, pResult) =>
|
|
392
|
+
{
|
|
393
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
394
|
+
{
|
|
395
|
+
try
|
|
396
|
+
{
|
|
397
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
398
|
+
if (!libFs.existsSync(tmpDir))
|
|
399
|
+
{
|
|
400
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
403
|
+
tmpSelf.fable.log.info(`Raw conversion via Ultravisor (dcraw) for ${tmpRelPath}`);
|
|
404
|
+
return fCallback(null);
|
|
405
|
+
}
|
|
406
|
+
catch (pWriteError)
|
|
407
|
+
{
|
|
408
|
+
// Fall through to local
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Fall through to local processing
|
|
413
|
+
tmpSelf._convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback);
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return this._convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Convert a raw image to JPEG locally using dcraw piped through Sharp.
|
|
424
|
+
*/
|
|
425
|
+
_convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback)
|
|
347
426
|
{
|
|
348
427
|
if (!this._capabilities.dcraw || !this._sharp)
|
|
349
428
|
{
|
|
@@ -413,6 +492,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
413
492
|
|
|
414
493
|
/**
|
|
415
494
|
* Convert a raw image to JPEG using ImageMagick's convert command.
|
|
495
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
416
496
|
* Works if ImageMagick has the dcraw/ufraw delegate installed.
|
|
417
497
|
*
|
|
418
498
|
* @param {string} pAbsPath - Raw file path
|
|
@@ -420,6 +500,70 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
420
500
|
* @param {Function} fCallback - Callback(pError)
|
|
421
501
|
*/
|
|
422
502
|
_convertWithImageMagick(pAbsPath, pOutputPath, fCallback)
|
|
503
|
+
{
|
|
504
|
+
let tmpSelf = this;
|
|
505
|
+
|
|
506
|
+
// Try Ultravisor dispatch first
|
|
507
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
508
|
+
{
|
|
509
|
+
let tmpRelPath;
|
|
510
|
+
try
|
|
511
|
+
{
|
|
512
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
513
|
+
}
|
|
514
|
+
catch (pErr)
|
|
515
|
+
{
|
|
516
|
+
tmpRelPath = null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
520
|
+
{
|
|
521
|
+
let tmpOutputFilename = libPath.basename(pOutputPath);
|
|
522
|
+
let tmpCommand = `convert "{SourcePath}" -auto-orient -quality 92 "{OutputPath}"`;
|
|
523
|
+
|
|
524
|
+
this._dispatcher.dispatchMediaCommand(
|
|
525
|
+
{
|
|
526
|
+
Command: tmpCommand,
|
|
527
|
+
InputPath: tmpRelPath,
|
|
528
|
+
OutputFilename: tmpOutputFilename,
|
|
529
|
+
AffinityKey: tmpRelPath,
|
|
530
|
+
TimeoutMs: 180000
|
|
531
|
+
},
|
|
532
|
+
(pDispatchError, pResult) =>
|
|
533
|
+
{
|
|
534
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
535
|
+
{
|
|
536
|
+
try
|
|
537
|
+
{
|
|
538
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
539
|
+
if (!libFs.existsSync(tmpDir))
|
|
540
|
+
{
|
|
541
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
542
|
+
}
|
|
543
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
544
|
+
tmpSelf.fable.log.info(`Raw conversion via Ultravisor (ImageMagick) for ${tmpRelPath}`);
|
|
545
|
+
return fCallback(null);
|
|
546
|
+
}
|
|
547
|
+
catch (pWriteError)
|
|
548
|
+
{
|
|
549
|
+
// Fall through to local
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Fall through to local processing
|
|
554
|
+
tmpSelf._convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback);
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return this._convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Convert a raw image to JPEG locally using ImageMagick's convert command.
|
|
565
|
+
*/
|
|
566
|
+
_convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback)
|
|
423
567
|
{
|
|
424
568
|
if (!this._capabilities.imagemagick)
|
|
425
569
|
{
|
|
@@ -608,7 +752,11 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
608
752
|
|
|
609
753
|
if (!this._sharp && !this._capabilities.imagemagick)
|
|
610
754
|
{
|
|
611
|
-
|
|
755
|
+
// Neither Sharp nor ImageMagick available locally — check for Ultravisor
|
|
756
|
+
if (!(this._dispatcher && this._dispatcher.isAvailable()))
|
|
757
|
+
{
|
|
758
|
+
return fCallback(new Error('Neither sharp nor ImageMagick is available.'));
|
|
759
|
+
}
|
|
612
760
|
}
|
|
613
761
|
|
|
614
762
|
let tmpMaxDim = pMaxDimension || this.options.DefaultMaxPreviewDimension;
|
|
@@ -682,11 +830,20 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
682
830
|
{
|
|
683
831
|
this._doGeneratePreview(pAbsPath, pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpCacheDir, tmpManifestPath, tmpOutputPath, tmpStat, false, fCallback);
|
|
684
832
|
}
|
|
685
|
-
else
|
|
833
|
+
else if (this._capabilities.imagemagick)
|
|
686
834
|
{
|
|
687
835
|
// No Sharp available — use ImageMagick for standard images too
|
|
688
836
|
this._doGeneratePreviewWithImageMagick(pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpManifestPath, tmpOutputPath, tmpStat, false, fCallback);
|
|
689
837
|
}
|
|
838
|
+
else if (this._dispatcher && this._dispatcher.isAvailable())
|
|
839
|
+
{
|
|
840
|
+
// No local tools available — dispatch to Ultravisor beacon
|
|
841
|
+
this._doGeneratePreviewWithDispatcher(pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpCacheDir, tmpManifestPath, tmpOutputPath, tmpStat, tmpIsRaw, fCallback);
|
|
842
|
+
}
|
|
843
|
+
else
|
|
844
|
+
{
|
|
845
|
+
return fCallback(new Error('No preview tools available.'));
|
|
846
|
+
}
|
|
690
847
|
}
|
|
691
848
|
|
|
692
849
|
/**
|
|
@@ -943,6 +1100,101 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
943
1100
|
}
|
|
944
1101
|
}
|
|
945
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Generate a preview by dispatching to an Ultravisor beacon.
|
|
1105
|
+
* Uses the MediaConversion capability (ImageResize) with a shell
|
|
1106
|
+
* convert fallback. The result is written to the cache directory.
|
|
1107
|
+
*
|
|
1108
|
+
* @param {string} pInputPath - Absolute path to the source image
|
|
1109
|
+
* @param {string} pRelPath - Relative path (for response/logging)
|
|
1110
|
+
* @param {number} pMaxDim - Max dimension
|
|
1111
|
+
* @param {string} pCacheKey - Cache key
|
|
1112
|
+
* @param {string} pOutputFilename - Output filename
|
|
1113
|
+
* @param {string} pCacheDir - Cache directory path
|
|
1114
|
+
* @param {string} pManifestPath - Manifest file path
|
|
1115
|
+
* @param {string} pOutputPath - Output file path
|
|
1116
|
+
* @param {object} pStat - Original file stat
|
|
1117
|
+
* @param {boolean} pIsRaw - Whether this is a raw camera format
|
|
1118
|
+
* @param {Function} fCallback - Callback(pError, pResult)
|
|
1119
|
+
*/
|
|
1120
|
+
_doGeneratePreviewWithDispatcher(pInputPath, pRelPath, pMaxDim, pCacheKey, pOutputFilename, pCacheDir, pManifestPath, pOutputPath, pStat, pIsRaw, fCallback)
|
|
1121
|
+
{
|
|
1122
|
+
let tmpSelf = this;
|
|
1123
|
+
let tmpRelPath;
|
|
1124
|
+
|
|
1125
|
+
try
|
|
1126
|
+
{
|
|
1127
|
+
tmpRelPath = libPath.relative(this.contentPath, pInputPath);
|
|
1128
|
+
}
|
|
1129
|
+
catch (pErr)
|
|
1130
|
+
{
|
|
1131
|
+
return fCallback(new Error('Could not resolve relative path for dispatch.'));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (!tmpRelPath || tmpRelPath.startsWith('..'))
|
|
1135
|
+
{
|
|
1136
|
+
return fCallback(new Error('File is outside content root.'));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
this._dispatcher.dispatchConversion(
|
|
1140
|
+
{
|
|
1141
|
+
Action: 'ImageResize',
|
|
1142
|
+
InputPath: tmpRelPath,
|
|
1143
|
+
OutputFilename: pOutputFilename,
|
|
1144
|
+
Width: pMaxDim,
|
|
1145
|
+
Height: pMaxDim,
|
|
1146
|
+
Format: 'jpeg',
|
|
1147
|
+
Quality: this.options.PreviewQuality || 85,
|
|
1148
|
+
AffinityKey: tmpRelPath,
|
|
1149
|
+
TimeoutMs: 120000,
|
|
1150
|
+
FallbackCommand: `convert "{SourcePath}"[0] -auto-orient -resize ${pMaxDim}x${pMaxDim} -quality ${this.options.PreviewQuality || 85} "{OutputPath}"`
|
|
1151
|
+
},
|
|
1152
|
+
(pDispatchError, pResult) =>
|
|
1153
|
+
{
|
|
1154
|
+
if (pDispatchError || !pResult || !pResult.OutputBuffer)
|
|
1155
|
+
{
|
|
1156
|
+
return fCallback(new Error('Ultravisor preview generation failed: ' + (pDispatchError ? pDispatchError.message : 'no output')));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
try
|
|
1160
|
+
{
|
|
1161
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
1162
|
+
}
|
|
1163
|
+
catch (pWriteError)
|
|
1164
|
+
{
|
|
1165
|
+
return fCallback(new Error('Failed to write preview output: ' + pWriteError.message));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
let tmpResult =
|
|
1169
|
+
{
|
|
1170
|
+
Success: true,
|
|
1171
|
+
SourcePath: pRelPath,
|
|
1172
|
+
CacheKey: pCacheKey,
|
|
1173
|
+
OutputFilename: pOutputFilename,
|
|
1174
|
+
Width: pMaxDim,
|
|
1175
|
+
Height: pMaxDim,
|
|
1176
|
+
OrigWidth: 0,
|
|
1177
|
+
OrigHeight: 0,
|
|
1178
|
+
FileSize: pResult.OutputBuffer.length,
|
|
1179
|
+
NeedsPreview: true,
|
|
1180
|
+
IsRawFormat: pIsRaw,
|
|
1181
|
+
GeneratedAt: new Date().toISOString()
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
try
|
|
1185
|
+
{
|
|
1186
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
1187
|
+
}
|
|
1188
|
+
catch (pWriteError)
|
|
1189
|
+
{
|
|
1190
|
+
tmpSelf.fable.log.warn(`Could not write preview manifest: ${pWriteError.message}`);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
tmpSelf.fable.log.info(`Generated image preview (Ultravisor): ${pRelPath}`);
|
|
1194
|
+
return fCallback(null, tmpResult);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
946
1198
|
// ---------------------------------------------------------------
|
|
947
1199
|
// DZI tile generation
|
|
948
1200
|
// ---------------------------------------------------------------
|
|
@@ -52,9 +52,22 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
52
52
|
this.thumbnailCache = new libThumbnailCache(this.fable);
|
|
53
53
|
this.pathRegistry = this.options.PathRegistry || null;
|
|
54
54
|
|
|
55
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
56
|
+
this._dispatcher = null;
|
|
57
|
+
|
|
55
58
|
this.fable.log.info(`Media Service: capabilities = ${JSON.stringify(this.capabilities)}`);
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
65
|
+
*/
|
|
66
|
+
setDispatcher(pDispatcher)
|
|
67
|
+
{
|
|
68
|
+
this._dispatcher = pDispatcher;
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* Sanitize a file path to prevent directory traversal.
|
|
60
73
|
*
|
|
@@ -483,6 +496,53 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
483
496
|
}
|
|
484
497
|
}
|
|
485
498
|
|
|
499
|
+
// Try Ultravisor dispatch as last resort for image thumbnails.
|
|
500
|
+
// Uses structured MediaConversion capability (orator-conversion beacon)
|
|
501
|
+
// with shell convert fallback if MediaConversion is not available.
|
|
502
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
503
|
+
{
|
|
504
|
+
let tmpRelPath;
|
|
505
|
+
try
|
|
506
|
+
{
|
|
507
|
+
tmpRelPath = libPath.relative(this.contentPath, pFullPath);
|
|
508
|
+
}
|
|
509
|
+
catch (pErr)
|
|
510
|
+
{
|
|
511
|
+
tmpRelPath = null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
515
|
+
{
|
|
516
|
+
let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
|
|
517
|
+
let tmpOutputFilename = `thumbnail.${pFormat === 'webp' ? 'webp' : 'jpg'}`;
|
|
518
|
+
|
|
519
|
+
this._dispatcher.dispatchConversion(
|
|
520
|
+
{
|
|
521
|
+
Action: 'ImageResize',
|
|
522
|
+
InputPath: tmpRelPath,
|
|
523
|
+
OutputFilename: tmpOutputFilename,
|
|
524
|
+
Width: pWidth,
|
|
525
|
+
Height: pHeight,
|
|
526
|
+
Format: tmpOutputFormat,
|
|
527
|
+
Quality: 80,
|
|
528
|
+
AffinityKey: tmpRelPath,
|
|
529
|
+
TimeoutMs: 30000,
|
|
530
|
+
FallbackCommand: `convert "{SourcePath}" -thumbnail ${pWidth}x${pHeight} -auto-orient ${tmpOutputFormat}:"{OutputPath}"`
|
|
531
|
+
},
|
|
532
|
+
(pDispatchError, pResult) =>
|
|
533
|
+
{
|
|
534
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
535
|
+
{
|
|
536
|
+
this.fable.log.info(`Image thumbnail generated via Ultravisor for ${tmpRelPath}`);
|
|
537
|
+
return fCallback(null, pResult.OutputBuffer);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return fCallback(new Error('No image thumbnail tools available.'));
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
486
546
|
return fCallback(new Error('No image thumbnail tools available.'));
|
|
487
547
|
}
|
|
488
548
|
|
|
@@ -623,10 +683,11 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
623
683
|
}
|
|
624
684
|
|
|
625
685
|
/**
|
|
626
|
-
* Fallback raw thumbnail generation: ImageMagick → exifr embedded preview.
|
|
686
|
+
* Fallback raw thumbnail generation: ImageMagick → Ultravisor MediaConversion → exifr embedded preview.
|
|
627
687
|
*/
|
|
628
688
|
_generateRawThumbnailFallback(pFullPath, pWidth, pHeight, pFormat, fCallback)
|
|
629
689
|
{
|
|
690
|
+
let tmpSelf = this;
|
|
630
691
|
let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
|
|
631
692
|
|
|
632
693
|
// Strategy 2: ImageMagick (may have dcraw delegate)
|
|
@@ -640,11 +701,67 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
640
701
|
}
|
|
641
702
|
catch (pError)
|
|
642
703
|
{
|
|
643
|
-
// Fall through to
|
|
704
|
+
// Fall through to Ultravisor
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Strategy 2.5: Try Ultravisor MediaConversion (beacon may have Sharp
|
|
709
|
+
// even if local machine does not have ImageMagick)
|
|
710
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
711
|
+
{
|
|
712
|
+
let tmpRelPath;
|
|
713
|
+
try
|
|
714
|
+
{
|
|
715
|
+
tmpRelPath = libPath.relative(this.contentPath, pFullPath);
|
|
716
|
+
}
|
|
717
|
+
catch (pErr)
|
|
718
|
+
{
|
|
719
|
+
tmpRelPath = null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
723
|
+
{
|
|
724
|
+
let tmpOutputFilename = `thumbnail.${pFormat === 'webp' ? 'webp' : 'jpg'}`;
|
|
725
|
+
|
|
726
|
+
this._dispatcher.dispatchConversion(
|
|
727
|
+
{
|
|
728
|
+
Action: 'ImageResize',
|
|
729
|
+
InputPath: tmpRelPath,
|
|
730
|
+
OutputFilename: tmpOutputFilename,
|
|
731
|
+
Width: pWidth,
|
|
732
|
+
Height: pHeight,
|
|
733
|
+
Format: tmpOutputFormat,
|
|
734
|
+
Quality: 80,
|
|
735
|
+
AffinityKey: tmpRelPath,
|
|
736
|
+
TimeoutMs: 60000,
|
|
737
|
+
FallbackCommand: `convert "{SourcePath}" -thumbnail ${pWidth}x${pHeight} -auto-orient ${tmpOutputFormat}:"{OutputPath}"`
|
|
738
|
+
},
|
|
739
|
+
(pDispatchError, pResult) =>
|
|
740
|
+
{
|
|
741
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
742
|
+
{
|
|
743
|
+
tmpSelf.fable.log.info(`Raw thumbnail generated via Ultravisor for ${tmpRelPath}`);
|
|
744
|
+
return fCallback(null, pResult.OutputBuffer);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Fall through to exifr embedded preview
|
|
748
|
+
tmpSelf._generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback);
|
|
749
|
+
});
|
|
750
|
+
return;
|
|
644
751
|
}
|
|
645
752
|
}
|
|
646
753
|
|
|
647
|
-
|
|
754
|
+
return this._generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Strategy 3 for raw thumbnails: extract embedded JPEG preview via exifr,
|
|
759
|
+
* resize with sharp. Most cameras embed a full-size JPEG preview in the raw file.
|
|
760
|
+
*/
|
|
761
|
+
_generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback)
|
|
762
|
+
{
|
|
763
|
+
let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
|
|
764
|
+
|
|
648
765
|
if (this.capabilities.sharp)
|
|
649
766
|
{
|
|
650
767
|
let tmpSharp = this.capabilities.sharpModule;
|
|
@@ -743,8 +860,62 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
743
860
|
|
|
744
861
|
/**
|
|
745
862
|
* Generate a video thumbnail by extracting a frame with ffmpeg.
|
|
863
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
746
864
|
*/
|
|
747
865
|
_generateVideoThumbnail(pFullPath, pWidth, pHeight, pFormat, fCallback)
|
|
866
|
+
{
|
|
867
|
+
let tmpSelf = this;
|
|
868
|
+
let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'mjpeg';
|
|
869
|
+
|
|
870
|
+
// Try Ultravisor dispatch first
|
|
871
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
872
|
+
{
|
|
873
|
+
let tmpRelPath;
|
|
874
|
+
try
|
|
875
|
+
{
|
|
876
|
+
tmpRelPath = libPath.relative(this.contentPath, pFullPath);
|
|
877
|
+
}
|
|
878
|
+
catch (pErr)
|
|
879
|
+
{
|
|
880
|
+
tmpRelPath = null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
884
|
+
{
|
|
885
|
+
let tmpOutputFilename = `thumbnail.${pFormat === 'webp' ? 'webp' : 'jpg'}`;
|
|
886
|
+
let tmpCommand = `ffmpeg -ss 00:00:02 -i "{SourcePath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpOutputFormat} "{OutputPath}"`;
|
|
887
|
+
|
|
888
|
+
this._dispatcher.dispatchMediaCommand(
|
|
889
|
+
{
|
|
890
|
+
Command: tmpCommand,
|
|
891
|
+
InputPath: tmpRelPath,
|
|
892
|
+
OutputFilename: tmpOutputFilename,
|
|
893
|
+
AffinityKey: tmpRelPath,
|
|
894
|
+
TimeoutMs: 60000
|
|
895
|
+
},
|
|
896
|
+
(pDispatchError, pResult) =>
|
|
897
|
+
{
|
|
898
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
899
|
+
{
|
|
900
|
+
tmpSelf.fable.log.info(`Video thumbnail generated via Ultravisor for ${tmpRelPath}`);
|
|
901
|
+
return fCallback(null, pResult.OutputBuffer);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Fall through to local processing
|
|
905
|
+
tmpSelf.fable.log.info(`Ultravisor dispatch failed for video thumbnail, falling back to local: ${pDispatchError ? pDispatchError.message : 'no output'}`);
|
|
906
|
+
tmpSelf._generateVideoThumbnailLocal(pFullPath, pWidth, pHeight, pFormat, fCallback);
|
|
907
|
+
});
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return this._generateVideoThumbnailLocal(pFullPath, pWidth, pHeight, pFormat, fCallback);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Generate a video thumbnail locally using ffmpeg.
|
|
917
|
+
*/
|
|
918
|
+
_generateVideoThumbnailLocal(pFullPath, pWidth, pHeight, pFormat, fCallback)
|
|
748
919
|
{
|
|
749
920
|
try
|
|
750
921
|
{
|
|
@@ -799,60 +970,126 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
799
970
|
|
|
800
971
|
/**
|
|
801
972
|
* Run ffprobe and parse the output.
|
|
973
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
802
974
|
*/
|
|
803
975
|
_ffprobe(pFullPath, fCallback)
|
|
976
|
+
{
|
|
977
|
+
let tmpSelf = this;
|
|
978
|
+
|
|
979
|
+
// Try Ultravisor dispatch first
|
|
980
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
981
|
+
{
|
|
982
|
+
let tmpRelPath;
|
|
983
|
+
try
|
|
984
|
+
{
|
|
985
|
+
tmpRelPath = libPath.relative(this.contentPath, pFullPath);
|
|
986
|
+
}
|
|
987
|
+
catch (pErr)
|
|
988
|
+
{
|
|
989
|
+
tmpRelPath = null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
993
|
+
{
|
|
994
|
+
let tmpCommand = `ffprobe -v quiet -print_format json -show_format -show_streams "{SourcePath}"`;
|
|
995
|
+
|
|
996
|
+
this._dispatcher.dispatchMediaCommand(
|
|
997
|
+
{
|
|
998
|
+
Command: tmpCommand,
|
|
999
|
+
InputPath: tmpRelPath,
|
|
1000
|
+
AffinityKey: tmpRelPath,
|
|
1001
|
+
TimeoutMs: 30000
|
|
1002
|
+
},
|
|
1003
|
+
(pDispatchError, pResult) =>
|
|
1004
|
+
{
|
|
1005
|
+
if (!pDispatchError && pResult && pResult.Outputs && pResult.Outputs.StdOut)
|
|
1006
|
+
{
|
|
1007
|
+
try
|
|
1008
|
+
{
|
|
1009
|
+
let tmpData = JSON.parse(pResult.Outputs.StdOut);
|
|
1010
|
+
let tmpParsed = tmpSelf._parseFfprobeData(tmpData);
|
|
1011
|
+
tmpSelf.fable.log.info(`ffprobe via Ultravisor for ${tmpRelPath}`);
|
|
1012
|
+
return fCallback(null, tmpParsed);
|
|
1013
|
+
}
|
|
1014
|
+
catch (pParseError)
|
|
1015
|
+
{
|
|
1016
|
+
// Fall through to local
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Fall through to local processing
|
|
1021
|
+
tmpSelf._ffprobeLocal(pFullPath, fCallback);
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return this._ffprobeLocal(pFullPath, fCallback);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Run ffprobe locally and parse the output.
|
|
1032
|
+
*/
|
|
1033
|
+
_ffprobeLocal(pFullPath, fCallback)
|
|
804
1034
|
{
|
|
805
1035
|
try
|
|
806
1036
|
{
|
|
807
1037
|
let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pFullPath}"`;
|
|
808
1038
|
let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 10000 });
|
|
809
1039
|
let tmpData = JSON.parse(tmpOutput.toString());
|
|
1040
|
+
return fCallback(null, this._parseFfprobeData(tmpData));
|
|
1041
|
+
}
|
|
1042
|
+
catch (pError)
|
|
1043
|
+
{
|
|
1044
|
+
return fCallback(pError);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
810
1047
|
|
|
811
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* Parse ffprobe JSON output into a normalized result object.
|
|
1050
|
+
*/
|
|
1051
|
+
_parseFfprobeData(pData)
|
|
1052
|
+
{
|
|
1053
|
+
let tmpResult = {};
|
|
812
1054
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1055
|
+
if (pData.format)
|
|
1056
|
+
{
|
|
1057
|
+
tmpResult.duration = parseFloat(pData.format.duration) || null;
|
|
1058
|
+
tmpResult.bitrate = parseInt(pData.format.bit_rate, 10) || null;
|
|
817
1059
|
|
|
818
|
-
|
|
819
|
-
|
|
1060
|
+
// Extract format-level tags (ID3, Vorbis comments, etc.)
|
|
1061
|
+
if (pData.format.tags)
|
|
1062
|
+
{
|
|
1063
|
+
tmpResult.tags = {};
|
|
1064
|
+
let tmpTagKeys = Object.keys(pData.format.tags);
|
|
1065
|
+
for (let t = 0; t < tmpTagKeys.length; t++)
|
|
820
1066
|
{
|
|
821
|
-
tmpResult.tags =
|
|
822
|
-
let tmpTagKeys = Object.keys(tmpData.format.tags);
|
|
823
|
-
for (let t = 0; t < tmpTagKeys.length; t++)
|
|
824
|
-
{
|
|
825
|
-
tmpResult.tags[tmpTagKeys[t].toLowerCase()] = tmpData.format.tags[tmpTagKeys[t]];
|
|
826
|
-
}
|
|
1067
|
+
tmpResult.tags[tmpTagKeys[t].toLowerCase()] = pData.format.tags[tmpTagKeys[t]];
|
|
827
1068
|
}
|
|
828
1069
|
}
|
|
1070
|
+
}
|
|
829
1071
|
|
|
830
|
-
|
|
831
|
-
|
|
1072
|
+
// Find video stream for dimensions
|
|
1073
|
+
if (pData.streams)
|
|
1074
|
+
{
|
|
1075
|
+
for (let i = 0; i < pData.streams.length; i++)
|
|
832
1076
|
{
|
|
833
|
-
|
|
1077
|
+
let tmpStream = pData.streams[i];
|
|
1078
|
+
if (tmpStream.codec_type === 'video')
|
|
834
1079
|
{
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if (tmpStream.codec_type === 'audio' && !tmpResult.codec)
|
|
844
|
-
{
|
|
845
|
-
tmpResult.codec = tmpStream.codec_name;
|
|
846
|
-
}
|
|
1080
|
+
tmpResult.width = tmpStream.width;
|
|
1081
|
+
tmpResult.height = tmpStream.height;
|
|
1082
|
+
tmpResult.codec = tmpStream.codec_name;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
if (tmpStream.codec_type === 'audio' && !tmpResult.codec)
|
|
1086
|
+
{
|
|
1087
|
+
tmpResult.codec = tmpStream.codec_name;
|
|
847
1088
|
}
|
|
848
1089
|
}
|
|
849
|
-
|
|
850
|
-
return fCallback(null, tmpResult);
|
|
851
|
-
}
|
|
852
|
-
catch (pError)
|
|
853
|
-
{
|
|
854
|
-
return fCallback(pError);
|
|
855
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
return tmpResult;
|
|
856
1093
|
}
|
|
857
1094
|
}
|
|
858
1095
|
|