Skip to content

Commit 551e63e

Browse files
Exit to the library when a dataset's media can't be loaded (#1998)
* Exit to the library when a dataset's media can't be loaded The desktop pre-load check now covers each camera's source folder, and image, video, or dataset load failures show an error and leave the viewer instead of spinning forever. * fixes largeimageWarning, eliminate false positives --------- Co-authored-by: Bryon Lewis <Bryon.Lewis@kitware.com>
1 parent 8e18829 commit 551e63e

7 files changed

Lines changed: 115 additions & 13 deletions

File tree

‎client/dive-common/components/Viewer.vue‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,9 +2155,13 @@ export default defineComponent({
21552155
errorEl.innerHTML = getResponseError(err);
21562156
loadError.value = errorEl.innerText
21572157
.concat(". If you don't know how to resolve this, please contact the server administrator.");
2158+
emit('load-error', loadError.value);
21582159
throw err;
21592160
}
21602161
};
2162+
function forwardLoadError(message: string, largeImage?: boolean) {
2163+
emit('load-error', message, largeImage);
2164+
}
21612165
loadData();
21622166
21632167
/**
@@ -2535,6 +2539,7 @@ export default defineComponent({
25352539
imageData,
25362540
lineChartData,
25372541
loadError,
2542+
forwardLoadError,
25382543
multiSelectActive,
25392544
lassoModeActive: lassoMode.lassoModeActive,
25402545
lassoDrawing: lassoMode.lassoDrawing,
@@ -3011,6 +3016,7 @@ export default defineComponent({
30113016
filterId: `imageEnhancements-${camera}`,
30123017
}"
30133018
@large-image-warning="$emit('large-image-warning', true)"
3019+
@load-error="forwardLoadError"
30143020
>
30153021
<LayerManager :camera="camera" />
30163022
</component>
@@ -3109,6 +3115,7 @@ export default defineComponent({
31093115
filterId: `imageEnhancements-${camera}`,
31103116
}"
31113117
@large-image-warning="$emit('large-image-warning', true)"
3118+
@load-error="forwardLoadError"
31123119
>
31133120
<LayerManager :camera="camera" />
31143121
</component>

‎client/platform/desktop/backend/native/common.spec.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,30 @@ beforeEach(() => {
625625
},
626626
},
627627
},
628+
projectidMulticamMovedSource: {
629+
'meta.json': JSON.stringify({
630+
version: 1,
631+
id: 'projectidMulticamMovedSource',
632+
name: 'movedStereo',
633+
type: 'multi',
634+
fps: 5,
635+
originalBasePath: '',
636+
multiCam: {
637+
defaultDisplay: 'left',
638+
cameras: {
639+
left: {
640+
type: 'image-sequence',
641+
originalBasePath: '/home/user/data/movedStereo/left',
642+
},
643+
right: {
644+
type: 'image-sequence',
645+
originalBasePath: '/home/user/data/movedStereo/right',
646+
},
647+
},
648+
},
649+
}),
650+
'result_whatever.json': JSON.stringify({}),
651+
},
628652
projectidFrameMetadata: {
629653
'meta.json': JSON.stringify({
630654
version: 1,
@@ -2816,6 +2840,9 @@ describe('native.common', () => {
28162840
await expect(common.checkDataset(settings, 'projectid3Bad')).rejects.toThrow('missing dataset json');
28172841
await expect(common.checkDataset(settings, 'projectid5Bad')).rejects.toThrow('missing track json file');
28182842
await expect(common.checkDataset(settings, 'missingFolder')).rejects.toThrow('missing project directory');
2843+
await expect(common.checkDataset(settings, 'projectidMulticamMovedSource')).rejects.toThrow(
2844+
'Dataset movedStereo does not contain source files at /home/user/data/movedStereo/left, /home/user/data/movedStereo/right',
2845+
);
28192846
});
28202847

28212848
it('checkDataset does not create directories for missing datasets', async () => {

‎client/platform/desktop/backend/native/common.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,11 +2061,16 @@ async function checkDataset(
20612061
): Promise<boolean> {
20622062
const projectDirData = await getValidatedProjectDir(settings, datasetId);
20632063
const projectMetaData = await loadJsonConfig(projectDirData.datasetFileAbsPath);
2064-
if (projectMetaData.originalBasePath !== '') {
2065-
const exists = await fs.pathExists(projectMetaData.originalBasePath);
2066-
if (!exists) {
2067-
throw new Error(`Dataset ${projectMetaData.name} does not contain source files at ${projectMetaData.originalBasePath}`);
2068-
}
2064+
const sourcePaths = [
2065+
...new Set([
2066+
projectMetaData.originalBasePath,
2067+
...Object.values(projectMetaData.multiCam?.cameras ?? {}).map((camera) => camera.originalBasePath),
2068+
].filter((path) => path)),
2069+
];
2070+
const exists = await Promise.all(sourcePaths.map((path) => fs.pathExists(path)));
2071+
const missing = sourcePaths.filter((_, index) => !exists[index]);
2072+
if (missing.length) {
2073+
throw new Error(`Dataset ${projectMetaData.name} does not contain source files at ${missing.join(', ')}`);
20692074
}
20702075
if (projectMetaData.error && projectMetaData.error !== '') {
20712076
throw new Error(`Dataset ${projectMetaData.name} contains error: ${projectMetaData.error}`);

‎client/platform/desktop/frontend/components/ViewerLoader.vue‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,24 @@ export default defineComponent({
256256
return results;
257257
});
258258
259+
let loadFailed = false;
260+
async function handleLoadError(message: string, largeImage?: boolean) {
261+
if (loadFailed) {
262+
return;
263+
}
264+
loadFailed = true;
265+
if (largeImage) {
266+
await largeImageWarning();
267+
} else {
268+
await prompt({
269+
title: 'Error Loading Data',
270+
text: [message],
271+
positiveButton: 'Okay',
272+
});
273+
}
274+
router.push({ name: 'recent' });
275+
}
276+
259277
async function largeImageWarning() {
260278
await prompt({
261279
title: 'Large Image Warning',
@@ -2565,6 +2583,7 @@ export default defineComponent({
25652583
readOnlyMode,
25662584
runningPipelines,
25672585
largeImageWarning,
2586+
handleLoadError,
25682587
timeFilter,
25692588
handleTextQuerySubmit,
25702589
handleTextQueryInit,
@@ -2620,6 +2639,7 @@ export default defineComponent({
26202639
@return-to-current-annotations="returnToCurrentAnnotations"
26212640
@change-camera="changeCamera"
26222641
@large-image-warning="largeImageWarning()"
2642+
@load-error="handleLoadError"
26232643
@text-query-submit="handleTextQuerySubmit"
26242644
@text-query-init="handleTextQueryInit"
26252645
@text-query-all-frames="handleTextQueryAllFrames"

‎client/platform/web-girder/views/ViewerLoader.vue‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { parentDatasetId } from 'dive-common/compositeDatasetId';
2323
import { getMultiCamCameraCount } from 'dive-common/pipelineMenuFilters';
2424
import { webExcludedPipelineTerms } from 'dive-common/constants';
2525
import { convertLargeImage } from 'platform/web-girder/api/rpc.service';
26+
import type { RawLocation } from 'vue-router';
2627
import { useRouter, useRoute } from 'vue-router/composables';
2728
import { ANNOTATION_SOURCE_QUERY } from 'dive-common/scoring/viewerNavigation';
2829
import { parseViewerFocus } from 'dive-common/review/viewerNavigation';
@@ -544,6 +545,24 @@ export default defineComponent({
544545
}
545546
}
546547
548+
let loadFailed = false;
549+
async function handleLoadError(message: string, largeImage?: boolean) {
550+
if (loadFailed) {
551+
return;
552+
}
553+
loadFailed = true;
554+
if (largeImage) {
555+
await largeImageWarning();
556+
} else {
557+
await prompt({
558+
title: 'Error Loading Data',
559+
text: [message],
560+
positiveButton: 'Okay',
561+
});
562+
}
563+
router.push(locationRoute.value as RawLocation);
564+
}
565+
547566
const annotationSourceLabel = computed(() => {
548567
const value = route.query[ANNOTATION_SOURCE_QUERY];
549568
return typeof value === 'string' ? value : '';
@@ -571,6 +590,7 @@ export default defineComponent({
571590
routeRevision,
572591
routeSet,
573592
largeImageWarning,
593+
handleLoadError,
574594
typeList,
575595
subTypeList,
576596
cameraNumbers,
@@ -638,6 +658,7 @@ export default defineComponent({
638658
:auto-populate-status="segmentationStatus"
639659
@return-to-current-annotations="returnToCurrentAnnotations"
640660
@large-image-warning="largeImageWarning()"
661+
@load-error="handleLoadError"
641662
@update:set="routeSet"
642663
@change-camera="changeCamera"
643664
@stereo-annotation-complete="handleStereoAnnotationComplete"

‎client/src/components/annotators/ImageAnnotator.vue‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ export default defineComponent({
125125
}
126126
return imgInternal;
127127
}
128+
function failedToLoad(imgInternal: ImageDataItemInternal, loaded: boolean) {
129+
if (loaded || local.imgs[imgInternal.frame] !== imgInternal) {
130+
return false;
131+
}
132+
loadingImage.value = false;
133+
emit('load-error', `Could not load ${imgInternal.filename}. The file may have been moved or deleted.`);
134+
return true;
135+
}
128136
/**
129137
* Draw image to the GeoJS map, and update the map dimensions if they have changed.
130138
*/
@@ -277,8 +285,8 @@ export default defineComponent({
277285
if (!imgInternal.cached) {
278286
loadingImage.value = true;
279287
// else wait for it to load
280-
await imgInternal.onloadPromise;
281-
if (imgInternal.frame === data.frame) {
288+
const loaded = await imgInternal.onloadPromise;
289+
if (imgInternal.frame === data.frame && !failedToLoad(imgInternal, loaded)) {
282290
loadingImage.value = false;
283291
// if the seek hasn't changed since the image completed loading, draw it.
284292
drawImage(imgInternal.image);
@@ -368,11 +376,21 @@ export default defineComponent({
368376
369377
if (local.imgs.length) {
370378
const imgInternal = cacheFrame(0);
371-
imgInternal.onloadPromise.then(async () => {
379+
imgInternal.onloadPromise.then(async (loaded) => {
380+
if (imgInternal.frame !== data.frame || failedToLoad(imgInternal, loaded)) {
381+
return;
382+
}
372383
try {
373384
await imgInternal.image.decode();
374385
} catch (error) {
375-
emit('large-image-warning', true);
386+
if (imgInternal.frame !== data.frame) {
387+
return;
388+
}
389+
loadingImage.value = false;
390+
emit('load-error', `Could not display ${imgInternal.filename}. Its resolution may be too large for this browser or hardware.`, true);
391+
return;
392+
}
393+
if (imgInternal.frame !== data.frame) {
376394
return;
377395
}
378396
initializeViewer(imgInternal.image.naturalWidth, imgInternal.image.naturalHeight);
@@ -428,8 +446,8 @@ export default defineComponent({
428446
drawImage(imgInternal.image);
429447
if (!imgInternal.cached) {
430448
loadingImage.value = true;
431-
imgInternal.onloadPromise.then(() => {
432-
if (imgInternal.frame === data.frame) {
449+
imgInternal.onloadPromise.then((loaded) => {
450+
if (imgInternal.frame === data.frame && !failedToLoad(imgInternal, loaded)) {
433451
loadingImage.value = false;
434452
drawImage(imgInternal.image);
435453
}
@@ -440,7 +458,10 @@ export default defineComponent({
440458
return;
441459
}
442460
const imgInternal = cacheFrame(0);
443-
imgInternal.onloadPromise.then(() => {
461+
imgInternal.onloadPromise.then((loaded) => {
462+
if (failedToLoad(imgInternal, loaded)) {
463+
return;
464+
}
444465
initializeViewer(imgInternal.image.naturalWidth, imgInternal.image.naturalHeight);
445466
const quadFeatureLayer = geoViewer.value.createLayer('feature', {
446467
features: ['quad'],

‎client/src/components/annotators/VideoAnnotator.vue‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default defineComponent({
5555
default: 'imageEnhancements',
5656
},
5757
},
58-
setup(props) {
58+
setup(props, { emit }) {
5959
const cameraInitializer = injectCameraInitializer();
6060
const {
6161
state: data,
@@ -168,6 +168,7 @@ export default defineComponent({
168168
}
169169
function logError(event: ErrorEvent) {
170170
console.error('Media failed to initialize', event);
171+
emit('load-error', 'Could not load the video. The file may have been moved or deleted.');
171172
}
172173
function setVolume(level: number) {
173174
video.volume = level;

0 commit comments

Comments
 (0)