Skip to content

Commit

Permalink
refactor(screensharing): move live preview to a new component
Browse files Browse the repository at this point in the history
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
  • Loading branch information
ShGKme committed Jan 31, 2025
1 parent 343bea1 commit 307d9e1
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 90 deletions.
95 changes: 5 additions & 90 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@
-->

<script setup lang="ts">
import type { ScreensharingSource, ScreensharingSourceId } from './screensharing.types.ts'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { ScreensharingSource } from './screensharing.types.ts'
import IconMonitor from 'vue-material-design-icons/Monitor.vue'
import IconApplicationOutline from 'vue-material-design-icons/ApplicationOutline.vue'
import IconVolumeHigh from 'vue-material-design-icons/VolumeHigh.vue'
import DesktopMediaSourcePreviewLive from './DesktopMediaSourcePreviewLive.vue'

// On Wayland getting each stream for the live preview requests user to select the source via system dialog again
// Instead - show static images.
// See: https://github.com/electron/electron/issues/27732
const previewType = window.systemInfo.isWayland ? 'thumbnail' : 'live'

const videoElement = ref<HTMLVideoElement | null>(null)
let stream: MediaStream | null = null

const props = defineProps<{
defineProps<{
source: ScreensharingSource
selected: boolean
}>()
Expand All @@ -27,86 +24,6 @@ const emit = defineEmits<{
(event: 'select'): void
(event: 'suspend'): void
}>()

const getStreamForMediaSource = (mediaSourceId: ScreensharingSourceId) => {
const MAX_PREVIEW_SIZE = 320
// Special case for sharing all the screens with desktop audio in Electron
// In this case, it must have exactly these constraints
// "entire-desktop:0:0" is a custom sourceId for this specific case
const constraints = mediaSourceId === 'entire-desktop:0:0'
? {
audio: {
mandatory: {
chromeMediaSource: 'desktop',
},
},
video: {
mandatory: {
chromeMediaSource: 'desktop',
maxWidth: MAX_PREVIEW_SIZE,
maxHeight: MAX_PREVIEW_SIZE,
},
},
}
: {
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: mediaSourceId,
maxWidth: MAX_PREVIEW_SIZE,
maxHeight: MAX_PREVIEW_SIZE,
},
},
}

// @ts-expect-error Each browser has a different API, the current object is compatible with Chromium
return navigator.mediaDevices.getUserMedia(constraints)
}

/**
* Set the video source to the selected source
*/
async function setVideoSource() {
stream = await getStreamForMediaSource(props.source.id)
if (videoElement.value) {
videoElement.value.srcObject = stream
} else {
// If there is no video element - something went wrong or the component is destroyed already
// We still must release the stream
releaseVideoSource()
}
}

/**
* Release the video source
*/
function releaseVideoSource() {
if (!stream) {
return
}
for (const track of stream.getTracks()) {
track.stop()
}
}

onMounted(async () => {
if (previewType === 'live') {
await setVideoSource()
}
})

onBeforeUnmount(() => {
releaseVideoSource()
})

/**
* Handle the loadedmetadata event of the video element
* @param event - The event
*/
function onLoadedMetadata(event: Event) {
(event.target as HTMLVideoElement).play()
}
</script>

<template>
Expand All @@ -118,11 +35,9 @@ function onLoadedMetadata(event: Event) {
:checked="selected"
@change="emit('select')">

<video v-if="previewType === 'live'"
ref="videoElement"
<DesktopMediaSourcePreviewLive v-if="previewType === 'live'"
class="capture-source__preview"
muted
@loadedmetadata="onLoadedMetadata"
:media-source-id="source.id"
@suspend="emit('suspend')" />
<img v-else-if="previewType === 'thumbnail' && source.thumbnail"
alt=""
Expand Down
119 changes: 119 additions & 0 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreviewLive.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { ScreensharingSourceId } from './screensharing.types.ts'

const props = defineProps<{
mediaSourceId: ScreensharingSourceId
}>()

const emit = defineEmits<{
(event: 'suspend'): void
}>()

const videoElement = ref<HTMLVideoElement | null>(null)
let stream: MediaStream | null = null

/**
* Get the stream for the media source
* @param mediaSourceId - The media source ID
*/
function getStreamForMediaSource(mediaSourceId: ScreensharingSourceId) {
const MAX_PREVIEW_SIZE = 320
// Special case for sharing all the screens with desktop audio in Electron
// In this case, it must have exactly these constraints
// "entire-desktop:0:0" is a custom sourceId for this specific case
const constraints = mediaSourceId === 'entire-desktop:0:0'
? {
audio: {
mandatory: {
chromeMediaSource: 'desktop',
},
},
video: {
mandatory: {
chromeMediaSource: 'desktop',
maxWidth: MAX_PREVIEW_SIZE,
maxHeight: MAX_PREVIEW_SIZE,
},
},
}
: {
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: mediaSourceId,
maxWidth: MAX_PREVIEW_SIZE,
maxHeight: MAX_PREVIEW_SIZE,
},
},
}

// @ts-expect-error Each browser has a different API, the current object is compatible with Chromium
return navigator.mediaDevices.getUserMedia(constraints)
}

/**
* Set the video source to the selected source
*/
async function setVideoSource() {
stream = await getStreamForMediaSource(props.mediaSourceId)
if (videoElement.value) {
videoElement.value.srcObject = stream
} else {
// If there is no video element - something went wrong or the component is destroyed already
// We still must release the stream
releaseVideoSource()
}
}

/**
* Release the video source
*/
function releaseVideoSource() {
if (!stream) {
return
}
for (const track of stream.getTracks()) {
track.stop()
}
}

onMounted(async () => {
await setVideoSource()
})

onBeforeUnmount(() => {
releaseVideoSource()
})

/**
* Handle the loadedmetadata event of the video element
* @param event - The event
*/
function onLoadedMetadata(event: Event) {
(event.target as HTMLVideoElement).play()
}

/**
* Handle the suspend event of the video element
* This is supposed to happen only if the source disappears, e.g., the source window is closed
*/
function onSuspend() {
if (videoElement.value!.srcObject) {
emit('suspend')
}
}
</script>

<template>
<video ref="videoElement"
muted
@loadedmetadata="onLoadedMetadata"
@suspend="onSuspend" />
</template>

0 comments on commit 307d9e1

Please sign in to comment.