Skip to content

Commit

Permalink
perf(screensharing): make live preview optional
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 307d9e1 commit fd588af
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 52 deletions.
6 changes: 4 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => {
return null
}

// We cannot show live previews on Wayland, so we show thumbnails
const thumbnailWidth = isWayland ? 320 : 0
// On Wayland instead of the list of all available sources,
// the system picker is used to have a list of a single selected source
// So we show can show the preview in higher resolution
const thumbnailWidth = isWayland ? 1024 : 288

const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
Expand Down
73 changes: 55 additions & 18 deletions src/talk/renderer/screensharing/DesktopMediaSourceDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { computed, ref, watch } from 'vue'
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
import IconMonitorShare from '@mdi/svg/svg/monitor-share.svg?raw'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcDialogButton from '@nextcloud/vue/dist/Components/NcDialogButton.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import { t } from '@nextcloud/l10n'
import { useDocumentVisibility } from '@vueuse/core'
import DesktopMediaSourcePreview from './DesktopMediaSourcePreview.vue'
Expand All @@ -20,23 +22,14 @@ const emit = defineEmits<{
(event: 'cancel'): void
}>()

const livePreview = ref(false)
const selectedSourceId = ref<ScreensharingSourceId | null>(null)
const sources = ref<ScreensharingSource[] | null>(null)

const dialogButtons = computed(() => [
{
label: t('talk_desktop', 'Cancel'),
icon: IconCancel,
callback: handleCancel,
},
{
label: t('talk_desktop', 'Share screen'),
type: 'primary',
icon: IconMonitorShare,
disabled: !selectedSourceId.value,
callback: handleSubmit,
},
])
const screenSources = computed(() => sources.value?.filter((source) => source.id.startsWith('screen:') || source.id.startsWith('entire-desktop:')))
const windowSources = computed(() => sources.value?.filter((source) => source.id.startsWith('window:')))

const singleSource = computed(() => sources.value && sources.value.length === 1)

