Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix slowness problem on the Electron version #1636

Merged
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@
</v-app>
<About v-if="showAboutDialog" @update:show-about-dialog="showAboutDialog = $event" />
<Tutorial :show-tutorial="interfaceStore.isTutorialVisible" />
<VideoLibraryModal :open-modal="interfaceStore.isVideoLibraryVisible" />
<VideoLibraryModal v-if="interfaceStore.isVideoLibraryVisible" />
<VehicleDiscoveryDialog v-model="showDiscoveryDialog" show-auto-search-option />
<UpdateNotification v-if="isElectron()" />
</template>
Expand Down
183 changes: 92 additions & 91 deletions src/components/VideoLibraryModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,27 @@
<div class="flex flex-col w-full h-full px-4 overflow-auto align-center">
<div v-for="video in availableVideos" :key="video.fileName" class="mb-4 video-container">
<div class="relative video-wrapper">
<video
:id="`video-library-${video.fileName}`"
class="border-4 border-white rounded-md cursor-pointer border-opacity-[0.1] hover:border-opacity-[0.4] transition duration-75 hover:ease-in"
<div
:id="`video-library-thumbnail-${video.fileName}`"
class="border-4 border-white rounded-md cursor-pointer border-opacity-[0.1] hover:border-opacity-[0.4] transition duration-75 hover:ease-in aspect-video"
:class="
selectedVideos.find((v) => v.fileName === video.fileName)
? ['border-opacity-[0.4]', 'w-[220px]']
: ['border-opacity-[0.1]', 'w-[190px]']
"
preload="auto"
:poster="!video.isProcessed ? video.thumbnail : undefined"
>
<source :src="video.url" />
</video>
<img
v-if="video.isProcessed && videoThumbnailURLs[video.fileName]"
:src="videoThumbnailURLs[video.fileName]"
/>
<img
v-else-if="!video.isProcessed && videoThumbnailURLs[video.hash]"
:src="videoThumbnailURLs[video.hash]"
/>
<div v-else class="w-full h-full flex justify-center items-center bg-black">
<v-icon size="60" class="text-white/30">mdi-video</v-icon>
</div>
</div>
<div
v-if="selectedVideos.find((v) => v.fileName === video.fileName) && !isMultipleSelectionMode"
class="play-button"
Expand Down Expand Up @@ -201,20 +209,28 @@
<!-- Video Player -->
<div v-if="availableVideos.length > 0" class="flex flex-col justify-between mt-5 align-center w-[720px]">
<div>
<div v-if="loadingVideoBlob" class="text-center w-full aspect-video flex justify-center items-center">
Loading video...
</div>
<video
v-if="
!isMultipleSelectionMode && selectedVideos.length === 1 && !isMultipleSelectionMode && !loadingData
v-show="
!isMultipleSelectionMode &&
selectedVideos.length === 1 &&
!loadingData &&
!loadingVideoBlob &&
selectedVideos[0].isProcessed
"
id="video-player"
ref="videoPlayerRef"
width="660px"
:controls="selectedVideos[0].isProcessed ? true : false"
:preload="selectedVideos[0].isProcessed ? 'auto' : 'none'"
:poster="selectedVideos[0]?.thumbnail || undefined"
class="border-[14px] border-white border-opacity-10 rounded-lg min-h-[382px] aspect-video"
>
<source :src="selectedVideos[0]?.url || undefined" />
</video>
></video>
<div
v-if="!isMultipleSelectionMode && selectedVideos.length === 1 && !selectedVideos[0].isProcessed"
class="w-[660px] border-[14px] border-white border-opacity-10 rounded-lg min-h-[382px] aspect-video"
/>
<v-btn
v-if="
!loadingData &&
Expand Down Expand Up @@ -314,7 +330,7 @@
</div>
<div class="flex flex-row mt-2">
<button
v-for="button in fileActionButtons"
v-for="button in fileActionButtons.filter((b) => b.show)"
:key="button.name"
class="flex flex-col justify-center ml-6 align-center"
:disabled="button.disabled"
Expand Down Expand Up @@ -480,7 +496,7 @@
import { useWindowSize } from '@vueuse/core'
import * as Hammer from 'hammerjs'
import { computed, nextTick, onBeforeUnmount, onMounted } from 'vue'
import { ref, watch } from 'vue'
import { reactive, ref, watch } from 'vue'

