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

Video store rules all #654

Merged
6 changes: 6 additions & 0 deletions src/components/ConfigurationMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import ConfigurationDevelopmentView from '../views/ConfigurationDevelopmentView.
import ConfigurationGeneralView from '../views/ConfigurationGeneralView.vue'
import ConfigurationJoystickView from '../views/ConfigurationJoystickView.vue'
import ConfigurationLogsView from '../views/ConfigurationLogsView.vue'
import ConfigurationVideoView from '../views/ConfigurationVideoView.vue'

const store = useMainVehicleStore()

Expand All @@ -59,6 +60,11 @@ const menus = [
title: 'Joystick',
component: ConfigurationJoystickView,
},
{
icon: 'mdi-video',
title: 'Video',
component: ConfigurationVideoView,
},
{
icon: 'mdi-script',
title: 'Logs',
Expand Down
118 changes: 31 additions & 87 deletions src/components/mini-widgets/MiniVideoRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<p class="text-xl font-semibold">Choose a stream to record</p>
<div class="w-auto h-px my-2 bg-grey-lighten-3" />
<v-select
:model-value="selectedStream"
:model-value="nameSelectedStream"
label="Stream name"
:items="availableStreams"
:items="namesAvailableStreams"
item-title="name"
density="compact"
variant="outlined"
Expand Down Expand Up @@ -60,24 +60,16 @@ import fixWebmDuration from 'fix-webm-duration'
import { storeToRefs } from 'pinia'
import Swal, { type SweetAlertResult } from 'sweetalert2'
import { computed, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
import adapter from 'webrtc-adapter'

import { WebRTCManager } from '@/composables/webRTC'
import { datalogger } from '@/libs/sensors-logging'
import type { Stream } from '@/libs/webrtc/signalling_protocol'
import { useMainVehicleStore } from '@/stores/mainVehicle'
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 { allowedIceIps } = storeToRefs(videoStore)

const { rtcConfiguration, webRTCSignallingURI } = useMainVehicleStore()
const { missionName } = useMissionStore()

console.debug('[WebRTC] Using webrtc-adapter for', adapter.browserDetails)

const props = defineProps<{
/**
* Configuration of the widget
Expand All @@ -86,17 +78,16 @@ const props = defineProps<{
}>()
const miniWidget = toRefs(props).miniWidget

const selectedStream = ref<Stream | undefined>()
const webRTCManager = new WebRTCManager(webRTCSignallingURI.val, rtcConfiguration)
const { availableStreams: externalStreams, mediaStream } = webRTCManager.startStream(selectedStream, allowedIceIps)
const nameSelectedStream = ref<string | undefined>()
const { namesAvailableStreams } = storeToRefs(videoStore)
const mediaRecorder = ref<MediaRecorder>()
const recorderWidget = ref()
const { isOutside } = useMouseInElement(recorderWidget)
const availableStreams = ref<Stream[]>([])
const isStreamSelectDialogOpen = ref(false)
const isLoadingStream = ref(false)
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'
Expand All @@ -109,7 +100,12 @@ onBeforeMount(async () => {
streamName: undefined as string | undefined,
}
}
addScreenStream()
nameSelectedStream.value = miniWidget.value.options.streamName
})

watch(nameSelectedStream, () => {
miniWidget.value.options.streamName = nameSelectedStream.value
mediaStream.value = undefined
})

const toggleRecording = async (): Promise<void> => {
Expand All @@ -121,61 +117,17 @@ const toggleRecording = async (): Promise<void> => {
isStreamSelectDialogOpen.value = true
}

const addScreenStream = (): void => {
const screenStream = {
id: 'screenStream',
name: 'Entire screen',
encode: null,
height: null,
width: null,
interval: null,
source: null,
created: null,
}
availableStreams.value.push(screenStream)
}

onBeforeUnmount(() => {
webRTCManager.close('WebRTC manager removed')
})

const startRecording = async (): Promise<SweetAlertResult | void> => {
if (availableStreams.value.isEmpty()) {
if (namesAvailableStreams.value.isEmpty()) {
return Swal.fire({ text: 'No streams available.', icon: 'error' })
}
if (selectedStream.value === undefined) {
if (availableStreams.value.length === 1) {
await updateCurrentStream(availableStreams.value[0])
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 (selectedStream.value?.id === 'screenStream') {
try {
// @ts-ignore: camera permission check is currently available in most browsers, including chromium-based ones
const displayPermission = await navigator.permissions.query({ name: 'display-capture' })
if (displayPermission.state === 'denied') {
const noPermissionHtml = `
<p>Your browser is currently blocking screen recording.</p>
<p>We are working to solve this automatically for you.</p>
<p>By the meantime, please follow the instructions.</p>
<br />
<l>
<li>Copy Cockpit's URL (usually "http://blueos.local:49153").</li>
<li>Open the following URL: "chrome://flags/#unsafely-treat-insecure-origin-as-secure".</li>
<li>Add Cockpit's URL to the "Insecure origins treated as secure" list.</li>
<li>Select "Enabled" on the side menu.</li>
<li>Restart your browser.</li>
</l>
`
return Swal.fire({ html: noPermissionHtml, icon: 'error' })
}
// @ts-ignore: preferCurrentTab option is currently available in most browsers, including chromium-based ones
mediaStream.value = await navigator.mediaDevices.getDisplayMedia({ preferCurrentTab: true })
} catch (err) {
return Swal.fire({ text: 'Could not get stream from user screen.', icon: 'error' })
}
}
if (mediaStream.value === undefined) {
return Swal.fire({ text: 'Media stream not defined.', icon: 'error' })
}
Expand Down Expand Up @@ -211,10 +163,6 @@ const startRecording = async (): Promise<SweetAlertResult | void> => {
})
chunks = []
mediaRecorder.value = undefined
if (selectedStream.value?.id === 'screenStream' && mediaStream.value !== undefined) {
// If recording the screen stream, stop the tracks also, so the browser removes the recording warning.
mediaStream.value.getTracks().forEach((track: MediaStreamTrack) => track.stop())
}
}
}

Expand All @@ -230,10 +178,10 @@ const timePassedString = computed(() => {
return `${durationHours}:${durationMinutes}:${durationSeconds}`
})

const updateCurrentStream = async (stream: Stream | undefined): Promise<SweetAlertResult | void> => {
selectedStream.value = stream
const updateCurrentStream = async (streamName: string | undefined): Promise<SweetAlertResult | void> => {
nameSelectedStream.value = streamName
mediaStream.value = undefined
if (selectedStream.value !== undefined && selectedStream.value.id !== 'screenStream') {
if (nameSelectedStream.value !== undefined) {
isLoadingStream.value = true
let millisPassed = 0
const timeStep = 100
Expand All @@ -248,27 +196,23 @@ const updateCurrentStream = async (stream: Stream | undefined): Promise<SweetAle
return Swal.fire({ text: 'Could not load media stream.', icon: 'error' })
}
}
miniWidget.value.options.streamName = selectedStream.value?.name
miniWidget.value.options.streamName = nameSelectedStream.value
}

watch(externalStreams, () => {
const savedStreamName: string | undefined = miniWidget.value.options.streamName as string
availableStreams.value = externalStreams.value
if (!availableStreams.value.find((stream) => stream.id === 'screenStream')) {
addScreenStream()
}
if (availableStreams.value.isEmpty()) {
return
const streamConnectionRoutine = setInterval(() => {
// If the video player widget is cold booted, assign the first stream to it
if (miniWidget.value.options.streamName === undefined && !namesAvailableStreams.value.isEmpty()) {
miniWidget.value.options.streamName = namesAvailableStreams.value[0]
nameSelectedStream.value = miniWidget.value.options.streamName
}

// Retrieve stream from the saved stream name, otherwise choose the first available stream as a fallback
const savedStream = savedStreamName ? availableStreams.value.find((s) => s.name === savedStreamName) : undefined

if (savedStream !== undefined && savedStream.id !== selectedStream.value?.id && selectedStream.value === undefined) {
console.debug!('[WebRTC] trying to set stream...')
updateCurrentStream(savedStream)
const updatedMediaStream = videoStore.getMediaStream(miniWidget.value.options.streamName)
// If the widget is not connected to the MediaStream, try to connect it
if (!isEqual(updatedMediaStream, mediaStream.value)) {
mediaStream.value = updatedMediaStream
}
})
}, 1000)
onBeforeUnmount(() => clearInterval(streamConnectionRoutine))

// Try to prevent user from closing Cockpit when a stream is being recorded
watch(isRecording, () => {
Expand Down
Loading