// On Wayland instead of the list of all available sources,
// the system picker is used to have a list of a single selected source.
Expand All @@ -46,6 +39,7 @@ const dialogButtons = computed(() => [
// - Sources list update is not possible
// - There is no the entire-desktop option
// See also: https://github.com/electron/electron/issues/27732
const livePreviewAvailable = !window.systemInfo.isWayland
if (!window.systemInfo.isWayland) {
const visibilityState = useDocumentVisibility()
watch(visibilityState, requestDesktopCapturerSources)
Expand Down Expand Up @@ -123,30 +117,73 @@ function handleCancel() {

<template>
<NcDialog :name="t('talk_desktop', 'Choose what to share')"
size="normal"
:buttons="dialogButtons"
size="large"
@update:open="handleCancel">
<div v-if="sources" class="capture-source-grid">
<DesktopMediaSourcePreview v-for="source in sources"
<h3 v-if="screenSources?.length && !singleSource" class="capture-source-section-heading">
{{ t('talk_desktop', 'Entire screens') }}
</h3>
<DesktopMediaSourcePreview v-for="source in screenSources"
:key="source.id"
:source="source"
:live="livePreview"
:selected="selectedSourceId === source.id"
@select="selectedSourceId = source.id"
@suspend="handleVideoSuspend(source)" />

<h3 v-if="!singleSource && windowSources?.length" class="capture-source-section-heading">
{{ t('talk_desktop', 'Application windows') }}
</h3>
<DesktopMediaSourcePreview v-for="source in windowSources"
:key="source.id"
:source="source"
:live="livePreview"
:selected="selectedSourceId === source.id"
@select="selectedSourceId = source.id"
@suspend="handleVideoSuspend(source)" />
</div>

<NcEmptyContent v-else :name="t('talk_desktop', 'Loading …')">
<template #icon>
<NcLoadingIcon />
</template>
</NcEmptyContent>

<template #actions>
<NcCheckboxRadioSwitch v-if="sources && livePreviewAvailable"
v-model="livePreview"
type="switch"
class="capture-mode-switch">
{{ t('talk_desktop', 'Live preview') }}
</NcCheckboxRadioSwitch>
<NcDialogButton :icon="IconCancel" :label="t('talk_desktop', 'Cancel')" @click="handleCancel" />
<NcDialogButton :icon="IconMonitorShare"
:label="t('talk_desktop', 'Share screen')"
type="primary"
:disabled="!selectedSourceId"
@click="handleSubmit" />
</template>
</NcDialog>
</template>

<style scoped lang="scss">
.capture-source-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
/* 280 is approximately 1/3 of the default large dialog size */
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-gap: calc(var(--default-grid-baseline) * 2);
width: 100%;
padding: calc(2 * var(--default-grid-baseline));
}

.capture-source-section-heading {
grid-column: 1 / -1;
font-size: 18px;
text-align: center;
margin-block: calc(2 * var(--default-grid-baseline)) 0;
}

.capture-mode-switch {
margin-inline-end: auto;
}
</style>
62 changes: 37 additions & 25 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,15 @@

<script setup lang="ts">
import type { ScreensharingSource } from './screensharing.types.ts'
import { t } from '@nextcloud/l10n'
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'

defineProps<{
source: ScreensharingSource
live: boolean
selected: boolean
}>()

Expand All @@ -35,15 +32,17 @@ const emit = defineEmits<{
:checked="selected"
@change="emit('select')">

<DesktopMediaSourcePreviewLive v-if="previewType === 'live'"
<DesktopMediaSourcePreviewLive v-if="live"
class="capture-source__preview"
:media-source-id="source.id"
@suspend="emit('suspend')" />
<img v-else-if="previewType === 'thumbnail' && source.thumbnail"
<img v-else-if="source.thumbnail"
alt=""
:src="source.thumbnail"
class="capture-source__preview">
<span v-else class="capture-source__preview" />
<span v-else class="capture-source__preview capture-source__preview-unavailable">
{{ t('talk_desktop', 'Preview is not available') }}
</span>

<span class="capture-source__caption">
<img v-if="source.icon"
Expand All @@ -60,6 +59,8 @@ const emit = defineEmits<{

<style scoped lang="scss">
.capture-source {
border-radius: var(--border-radius-element);
padding: calc(2 * var(--default-grid-baseline));
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
Expand All @@ -74,31 +75,42 @@ const emit = defineEmits<{
margin: 4px 14px;
}

&__preview {
aspect-ratio: 16 / 9;
object-fit: contain;
width: calc(100% - 4px - 4px);
flex: 1 0;
margin: 4px auto;
border-radius: var(--border-radius-large);
}
&__preview {
aspect-ratio: 16 / 9;
object-fit: contain;
width: 100%;
border-radius: var(--border-radius-small);
flex: 1 0;
}

&:focus &__preview,
&:hover &__preview {
box-shadow: 0 0 0 2px var(--color-primary-element);
background-color: var(--color-background-hover);
&__preview-unavailable {
background-color: var(--color-background-hover);
display: grid;
place-content: center;
color: var(--color-text-maxcontrast);
font-size: 120%;
}

&:focus,
&:hover {
background-color: var(--color-background-hover);
outline: 2px solid var(--color-primary-element);
outline-offset: 2px;
}

&:has(&__input:checked) &__preview {
box-shadow: 0 0 0 4px var(--color-primary-element);
background-color: var(--color-background-hover);
&:has(&__input:checked) {
background-color: var(--color-primary-element);
color: var(--color-primary-text);

.capture-source__caption-text {
font-weight: bolder;
}
}

&__caption {
display: flex;
gap: var(--default-grid-baseline);
gap: 1ch;
align-items: center;
padding: var(--default-grid-baseline);
}

&__caption-icon {
Expand Down
45 changes: 38 additions & 7 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreviewLive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
-->

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

const props = defineProps<{
mediaSourceId: ScreensharingSourceId
Expand All @@ -17,13 +18,14 @@ const emit = defineEmits<{

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

/**
* Get the stream for the media source
* @param mediaSourceId - The media source ID
*/
function getStreamForMediaSource(mediaSourceId: ScreensharingSourceId) {
const MAX_PREVIEW_SIZE = 320
const MAX_PREVIEW_SIZE = 1024
// 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
Expand Down Expand Up @@ -97,7 +99,8 @@ onBeforeUnmount(() => {
* @param event - The event
*/
function onLoadedMetadata(event: Event) {
(event.target as HTMLVideoElement).play()
isReady.value = true
;(event.target as HTMLVideoElement).play()
}

/**
Expand All @@ -112,8 +115,36 @@ function onSuspend() {
</script>

<template>
<video ref="videoElement"
muted
@loadedmetadata="onLoadedMetadata"
@suspend="onSuspend" />
<div class="live-preview">
<video v-show="isReady"
ref="videoElement"
class="live-preview__video"
muted
@loadedmetadata="onLoadedMetadata"
@suspend="onSuspend" />
<span v-if="!isReady" class="live-preview__placeholder">
<NcLoadingIcon :size="40" />
</span>
</div>
</template>

<style scoped>
.live-preview {
max-width: 100%;
}

.live-preview__video {
width: 100%;
height: 100%;
}

.live-preview__placeholder {
border-radius: var(--border-radius-small);
background-color: var(--color-background-hover);
color: var(--color-text-maxcontrast);
display: grid;
place-content: center;
width: 100%;
height: 100%;
}
</style>

0 comments on commit fd588af

Please sign in to comment.