import { useInteractionDialog } from '@/composables/interactionDialog'
import { useSnackbar } from '@/composables/snackbar'
Expand All @@ -496,17 +512,9 @@ const videoStore = useVideoStore()
const interfaceStore = useAppInterfaceStore()
const { showSnackbar } = useSnackbar()

const props = defineProps({
openModal: Boolean,
})
const emits = defineEmits(['update:openModal'])

const { showDialog, closeDialog } = useInteractionDialog()
const { width: windowWidth } = useWindowSize()

// Track the blob URLs to revoke them when the modal is closed
const blobURLs = ref<string[]>([])

/* eslint-disable jsdoc/require-jsdoc */
interface CustomHammerInstance {
destroy(): void
Expand All @@ -519,7 +527,7 @@ interface HammerInstances {
/* eslint-enable jsdoc/require-jsdoc */
const availableVideos = ref<VideoLibraryFile[]>([])
const availableLogFiles = ref<VideoLibraryLogFile[]>([])
const isVisible = ref(props.openModal)
const isVisible = ref(true)
const selectedVideos = ref<VideoLibraryFile[]>([])
const videoPlayerRef = ref<HTMLVideoElement | null>(null)
const currentTab = ref('videos')
Expand All @@ -546,6 +554,9 @@ const showOnScreenProgress = ref(false)
const lastSelectedVideo = ref<VideoLibraryFile | null>(null)
const errorProcessingVideos = ref(false)
const deleteButtonLoading = ref(false)
const videoBlobURL = ref<string | null>(null)
const loadingVideoBlob = ref(false)
const videoThumbnailURLs = reactive<Record<string, string | null>>({})

const dialogStyle = computed(() => {
const scale = interfaceStore.isOnSmallScreen ? windowWidth.value / 1100 : 1
Expand Down Expand Up @@ -577,7 +588,7 @@ const fileActionButtons = computed(() => [
size: 28,
tooltip: 'Download selected videos with logs',
confirmAction: false,
show: true,
show: !isElectron(),
disabled: showOnScreenProgress.value === true || isPreparingDownload.value === true,
action: () => downloadVideoAndTelemetryFiles(),
},
Expand Down Expand Up @@ -608,10 +619,7 @@ const openVideoFolder = (): void => {

const closeModal = (): void => {
isVisible.value = false
emits('update:openModal', false)
currentTab.value = 'videos'
blobURLs.value.forEach((url) => URL.revokeObjectURL(url))
blobURLs.value = []
deselectAllVideos()
lastSelectedVideo.value = null
isMultipleSelectionMode.value = false
Expand All @@ -635,9 +643,8 @@ const parseMissionAndDateFromTitle = (title: string): string => {
const playVideo = (): void => {
if (selectedVideos.value.length === 1 && !isMultipleSelectionMode.value) {
const videoPlayer = document.getElementById(`video-player`) as HTMLVideoElement
if (videoPlayer) {
videoPlayer.play().catch((e: Error) => console.error('Error auto-playing video:', e))
}
if (!videoPlayer) return
videoPlayer.play().catch((e: Error) => console.error('Error auto-playing video:', e))
}
}

Expand Down Expand Up @@ -699,11 +706,12 @@ const processSingleVideo = async (): Promise<void> => {
if (selectedVideos.value.length === 1 && !selectedVideos.value[0].isProcessed) {
showOnScreenProgress.value = true
}
processVideos()
await processVideos()
await loadVideoBlobIntoPlayer(selectedVideos.value[0].fileName)
}

// Process multiple videos with progress bars dialog
const processMultipleVideosDialog = (): void => {
const processMultipleVideosDialog = async (): Promise<void> => {
numberOfFilesToProcess.value = selectedVideos.value.length
progressInteractionDialogTitle.value = 'Processing Videos'
progressInteractionDialogActions.value = [
Expand All @@ -716,7 +724,8 @@ const processMultipleVideosDialog = (): void => {
]
showProcessingInteractionDialog.value = false
showProgressInteractionDialog.value = true
processVideos()
await processVideos()
await loadVideoBlobIntoPlayer(selectedVideos.value[0].fileName)
}

const showProcessVideosWarningDialog = (): void => {
Expand All @@ -736,7 +745,7 @@ const showProcessVideosWarningDialog = (): void => {
class: 'font-bold',
action: async () => {
showProcessingInteractionDialog.value = false
processMultipleVideosDialog()
await processMultipleVideosDialog()
},
},
]
Expand Down Expand Up @@ -879,7 +888,10 @@ const discardVideosAndUpdateDB = async (): Promise<void> => {
let unprocessedVideosToDiscard: string[] = []

await selectedVideos.value.forEach((video: VideoLibraryFile) => {
if (video.isProcessed) processedVideosToDiscard.push(video.fileName)
if (video.isProcessed) {
processedVideosToDiscard.push(video.fileName)
processedVideosToDiscard.push(videoStore.videoThumbnailFilename(video.fileName))
}
if (!video.isProcessed && video.hash) unprocessedVideosToDiscard.push(video.hash)
})

Expand Down Expand Up @@ -916,44 +928,21 @@ const fetchVideosAndLogData = async (): Promise<void> => {
const keys = await videoStore.videoStorage.keys()
for (const key of keys) {
if (videoStore.isVideoFilename(key)) {
videoFilesOperations.push(
(async () => {
const videoBlob = await videoStore.videoStorage.getItem(key)
let url = ''
let isProcessed = true
if (videoBlob instanceof Blob) {
url = URL.createObjectURL(videoBlob)
blobURLs.value.push(url)
} else {
console.error('Video data is not a Blob:', videoBlob)
}
const size = (await videoStore.videoStorageFileSize(key)) ?? 0
return { fileName: key, size, url, isProcessed }
})()
)
videoFilesOperations.push({ fileName: key, isProcessed: true, thumbnail: videoStore.videoStorage.getItem(key) })
const thumbnail = await videoStore.getVideoThumbnail(key, true)
videoThumbnailURLs[key] = thumbnail ? URL.createObjectURL(thumbnail) : null
}
if (key.endsWith('.ass')) {
logFileOperations.push(
(async () => {
const videoBlob = await videoStore.videoStorage.getItem(key)
let url = ''
if (videoBlob instanceof Blob) {
url = URL.createObjectURL(videoBlob)
blobURLs.value.push(url)
} else {
console.error('Video data is not a Blob:', videoBlob)
}
const size = (await videoStore.videoStorageFileSize(key)) ?? 0
return { fileName: key, url, size }
})()
)
logFileOperations.push({ fileName: key })
}
}

// Fetch unprocessed videos
const unprocessedVideos = await videoStore.unprocessedVideos
const unprocessedVideoOperations = Object.entries(unprocessedVideos).map(async ([hash, videoInfo]) => {
return { ...videoInfo, ...{ hash: hash, url: '', isProcessed: false } }
const thumbnail = await videoStore.getVideoThumbnail(hash, false)
videoThumbnailURLs[hash] = thumbnail ? URL.createObjectURL(thumbnail) : null
return { ...videoInfo, ...{ hash: hash, isProcessed: false } }
})

const videos = await Promise.all(videoFilesOperations)
Expand Down Expand Up @@ -998,7 +987,6 @@ watch(
)

watch(isVisible, (newValue) => {
emits('update:openModal', newValue)
if (!newValue) {
resetProgressBars()
isMultipleSelectionMode.value = false
Expand All @@ -1008,34 +996,46 @@ watch(isVisible, (newValue) => {
}
})

watch(
() => props.openModal,
async (newVal) => {
isVisible.value = newVal
if (newVal === true) {
await fetchVideosAndLogData()
showOnScreenProgress.value = false
const loadVideoBlobIntoPlayer = async (videoFileName: string): Promise<void> => {
loadingVideoBlob.value = true

try {
const videoPlayer = document.getElementById(`video-player`) as HTMLVideoElement
const videoBlob = await videoStore.videoStorage.getItem(videoFileName)

if (videoBlob instanceof Blob && videoPlayer) {
videoBlobURL.value = URL.createObjectURL(videoBlob)
videoPlayer.src = videoBlobURL.value
videoPlayer.load()
}
} catch (error) {
const msg = 'Error loading video blob into player'
showSnackbar({ message: msg, duration: 3000, variant: 'error', closeButton: true })
} finally {
loadingVideoBlob.value = false
}
)
}

const unloadVideoBlob = (): void => {
if (!videoBlobURL.value) return
URL.revokeObjectURL(videoBlobURL.value)
videoBlobURL.value = null
}

watch(
selectedVideos,
(newVal) => {
async (newVal) => {
if (newVal.length === 1) {
lastSelectedVideo.value = newVal[0]
await loadVideoBlobIntoPlayer(newVal[0].fileName)
if (errorProcessingVideos.value) {
resetProgressBars()
}
lastSelectedVideo.value = newVal[0]
const videoSrc = newVal[0].url
const videoPlayer = videoPlayerRef.value
if (videoPlayer) {
videoPlayer.src = videoSrc
videoPlayer.load()
}
} else {
unloadVideoBlob()
}
},
{ immediate: true, deep: true }
{ deep: true }
)

// Keep last processed video selected after refresh
Expand Down Expand Up @@ -1067,11 +1067,11 @@ watch(
async () => {
await nextTick()
availableVideos.value.forEach((video) => {
const videoElement = document.getElementById(`video-library-${video.fileName}`)
if (videoElement) {
const videoThumbnailElement = document.getElementById(`video-library-thumbnail-${video.fileName}`)
if (videoThumbnailElement) {
hammerInstances.value[video.fileName]?.destroy()

const hammerManager = new Hammer.Manager(videoElement)
const hammerManager = new Hammer.Manager(videoThumbnailElement)
hammerManager.add(new Hammer.Tap())
hammerManager.add(new Hammer.Press({ time: 500 }))

Expand Down Expand Up @@ -1118,20 +1118,21 @@ watch(
)

onMounted(async () => {
loadingData.value = true
await fetchVideosAndLogData()
if (availableVideos.value.length > 0) {
await loadVideoBlobIntoPlayer(availableVideos.value[0].fileName)
}
showOnScreenProgress.value = false
})

onBeforeUnmount(() => {
currentTab.value = 'videos'
// Revoke each blob URL
blobURLs.value.forEach((url) => URL.revokeObjectURL(url))
blobURLs.value = []
// Properly destroy Hammer instances
Object.values(hammerInstances.value).forEach((instance) => {
instance.destroy()
})
interfaceStore.videoLibraryVisibility = false
unloadVideoBlob()
})
</script>

Expand Down
2 changes: 1 addition & 1 deletion electron/main.ts → src/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function createWindow(): void {
y: store.get('windowBounds')?.y ?? screen.getPrimaryDisplay().bounds.y,
})

mainWindow.on('close', () => {
mainWindow.on('move', () => {
const windowBounds = mainWindow!.getBounds()
const { x, y, width, height } = windowBounds
store.set('windowBounds', { x, y, width, height })
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ipcMain } from 'electron'
import { networkInterfaces } from 'os'

import { NetworkInfo } from '../../src/types/network'
import { NetworkInfo } from '../../types/network'
/**
* Get the network information
* @returns {NetworkInfo} The network information
Expand Down
File renamed without changes.
Loading