Skip to content

Commit

Permalink
almost working
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Dec 22, 2023
1 parent 699f90b commit b15501d
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 80 deletions.
110 changes: 30 additions & 80 deletions src/components/mini-widgets/MiniVideoRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<button
class="flex items-center p-3 mx-2 font-medium transition-all rounded-md shadow-md w-fit text-uppercase hover:bg-slate-100"
:class="{ 'bg-slate-200 opacity-30 pointer-events-none': isLoadingStream }"
@click=";[startRecording(), (isStreamSelectDialogOpen = false)]"
@click="startRecording"
>
<span>Record</span>
<v-icon v-if="isLoadingStream" class="m-2 animate-spin">mdi-loading</v-icon>
Expand All @@ -54,21 +54,16 @@

<script setup lang="ts">
import { useMouseInElement, useTimestamp } from '@vueuse/core'
import { format, intervalToDuration } from 'date-fns'
import { saveAs } from 'file-saver'
import fixWebmDuration from 'fix-webm-duration'
import { intervalToDuration } from 'date-fns'
import { storeToRefs } from 'pinia'
import Swal, { type SweetAlertResult } from 'sweetalert2'
import { computed, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
import { datalogger } from '@/libs/sensors-logging'
import { isEqual } from '@/libs/utils'
import { useMissionStore } from '@/stores/mission'
import { useVideoStore } from '@/stores/video'
import type { MiniWidget } from '@/types/miniWidgets'
const videoStore = useVideoStore()
const { missionName } = useMissionStore()
const props = defineProps<{
/**
Expand All @@ -80,7 +75,6 @@ const miniWidget = toRefs(props).miniWidget
const nameSelectedStream = ref<string | undefined>()
const { namesAvailableStreams } = storeToRefs(videoStore)
const mediaRecorder = ref<MediaRecorder>()
const recorderWidget = ref()
const { isOutside } = useMouseInElement(recorderWidget)
const isStreamSelectDialogOpen = ref(false)
Expand All @@ -89,10 +83,6 @@ const timeRecordingStart = ref(new Date())
const timeNow = useTimestamp({ interval: 100 })
const mediaStream = ref<MediaStream | undefined>()
const isRecording = computed(() => {
return mediaRecorder.value !== undefined && mediaRecorder.value.state === 'recording'
})
onBeforeMount(async () => {
// Set initial widget options if they don't exist
if (Object.keys(miniWidget.value.options).length === 0) {
Expand All @@ -109,67 +99,18 @@ watch(nameSelectedStream, () => {
})
const toggleRecording = async (): Promise<void> => {
if (isRecording.value) {
stopRecording()
if (nameSelectedStream.value === undefined) {
Swal.fire({ text: 'No stream selected. Please choose one before continuing.', icon: 'error' })
return
}
if (videoStore.isRecording(nameSelectedStream.value)) {
videoStore.stopRecording(nameSelectedStream.value)
return
}
// Open dialog so user can choose the stream which will be recorded
isStreamSelectDialogOpen.value = true
}
const startRecording = async (): Promise<SweetAlertResult | void> => {
if (namesAvailableStreams.value.isEmpty()) {
return Swal.fire({ text: 'No streams available.', icon: 'error' })
}
if (nameSelectedStream.value === undefined) {
if (namesAvailableStreams.value.length === 1) {
await updateCurrentStream(namesAvailableStreams.value[0])
} else {
return Swal.fire({ text: 'No stream selected. Please choose one before continuing.', icon: 'error' })
}
}
if (mediaStream.value === undefined) {
return Swal.fire({ text: 'Media stream not defined.', icon: 'error' })
}
if (!mediaStream.value.active) {
return Swal.fire({ text: 'Media stream not yet active. Wait a second and try again.', icon: 'error' })
}
timeRecordingStart.value = new Date()
const fileName = `${missionName || 'Cockpit'} (${format(timeRecordingStart.value, 'LLL dd, yyyy - HH꞉mm꞉ss O')})`
mediaRecorder.value = new MediaRecorder(mediaStream.value)
if (!datalogger.logging()) {
datalogger.startLogging()
}
const videoTrack = mediaStream.value.getVideoTracks()[0]
const vWidth = videoTrack.getSettings().width || 1920
const vHeight = videoTrack.getSettings().height || 1080
mediaRecorder.value.start(1000)
let chunks: Blob[] = []
mediaRecorder.value.ondataavailable = async (e) => {
chunks.push(e.data)
await videoStore.videoRecoveryDB.setItem(fileName, chunks)
}
mediaRecorder.value.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' })
const videoTelemetryLog = datalogger.getSlice(datalogger.currentCockpitLog, timeRecordingStart.value, new Date())
const assLog = datalogger.toAssOverlay(videoTelemetryLog, vWidth, vHeight, timeRecordingStart.value.getTime())
var logBlob = new Blob([assLog], { type: 'text/plain' })
fixWebmDuration(blob, Date.now() - timeRecordingStart.value.getTime()).then((fixedBlob) => {
saveAs(fixedBlob, `${fileName}.webm`)
saveAs(logBlob, `${fileName}.ass`)
videoStore.videoRecoveryDB.removeItem(fileName)
})
chunks = []
mediaRecorder.value = undefined
}
}
const stopRecording = (): void => {
mediaRecorder.value?.stop()
}
const timePassedString = computed(() => {
const duration = intervalToDuration({ start: timeRecordingStart.value, end: timeNow.value })
const durationHours = duration.hours?.toFixed(0).length === 1 ? `0${duration.hours}` : duration.hours
Expand Down Expand Up @@ -214,21 +155,30 @@ const streamConnectionRoutine = setInterval(() => {
}, 1000)
onBeforeUnmount(() => clearInterval(streamConnectionRoutine))
// Try to prevent user from closing Cockpit when a stream is being recorded
watch(isRecording, () => {
if (!isRecording.value) {
window.onbeforeunload = null
const startRecording = (): void => {
if (nameSelectedStream.value === undefined) {
Swal.fire({ text: 'No stream selected.', icon: 'error' })
return
}
window.onbeforeunload = () => {
const alertMsg = `
You have a video recording ongoing.
Remember to stop it before closing Cockpit, or the record will be lost.
`
Swal.fire({ text: alertMsg, icon: 'warning' })
return 'I hope the user does not click on the leave button.'
}
})
videoStore.startRecording(nameSelectedStream.value)
isStreamSelectDialogOpen.value = false
}
// Try to prevent user from closing Cockpit when a stream is being recorded
// watch(videoStore.isRecording(nameSelectedStream.value), () => {
// if (!videoStore.isRecording(nameSelectedStream.value)) {
// window.onbeforeunload = null
// return
// }
// window.onbeforeunload = () => {
// const alertMsg = `
// You have a video recording ongoing.
// Remember to stop it before closing Cockpit, or the record will be lost.
// `
// Swal.fire({ text: alertMsg, icon: 'warning' })
// return 'I hope the user does not click on the leave button.'
// }
// })
</script>

<style scoped>
Expand Down
99 changes: 99 additions & 0 deletions src/stores/video.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
import fixWebmDuration from 'fix-webm-duration'
import localforage from 'localforage'
import { defineStore } from 'pinia'
import Swal from 'sweetalert2'
import { computed, ref, watch } from 'vue'
import adapter from 'webrtc-adapter'

import { WebRTCManager } from '@/composables/webRTC'
import { datalogger } from '@/libs/sensors-logging'
import { isEqual } from '@/libs/utils'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import { useMissionStore } from '@/stores/mission'
import type { StreamData } from '@/types/video'

export const useVideoStore = defineStore('video', () => {
const { missionName } = useMissionStore()
const { rtcConfiguration, webRTCSignallingURI } = useMainVehicleStore()
console.debug('[WebRTC] Using webrtc-adapter for', adapter.browserDetails)

Expand Down Expand Up @@ -62,10 +67,12 @@ export const useVideoStore = defineStore('video', () => {
const stream = ref()
const webRtcManager = new WebRTCManager(webRTCSignallingURI.val, rtcConfiguration)
const { mediaStream } = webRtcManager.startStream(stream, allowedIceIps)
const mediaRecorder: MediaRecorder = MediaRecorder
activeStreams.value[streamName] = {
stream: stream,
webRtcManager: webRtcManager,
mediaStream: mediaStream,
mediaRecorder: mediaRecorder,
}
}

Expand All @@ -82,6 +89,95 @@ export const useVideoStore = defineStore('video', () => {
return activeStreams.value[streamName].mediaStream
}

/**
* Wether or not the stream is currently being recorded
* @param {string} streamName - Name of the stream
* @returns {boolean}
*/
const isRecording = (streamName: string): boolean => {
if (activeStreams.value[streamName] === undefined) activateStream(streamName)

return (
activeStreams.value[streamName]!.mediaRecorder !== undefined &&
activeStreams.value[streamName]!.mediaRecorder.state === 'recording'
)
}

/**
* Stop recording the stream
* @param {string} streamName - Name of the stream
*/
const stopRecording = (streamName: string): void => {
if (activeStreams.value[streamName] === undefined) activateStream(streamName)

activeStreams.value[streamName]!.mediaRecorder.stop()
}

/**
* Start recording the stream
* @param {string} streamName - Name of the stream
*/
const startRecording = (streamName: string): void => {
if (activeStreams.value[streamName] === undefined) activateStream(streamName)

if (namesAvailableStreams.value.isEmpty()) {
Swal.fire({ text: 'No streams available.', icon: 'error' })
return
}

if (activeStreams.value[streamName]!.mediaStream === undefined) {
Swal.fire({ text: 'Media stream not defined.', icon: 'error' })
return
}
if (!activeStreams.value[streamName]!.mediaStream.active) {
Swal.fire({ text: 'Media stream not yet active. Wait a second and try again.', icon: 'error' })
return
}

activeStreams.value[streamName]!.timeRecordingStart = new Date()
const streamData = activeStreams.value[streamName] as StreamData
const fileName = `${missionName || 'Cockpit'} (${format(
streamData.timeRecordingStart,
'LLL dd, yyyy - HH꞉mm꞉ss O'
)})`
activeStreams.value[streamName]!.mediaRecorder = new MediaRecorder(streamData.mediaStream)
if (!datalogger.logging()) {
datalogger.startLogging()
}
const videoTrack = streamData.mediaStream.getVideoTracks()[0]
const vWidth = videoTrack.getSettings().width || 1920
const vHeight = videoTrack.getSettings().height || 1080
activeStreams.value[streamName]!.mediaRecorder.start(1000)
let chunks: Blob[] = []
activeStreams.value[streamName]!.mediaRecorder.ondataavailable = async (e) => {
chunks.push(e.data)
await videoRecoveryDB.setItem(fileName, chunks)
}

activeStreams.value[streamName]!.mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' })
const videoTelemetryLog = datalogger.getSlice(
datalogger.currentCockpitLog,
streamData.timeRecordingStart!,
new Date()
)
const assLog = datalogger.toAssOverlay(
videoTelemetryLog,
vWidth,
vHeight,
streamData.timeRecordingStart!.getTime()
)
const logBlob = new Blob([assLog], { type: 'text/plain' })
fixWebmDuration(blob, Date.now() - streamData.timeRecordingStart!.getTime()).then((fixedBlob) => {
saveAs(fixedBlob, `${fileName}.webm`)
saveAs(logBlob, `${fileName}.ass`)
videoRecoveryDB.removeItem(fileName)
})
chunks = []
activeStreams.value[streamName]!.mediaRecorder = undefined
}
}

// Offer download of backuped videos
const videoRecoveryDB = localforage.createInstance({
driver: localforage.INDEXEDDB,
Expand Down Expand Up @@ -155,5 +251,8 @@ export const useVideoStore = defineStore('video', () => {
namesAvailableStreams,
videoRecoveryDB,
getMediaStream,
isRecording,
stopRecording,
startRecording,
}
})
8 changes: 8 additions & 0 deletions src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ export interface StreamData {
* MediaStream object, if WebRTC stream is chosen
*/
mediaStream: Ref<MediaStream | undefined>
/**
* MediaRecorder object for that stream
*/
mediaRecorder: MediaRecorder | undefined
/**
*
*/
timeRecordingStart: Date | undefined
}

0 comments on commit b15501d

Please sign in to comment.