Skip to content

Commit 9fbaedd

Browse files
video: refactor: Address PR review requests
1 parent 40d8509 commit 9fbaedd

5 files changed

Lines changed: 208 additions & 47 deletions

File tree

src/components/VideoLibraryModal.vue

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@
188188
<!-- Videos Tab with Sub-tabs -->
189189
<div class="flex flex-col h-full w-full">
190190
<!-- Sub-tabs Navigation -->
191-
<div class="px-4 pt-4 pb-2">
191+
<div class="px-4 pt-2 pb-2">
192192
<v-tabs v-model="currentVideoSubTab" color="white" fixed-tabs class="video-sub-tabs">
193193
<v-tab
194194
v-for="tab in videoSubTabs"
@@ -283,7 +283,7 @@
283283
@click.stop="playVideoInDefaultPlayer(video.fileName)"
284284
>
285285
<v-tooltip open-delay="500" activator="parent" location="bottom"> Play video </v-tooltip>
286-
<v-icon>mdi-play</v-icon>
286+
<v-icon size="22">mdi-play</v-icon>
287287
</v-btn>
288288
<v-btn
289289
icon
@@ -560,6 +560,18 @@
560560
</div>
561561
</div>
562562
<div class="flex gap-2 mt-4">
563+
<v-btn
564+
icon
565+
variant="outlined"
566+
size="small"
567+
:disabled="isProcessingChunks"
568+
@click="processChunkGroup(group)"
569+
>
570+
<v-tooltip open-delay="500" activator="parent" location="bottom">
571+
Process video chunks
572+
</v-tooltip>
573+
<v-icon>mdi-file-cog</v-icon>
574+
</v-btn>
563575
<v-btn
564576
icon
565577
variant="outlined"
@@ -681,7 +693,7 @@
681693
<v-icon class="mr-2">mdi-plus</v-icon>
682694
Process Another ZIP File
683695
</v-btn>
684-
<v-btn variant="outlined" size="small" @click="goToVideosTab">
696+
<v-btn variant="outlined" size="small" @click="currentVideoSubTab = 'processed'">
685697
<v-icon class="mr-2">mdi-video</v-icon>
686698
View Videos
687699
</v-btn>
@@ -878,7 +890,7 @@ import { computed, markRaw, nextTick, onBeforeUnmount, onMounted, reactive, ref,
878890
import { useInteractionDialog } from '@/composables/interactionDialog'
879891
import { useSnackbar } from '@/composables/snackbar'
880892
import { useVideoChunkManager } from '@/composables/videoChunkManager'
881-
import { formatBytes, isElectron } from '@/libs/utils'
893+
import { formatBytes, formatDate, isElectron } from '@/libs/utils'
882894
import { useAppInterfaceStore } from '@/stores/appInterface'
883895
import { useSnapshotStore } from '@/stores/snapshot'
884896
import { useVideoStore } from '@/stores/video'
@@ -905,11 +917,11 @@ const {
905917
zipProcessingComplete,
906918
zipProcessingProgress,
907919
zipProcessingMessage,
908-
formatDate,
909920
fetchChunkGroups,
910921
deleteChunkGroup,
911922
deleteAllChunks,
912923
downloadChunkGroup,
924+
processChunkGroup,
913925
openVideoChunksFolder,
914926
processVideoChunksZip,
915927
processAnotherZip,
@@ -1373,15 +1385,6 @@ const deselectAllPictures = (): void => {
13731385
isMultipleSelectionMode.value = false
13741386
}
13751387
1376-
/**
1377-
* Go to Videos tab to view the processed video
1378-
*/
1379-
const goToVideosTab = async (): Promise<void> => {
1380-
currentTab.value = 'videos'
1381-
currentVideoSubTab.value = 'processed'
1382-
await fetchVideosAndLogData()
1383-
}
1384-
13851388
/**
13861389
* Handle ZIP processing with callback to refresh videos
13871390
*/
@@ -1534,6 +1537,14 @@ watch(currentTab, async (newTab) => {
15341537
}
15351538
})
15361539
1540+
watch(currentVideoSubTab, async (newSubTab) => {
1541+
if (newSubTab === 'processed') {
1542+
await fetchVideosAndLogData()
1543+
} else if (newSubTab === 'raw') {
1544+
await fetchChunkGroups()
1545+
}
1546+
})
1547+
15371548
// Gestures library (hammer.js) for video selection
15381549
watch(
15391550
[availableVideos, currentVideoSubTab],

src/components/mini-widgets/MiniVideoRecorder.vue

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,63 @@ watch(nameSelectedStream, (newName) => {
183183
mediaStream.value = undefined
184184
})
185185
186-
// Fetch number of temporary videos on storage
186+
// Fetch number of videos on storage (chunk groups + processed videos without overlap)
187187
const fetchNumberOfTempVideos = async (): Promise<void> => {
188-
const keys = await videoStore.videoStorage.keys()
189-
const nProcessedVideos = keys.filter((k) => videoStore.isVideoFilename(k)).length
190-
numberOfVideosOnDB.value = nProcessedVideos
188+
// Get processed videos from videoStorage
189+
const processedKeys = await videoStore.videoStorage.keys()
190+
const processedVideos = processedKeys.filter((k) => videoStore.isVideoFilename(k))
191+
192+
// Get chunk groups from tempVideoStorage
193+
const tempKeys = await videoStore.tempVideoStorage.keys()
194+
const chunkGroups: Set<string> = new Set()
195+
196+
for (const key of tempKeys) {
197+
if (key.includes('thumbnail_')) continue
198+
199+
const parts = key.split('_')
200+
if (parts.length < 2) continue
201+
202+
const hash = parts[0]
203+
const chunkNumber = parseInt(parts[parts.length - 1], 10)
204+
if (isNaN(chunkNumber)) continue
205+
206+
// Check if this chunk actually exists and has content
207+
try {
208+
const blob = (await videoStore.tempVideoStorage.getItem(key)) as Blob
209+
if (blob && blob.size > 0) {
210+
chunkGroups.add(hash)
211+
}
212+
} catch (error) {
213+
console.warn(`Failed to check chunk ${key}:`, error)
214+
}
215+
}
216+
217+
// Count processed videos that don't have corresponding chunk groups
218+
const processedVideoHashes = new Set<string>()
219+
for (const videoKey of processedVideos) {
220+
// Extract hash from processed video filename
221+
// Processed videos have format like "MissionName (Date) #hash.webm"
222+
// We need to extract the hash after the # symbol
223+
const hashMatch = videoKey.match(/#([a-f0-9]+)\./)
224+
if (hashMatch && hashMatch[1]) {
225+
processedVideoHashes.add(hashMatch[1])
226+
}
227+
}
228+
229+
// Count unique videos: chunk groups + processed videos that don't have chunk groups
230+
const uniqueVideos = new Set<string>()
231+
232+
// Add all chunk groups
233+
chunkGroups.forEach((hash) => uniqueVideos.add(hash))
234+
235+
// Add processed videos that don't have corresponding chunk groups
236+
processedVideoHashes.forEach((hash) => {
237+
if (!chunkGroups.has(hash)) {
238+
uniqueVideos.add(hash)
239+
}
240+
})
241+
242+
numberOfVideosOnDB.value = uniqueVideos.size
191243
}
192244
193245
// eslint-disable-next-line jsdoc/require-jsdoc

src/composables/videoChunkManager.ts

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export const useVideoChunkManager = (): {
3434
zipProcessingComplete: any
3535
zipProcessingProgress: any
3636
zipProcessingMessage: any
37-
formatDate: (date: Date) => string
3837
fetchChunkGroups: () => Promise<void>
3938
deleteChunkGroup: (group: ChunkGroup) => Promise<void>
4039
deleteAllChunks: () => Promise<void>
4140
downloadChunkGroup: (group: ChunkGroup) => Promise<void>
41+
processChunkGroup: (group: ChunkGroup) => Promise<void>
4242
openVideoChunksFolder: () => Promise<void>
4343
processVideoChunksZip: (onComplete?: () => Promise<void>) => Promise<void>
4444
processAnotherZip: () => void
@@ -59,25 +59,6 @@ export const useVideoChunkManager = (): {
5959
const zipProcessingProgress = ref(0)
6060
const zipProcessingMessage = ref('')
6161

62-
/**
63-
* Format date for display
64-
* @param {Date} date
65-
* @returns {string} Formatted date string
66-
*/
67-
const formatDate = (date: Date): string => {
68-
if (date.getTime() === 0) {
69-
return 'Unknown creation datetime'
70-
}
71-
return new Intl.DateTimeFormat('en-US', {
72-
year: 'numeric',
73-
month: 'short',
74-
day: 'numeric',
75-
hour: '2-digit',
76-
minute: '2-digit',
77-
second: '2-digit',
78-
}).format(date)
79-
}
80-
8162
/**
8263
* Get timestamp for a chunk
8364
* @param {string} key
@@ -445,6 +426,93 @@ export const useVideoChunkManager = (): {
445426
}
446427
}
447428

429+
/**
430+
* Process a chunk group using the live video processor
431+
* @param {ChunkGroup} group
432+
* @returns {Promise<void>} Promise that resolves when processing is complete
433+
*/
434+
const processChunkGroup = async (group: ChunkGroup): Promise<void> => {
435+
if (!isElectron() || !window.electronAPI) {
436+
openSnackbar({ message: 'Manual processing is only available in Electron', variant: 'error' })
437+
return
438+
}
439+
440+
try {
441+
isProcessingChunks.value = true
442+
443+
// Generate filename based on the chunk group
444+
const timeString = group.firstChunkDate.toLocaleDateString('en-US', {
445+
month: 'short',
446+
day: '2-digit',
447+
year: 'numeric',
448+
hour: '2-digit',
449+
minute: '2-digit',
450+
second: '2-digit',
451+
})
452+
const fileName = group.fileName || `Cockpit (${timeString}) #${group.hash}`
453+
454+
// Get the chunk file paths from the group's chunks array, sorted by chunk number
455+
const sortedChunks = group.chunks.sort((a, b) => {
456+
const aNum = parseInt(a.key.split('_').pop() || '0', 10)
457+
const bNum = parseInt(b.key.split('_').pop() || '0', 10)
458+
return aNum - bNum
459+
})
460+
461+
if (sortedChunks.length === 0) {
462+
throw new Error('No chunk files found for processing')
463+
}
464+
465+
// Read first chunk using the electron storage API to get the blob
466+
const firstChunkBlob = (await videoStore.tempVideoStorage.getItem(sortedChunks[0].key)) as Blob
467+
if (!firstChunkBlob) {
468+
throw new Error('Failed to read first chunk')
469+
}
470+
471+
// Start FFmpeg streaming process with first chunk
472+
const { id: processId, outputPath } = await window.electronAPI.startVideoRecording(
473+
firstChunkBlob,
474+
group.hash,
475+
fileName,
476+
false // Don't keep chunk backup since we're processing existing chunks
477+
)
478+
479+
// Stream remaining chunks to FFmpeg in order
480+
for (let i = 1; i < sortedChunks.length; i++) {
481+
const chunkBlob = (await videoStore.tempVideoStorage.getItem(sortedChunks[i].key)) as Blob
482+
if (!chunkBlob) {
483+
console.warn(`Failed to read chunk ${i}, skipping`)
484+
continue
485+
}
486+
487+
await window.electronAPI.appendChunkToVideoRecording(processId, chunkBlob, i)
488+
}
489+
490+
// Finalize the streaming process
491+
await window.electronAPI.finalizeVideoRecording(processId)
492+
493+
// Find and copy telemetry file if it exists
494+
try {
495+
const videoKeys = await videoStore.videoStorage.keys()
496+
const assFile = videoKeys.find((key) => key.includes(group.hash) && key.endsWith('.ass'))
497+
if (assFile) {
498+
await window.electronAPI.copyTelemetryFile(assFile, outputPath)
499+
}
500+
} catch (error) {
501+
console.warn('Failed to copy telemetry file:', error)
502+
}
503+
504+
openSnackbar({
505+
message: `Video processed successfully! Output: ${outputPath}`,
506+
variant: 'success',
507+
})
508+
} catch (error) {
509+
const msg = `Failed to process video chunks: ${error instanceof Error ? error.message : 'Unknown error'}`
510+
openSnackbar({ message: msg, variant: 'error' })
511+
} finally {
512+
isProcessingChunks.value = false
513+
}
514+
}
515+
448516
/**
449517
* Reset ZIP processing state
450518
*/
@@ -466,11 +534,11 @@ export const useVideoChunkManager = (): {
466534
zipProcessingMessage,
467535

468536
// Methods
469-
formatDate,
470537
fetchChunkGroups,
471538
deleteChunkGroup,
472539
deleteAllChunks,
473540
downloadChunkGroup,
541+
processChunkGroup,
474542
openVideoChunksFolder,
475543
processVideoChunksZip,
476544
processAnotherZip,

src/electron/services/video-chunks-zip.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,16 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {
7979

8080
zipfile.outputStream.pipe(outputStream)
8181

82-
let addedFiles = 0
83-
const totalFiles = chunkFiles.length + (assFileName ? 1 : 0)
82+
let completedChunks = 0
83+
let assFileProcessed = false
84+
const totalChunks = chunkFiles.length
8485

85-
// Function to check if all files have been added
86+
// Function to check if all processing is complete
8687
const checkComplete = (): void => {
87-
addedFiles++
88-
if (addedFiles >= totalFiles) {
88+
const allChunksProcessed = completedChunks >= totalChunks
89+
const assFileDone = !assFileName || assFileProcessed
90+
91+
if (allChunksProcessed && assFileDone) {
8992
zipfile.end()
9093
}
9194
}
@@ -102,15 +105,17 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {
102105
mtime: stats.mtime,
103106
})
104107
console.debug(`Added chunk ${chunkFile} to ZIP`)
105-
checkComplete()
106108
})
107109
.catch((error) => {
108110
console.error(`Failed to add chunk ${chunkFile} to ZIP:`, error)
111+
})
112+
.finally(() => {
113+
completedChunks++
109114
checkComplete()
110115
})
111116
})
112117

113-
// Add .ass telemetry file if found
118+
// Add .ass telemetry file if found - ALWAYS process this regardless of chunk failures
114119
if (assFileName) {
115120
const assFilePath = join(cockpitFolderPath, 'videos', assFileName)
116121

@@ -121,12 +126,18 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {
121126
mtime: stats.mtime,
122127
})
123128
console.debug(`Added .ass file ${assFileName} to ZIP`)
124-
checkComplete()
125129
})
126130
.catch((error) => {
127131
console.warn(`Failed to add .ass file ${assFileName} to ZIP:`, error)
132+
})
133+
.finally(() => {
134+
assFileProcessed = true
128135
checkComplete()
129136
})
137+
} else {
138+
// No .ass file to process, mark as done
139+
assFileProcessed = true
140+
checkComplete()
130141
}
131142

132143
// Handle ZIP completion

src/libs/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,22 @@ export const frequencyHzToIntervalUs = (frequencyHz: number): number => {
281281
* @returns {number} Normalized angle in degrees
282282
*/
283283
export const norm360 = (deg: number): number => ((deg % 360) + 360) % 360
284+
285+
/**
286+
* Format date for display
287+
* @param {Date} date
288+
* @returns {string} Formatted date string
289+
*/
290+
export const formatDate = (date: Date): string => {
291+
if (date.getTime() === 0) {
292+
return 'Unknown creation datetime'
293+
}
294+
return new Intl.DateTimeFormat('en-US', {
295+
year: 'numeric',
296+
month: 'short',
297+
day: 'numeric',
298+
hour: '2-digit',
299+
minute: '2-digit',
300+
second: '2-digit',
301+
}).format(date)
302+
}

0 commit comments

Comments
 (0)