Skip to content

Commit 423f1c4

Browse files
Multicam web features phase 4 (#1665)
* Multicam web features phase 2 (#1660) * multcam web planning document * Multicam web feature phase 1 (#1659) * model updates * dataset verification for multicam * multcam media and update media endpoints for multicam * tests and update plan * multicam config for desktop init * update metadata requests for multicam data * implement tests and update plan * Revert "Multicam web features phase 2 (#1660)" (#1661) This reverts commit 0c2cc6e. * Bump idna from 3.13 to 3.15 in /server (#1664) Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](kjd/idna@v3.13...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * discover stereo/multicam pipelines * support for multicam arguments when running tasks * add multicam logic for running multicam/stereo tasks * updating tests * update base planning document * manual docs deployment, or on changes in main (#1666) * fix docker image references (#1667) * fix pipeline categories and display * pipeline menu styling updates * Bump qs, body-parser and express in /client (#1668) Bumps [qs](https://github.com/ljharb/qs) to 6.15.2 and updates ancestor dependencies [qs](https://github.com/ljharb/qs), [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `qs` from 6.13.0 to 6.15.2 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](ljharb/qs@v6.13.0...v6.15.2) Updates `body-parser` from 1.20.3 to 1.20.5 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/1.20.5/HISTORY.md) - [Commits](expressjs/body-parser@1.20.3...1.20.5) Updates `express` from 4.20.0 to 4.22.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md) - [Commits](expressjs/express@4.20.0...v4.22.2) --- updated-dependencies: - dependency-name: qs dependency-version: 6.15.2 dependency-type: indirect - dependency-name: body-parser dependency-version: 1.20.5 dependency-type: direct:development - dependency-name: express dependency-version: 4.22.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * linting * docuemtnation updates * stereoscopic support * revert common stereo changes * handling task locations * Bump tmp and ffmpeg-ffprobe-static in /client (#1669) Bumps [tmp](https://github.com/raszi/node-tmp) to 0.2.6 and updates ancestor dependency [ffmpeg-ffprobe-static](https://github.com/descriptinc/ffmpeg-ffprobe-static). These dependencies need to be updated together. Updates `tmp` from 0.2.5 to 0.2.6 - [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md) - [Commits](raszi/node-tmp@v0.2.5...v0.2.6) Updates `ffmpeg-ffprobe-static` from 4.4.0-rc.11 to 6.1.2-rc.1 - [Release notes](https://github.com/descriptinc/ffmpeg-ffprobe-static/releases) - [Commits](descriptinc/ffmpeg-ffprobe-static@b4.4.0-rc.11...b6.1.2-rc.1) --- updated-dependencies: - dependency-name: tmp dependency-version: 0.2.6 dependency-type: indirect - dependency-name: ffmpeg-ffprobe-static dependency-version: 6.1.2-rc.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * development overview (#1670) * update to allow video folder importing for stereoscopic video * stereoscopic calibration file imports * multicam import status * linting and tests * update rabbit mq timeouts (#1674) * Shortcut fix (#1676) * fix the shortcuts in the default view * fix select next/previous * fix virtual scrolling (#1678) * add VIAME csv generation to import assetStore testing (#1679) * allow non-utf characters in logs for multicam pipelines * icon alignment, return to data * update multicam toolbar functionality * update plan --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent a0be985 commit 423f1c4

74 files changed

Lines changed: 3565 additions & 998 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docs.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish Docs
2+
on:
3+
workflow_dispatch:
4+
release:
5+
types: [published]
6+
push:
7+
branches:
8+
- main
9+
paths:
10+
- 'docs/**'
11+
- 'mkdocs.yml'
12+
13+
jobs:
14+
docs:
15+
name: Deploy docs
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v2
19+
with:
20+
ref: ${{ github.event_name == 'release' && github.event.release.target_commitish || github.ref }}
21+
- name: Deploy docs
22+
uses: mhausenblas/mkdocs-deploy-gh-pages@master
23+
env:
24+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25+
CONFIG_FILE: mkdocs.yml
26+
EXTRA_PACKAGES: build-base

.github/workflows/release.yml

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Electron Build and Docs Deployment
1+
name: Electron Build
22
on:
33
release:
44
types: [published]
@@ -43,21 +43,3 @@ jobs:
4343
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4444
with:
4545
asset_paths: '["./client/dist_electron/DIVE-Desktop*"]'
46-
47-
docs:
48-
name: Deploy docs
49-
runs-on: ubuntu-latest
50-
steps:
51-
- uses: actions/checkout@v2
52-
with:
53-
# "ref" specifies the branch to check out.
54-
# "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted
55-
ref: ${{ github.event.release.target_commitish }}
56-
57-
# Deploy docs
58-
- name: Deploy docs
59-
uses: mhausenblas/mkdocs-deploy-gh-pages@master
60-
env:
61-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62-
CONFIG_FILE: mkdocs.yml
63-
EXTRA_PACKAGES: build-base

WEB_MULTICAM_PLAN.MD

Lines changed: 131 additions & 134 deletions
Large diffs are not rendered by default.

client/dive-common/apispec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ interface Api {
230230
Promise<{canceled?: boolean; filePaths: string[]; fileList?: File[]; root?: string}>;
231231
/** Desktop: immediate child directory names under a parent folder (multicam subfolder import). */
232232
listImmediateSubfolders?(parentPath: string): Promise<string[]>;
233+
/** Desktop: subfolders or root-level video files under a parent folder (multicam import). */
234+
listParentFolderCameras?(
235+
parentPath: string,
236+
mediaType: 'image-sequence' | 'video',
237+
): Promise<{ name: string; sourcePath: string }[]>;
233238
/** Desktop: folder path for image-sequence, or first video file inside the folder for video. */
234239
resolveMulticamCameraSourcePath?(
235240
subfolderPath: string,

client/dive-common/components/ImportMultiCamDialog/ImportMultiCamSubfolders.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ export default defineComponent({
5555
dense
5656
class="mb-3"
5757
>
58-
Choose a parent folder containing one subfolder per camera (2 or 3 subfolders).
59-
Each subfolder name becomes the camera name (letters and numbers only).
58+
Choose a parent folder with either one subfolder per camera (2 or 3 subfolders)
59+
or separate video files in the folder (2 or 3 videos). Names come from the subfolder
60+
or video file name (letters and numbers only).
6061
</v-alert>
6162
<v-row
6263
no-gutters

client/dive-common/components/ImportMultiCamDialog/ImportMultiCamTypeSelector.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default defineComponent({
4040
<v-radio
4141
v-if="enableSubfolderImport"
4242
value="subfolders"
43-
label="Parent folder: each immediate subfolder is a camera"
43+
label="Parent folder: subfolders or separate video files per camera"
4444
/>
4545
<v-radio
4646
v-if="dataType === 'image-sequence'"

client/dive-common/components/ImportMultiCamDialog/multicamSubfolderLayout.spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest';
44
import {
55
applyParentPathToAssignments,
66
groupFilesByImmediateSubfolder,
7+
groupParentFolderByCamera,
8+
groupRootLevelVideoFiles,
79
isValidCameraName,
10+
isVideoFileName,
811
organizeSubfolderCameras,
912
orderSubfolderCameraNames,
1013
preferLeftSubfolderFirst,
@@ -115,8 +118,63 @@ describe('organizeSubfolderCameras', () => {
115118
});
116119

117120
it('rejects wrong folder count', () => {
118-
expect(organizeSubfolderCameras(['only']).error).toMatch(/Expected 2 or 3/);
119-
expect(organizeSubfolderCameras(['a', 'b', 'c', 'd']).error).toMatch(/Expected 2 or 3/);
121+
expect(organizeSubfolderCameras(['only']).error).toMatch(/Expected 2 or 3 cameras/);
122+
expect(organizeSubfolderCameras(['a', 'b', 'c', 'd']).error).toMatch(/Expected 2 or 3 cameras/);
123+
});
124+
});
125+
126+
describe('isVideoFileName', () => {
127+
it('recognizes common video extensions', () => {
128+
expect(isVideoFileName('left.mp4')).toBe(true);
129+
expect(isVideoFileName('right.MOV')).toBe(true);
130+
expect(isVideoFileName('notes.txt')).toBe(false);
131+
});
132+
});
133+
134+
describe('groupRootLevelVideoFiles', () => {
135+
const mk = (path: string) => ({ webkitRelativePath: path, name: path.split('/').pop() } as File);
136+
137+
it('groups videos directly under the parent folder by file stem', () => {
138+
const groups = groupRootLevelVideoFiles([
139+
mk('stereo/left.mp4'),
140+
mk('stereo/right.mp4'),
141+
mk('stereo/readme.txt'),
142+
], 'stereo');
143+
expect([...groups.keys()].sort()).toEqual(['left', 'right']);
144+
expect(groups.get('left')?.length).toBe(1);
145+
expect(groups.get('right')?.length).toBe(1);
146+
});
147+
});
148+
149+
describe('groupParentFolderByCamera', () => {
150+
const mk = (path: string) => ({ webkitRelativePath: path, name: path.split('/').pop() } as File);
151+
152+
it('prefers subfolders when at least two exist', () => {
153+
const groups = groupParentFolderByCamera([
154+
mk('set/left/a.mp4'),
155+
mk('set/right/b.mp4'),
156+
mk('set/left_cam.mp4'),
157+
], { allowRootLevelVideos: true }, 'set');
158+
expect([...groups.keys()].sort()).toEqual(['left', 'right']);
159+
});
160+
161+
it('falls back to root-level videos when there are no subfolders', () => {
162+
const groups = groupParentFolderByCamera([
163+
mk('stereo/left.mp4'),
164+
mk('stereo/right.mp4'),
165+
], { allowRootLevelVideos: true }, 'stereo');
166+
expect([...groups.keys()].sort()).toEqual(['left', 'right']);
167+
const organized = organizeSubfolderCameras([...groups.keys()], { preferLeftForStereo: true });
168+
expect(organized.error).toBeNull();
169+
expect(organized.assignments.map((a) => a.cameraName)).toEqual(['left', 'right']);
170+
});
171+
172+
it('does not use root-level videos when subfolder import is disabled', () => {
173+
const groups = groupParentFolderByCamera([
174+
mk('stereo/left.mp4'),
175+
mk('stereo/right.mp4'),
176+
], undefined, 'stereo');
177+
expect(groups.size).toBe(0);
120178
});
121179
});
122180

client/dive-common/components/ImportMultiCamDialog/multicamSubfolderLayout.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fileVideoTypes } from 'dive-common/constants';
2+
13
/** Assign immediate child folders to multicam cameras (one camera per subfolder). */
24

35
export interface SubfolderCameraAssignment {
@@ -162,7 +164,7 @@ export function organizeSubfolderCameras(
162164
if (unique.length < 2 || unique.length > 3) {
163165
return {
164166
...empty,
165-
error: `Expected 2 or 3 camera subfolders, found ${unique.length} (${unique.join(', ')})`,
167+
error: `Expected 2 or 3 cameras (subfolders or video files), found ${unique.length} (${unique.join(', ')})`,
166168
};
167169
}
168170

@@ -261,3 +263,62 @@ export function groupFilesByImmediateSubfolder(
261263

262264
return groups;
263265
}
266+
267+
export function isVideoFileName(fileName: string): boolean {
268+
const parts = fileName.split('.');
269+
if (parts.length < 2) {
270+
return false;
271+
}
272+
const ext = parts.pop()?.toLowerCase() ?? '';
273+
return fileVideoTypes.includes(ext);
274+
}
275+
276+
/**
277+
* Group video files that sit directly in the selected parent folder (one camera per file).
278+
* Camera keys are the file stem (basename without extension).
279+
*/
280+
export function groupRootLevelVideoFiles(
281+
fileList: File[],
282+
root = '',
283+
): Map<string, File[]> {
284+
const groups = new Map<string, File[]>();
285+
const paths = fileList.map((file) => file.webkitRelativePath || file.name);
286+
const effectiveRoot = root || commonPathPrefix(paths);
287+
288+
fileList.forEach((file, index) => {
289+
const rel = paths[index];
290+
const path = stripPathPrefix(rel, effectiveRoot);
291+
const parts = path.split('/').filter(Boolean);
292+
if (parts.length !== 1 || !isVideoFileName(parts[0])) {
293+
return;
294+
}
295+
const stem = parts[0].replace(/\.[^.]+$/, '');
296+
const existing = groups.get(stem) ?? [];
297+
existing.push(file);
298+
groups.set(stem, existing);
299+
});
300+
301+
return groups;
302+
}
303+
304+
/**
305+
* Group a parent-folder selection by camera: prefer immediate subfolders; for video imports,
306+
* fall back to separate video files in the parent folder when there are not enough subfolders.
307+
*/
308+
export function groupParentFolderByCamera(
309+
fileList: File[],
310+
options?: { allowRootLevelVideos?: boolean },
311+
root = '',
312+
): Map<string, File[]> {
313+
const subfolderGroups = groupFilesByImmediateSubfolder(fileList, root);
314+
if (subfolderGroups.size >= 2) {
315+
return subfolderGroups;
316+
}
317+
if (options?.allowRootLevelVideos) {
318+
const videoGroups = groupRootLevelVideoFiles(fileList, root);
319+
if (videoGroups.size >= 2) {
320+
return videoGroups;
321+
}
322+
}
323+
return subfolderGroups;
324+
}

client/dive-common/components/ImportMultiCamDialog/useImportMultiCamDialog.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import {
1818
applyParentPathToAssignments,
1919
commonPathPrefix,
20-
groupFilesByImmediateSubfolder,
20+
groupParentFolderByCamera,
2121
isValidCameraName,
2222
organizeSubfolderCameras,
2323
pickDefaultMulticamCamera,
@@ -51,7 +51,7 @@ export function useImportMultiCamDialog(
5151
openFromDisk,
5252
getLastCalibration,
5353
saveCalibration,
54-
listImmediateSubfolders,
54+
listParentFolderCameras,
5555
resolveMulticamCameraSourcePath,
5656
} = useApi();
5757
const importType: Ref<MulticamImportType> = ref('');
@@ -249,7 +249,7 @@ export function useImportMultiCamDialog(
249249
return;
250250
}
251251
const useDesktopDiscovery = !ret.fileList?.length && !!ret.filePaths?.[0]
252-
&& !!listImmediateSubfolders;
252+
&& !!listParentFolderCameras;
253253
if (!ret.fileList?.length && !useDesktopDiscovery) {
254254
return;
255255
}
@@ -258,16 +258,21 @@ export function useImportMultiCamDialog(
258258
let parentPath = '';
259259
let grouped: Map<string, File[]> | undefined;
260260
let folderNames: string[] = [];
261+
let desktopCameras: { name: string; sourcePath: string }[] | undefined;
262+
const mediaType = props.dataType === VideoType ? 'video' : 'image-sequence';
261263

262264
if (ret.fileList?.length) {
263265
const paths = ret.fileList.map((f) => f.webkitRelativePath || f.name);
264266
parentPath = ret.root || commonPathPrefix(paths);
265-
grouped = groupFilesByImmediateSubfolder(ret.fileList, parentPath);
267+
grouped = groupParentFolderByCamera(ret.fileList, {
268+
allowRootLevelVideos: props.dataType === VideoType,
269+
}, parentPath);
266270
folderNames = [...grouped.keys()];
267271
} else {
268272
const [firstPath] = ret.filePaths;
269273
parentPath = firstPath;
270-
folderNames = await listImmediateSubfolders!(parentPath);
274+
desktopCameras = await listParentFolderCameras!(parentPath, mediaType);
275+
folderNames = desktopCameras.map((camera) => camera.name);
271276
}
272277

273278
const organized = organizeSubfolderCameras(folderNames, {
@@ -286,9 +291,19 @@ export function useImportMultiCamDialog(
286291

287292
let { assignments } = organized;
288293
if (useDesktopDiscovery) {
289-
assignments = applyParentPathToAssignments(parentPath, assignments);
294+
if (desktopCameras?.length) {
295+
assignments = assignments.map((assignment) => {
296+
const discovered = desktopCameras?.find(
297+
(camera) => camera.name === assignment.folderName,
298+
);
299+
return discovered
300+
? { ...assignment, sourcePath: discovered.sourcePath }
301+
: assignment;
302+
});
303+
} else {
304+
assignments = applyParentPathToAssignments(parentPath, assignments);
305+
}
290306
if (resolveMulticamCameraSourcePath) {
291-
const mediaType = props.dataType === VideoType ? 'video' : 'image-sequence';
292307
assignments = await Promise.all(assignments.map(async (assignment) => ({
293308
...assignment,
294309
sourcePath: await resolveMulticamCameraSourcePath(assignment.sourcePath, mediaType),
@@ -313,7 +328,7 @@ export function useImportMultiCamDialog(
313328
for (let i = 0; i < registryPayload.length; i += 1) {
314329
const { cameraName, sourcePath, files } = registryPayload[i];
315330
if (grouped && !files.length) {
316-
throw new Error(`Subfolder "${organized.assignments[i].folderName}" has no media files`);
331+
throw new Error(`Camera "${organized.assignments[i].folderName}" has no media files`);
317332
}
318333
Vue.set(subfolderOriginalNames.value, cameraName, organized.assignments[i].folderName);
319334
Vue.set(folderList.value, cameraName, { sourcePath, trackFile: '' });
@@ -350,7 +365,7 @@ export function useImportMultiCamDialog(
350365
}
351366

352367
Vue.set(folderList.value, newKey, {
353-
sourcePath: (importType.value === 'subfolders' && !listImmediateSubfolders) ? newKey : sourcePath,
368+
sourcePath: (importType.value === 'subfolders' && !listParentFolderCameras) ? newKey : sourcePath,
354369
trackFile: entry.trackFile,
355370
});
356371
Vue.delete(folderList.value, oldKey);

0 commit comments

Comments
 (0)