Skip to content

Commit 8281c85

Browse files
committed
perf(screensharing): make live preview optional
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
1 parent 6d61e40 commit 8281c85

File tree

4 files changed

+131
-52
lines changed

4 files changed

+131
-52
lines changed

src/main.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => {
9494
return null
9595
}
9696

97-
// We cannot show live previews on Wayland, so we show thumbnails
98-
const thumbnailWidth = isWayland ? 320 : 0
97+
const thumbnailWidth = 800
9998

10099
const sources = await desktopCapturer.getSources({
101100
types: ['screen', 'window'],

src/talk/renderer/screensharing/DesktopMediaSourceDialog.vue

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { computed, ref, watch } from 'vue'
99
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
1010
import IconMonitorShare from '@mdi/svg/svg/monitor-share.svg?raw'
1111
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
12+
import NcDialogButton from '@nextcloud/vue/dist/Components/NcDialogButton.js'
1213
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
1314
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
15+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
1416
import { t } from '@nextcloud/l10n'
1517
import { useWindowFocus } from '@vueuse/core'
1618
import DesktopMediaSourcePreview from './DesktopMediaSourcePreview.vue'
@@ -20,23 +22,14 @@ const emit = defineEmits<{
2022
(event: 'cancel'): void
2123
}>()
2224
25+
const livePreview = ref(false)
2326
const selectedSourceId = ref<ScreensharingSourceId | null>(null)
2427
const sources = ref<ScreensharingSource[] | null>(null)
2528
26-
const dialogButtons = computed(() => [
27-
{
28-
label: t('talk_desktop', 'Cancel'),
29-
icon: IconCancel,
30-
callback: handleCancel,
31-
},
32-
{
33-
label: t('talk_desktop', 'Share screen'),
34-
type: 'primary',
35-
icon: IconMonitorShare,
36-
disabled: !selectedSourceId.value,
37-
callback: handleSubmit,
38-
},
39-
])
29+
const screenSources = computed(() => sources.value?.filter((source) => source.id.startsWith('screen:') || source.id.startsWith('entire-desktop:')))
30+
const windowSources = computed(() => sources.value?.filter((source) => source.id.startsWith('window:')))
31+
32+
const singleSource = computed(() => sources.value && sources.value.length === 1)
4033
4134
// On Wayland instead of the list of all available sources,
4235
// the system picker is used to have a list of a single selected source.
@@ -46,6 +39,7 @@ const dialogButtons = computed(() => [
4639
// - Sources list update is not possible
4740
// - There is no the entire-desktop option
4841
// See also: https://github.com/electron/electron/issues/27732
42+
const livePreviewAvailable = !window.systemInfo.isWayland
4943
if (!window.systemInfo.isWayland) {
5044
const isWindowFocused = useWindowFocus()
5145
watch(isWindowFocused, requestDesktopCapturerSources)
@@ -125,30 +119,73 @@ function handleCancel() {
125119

126120
<template>
127121
<NcDialog :name="t('talk_desktop', 'Choose what to share')"
128-
size="normal"
129-
:buttons="dialogButtons"
122+
size="large"
130123
@update:open="handleCancel">
131124
<div v-if="sources" class="capture-source-grid">
132-
<DesktopMediaSourcePreview v-for="source in sources"
125+
<h3 v-if="screenSources?.length && !singleSource" class="capture-source-section-heading">
126+
{{ t('talk_desktop', 'Entire screens') }}
127+
</h3>
128+
<DesktopMediaSourcePreview v-for="source in screenSources"
129+
:key="source.id"
130+
:source="source"
131+
:live="livePreview"
132+
:selected="selectedSourceId === source.id"
133+
@select="selectedSourceId = source.id"
134+
@suspend="handleVideoSuspend(source)" />
135+
136+
<h3 v-if="!singleSource && windowSources?.length" class="capture-source-section-heading">
137+
{{ t('talk_desktop', 'Application windows') }}
138+
</h3>
139+
<DesktopMediaSourcePreview v-for="source in windowSources"
133140
:key="source.id"
134141
:source="source"
142+
:live="livePreview"
135143
:selected="selectedSourceId === source.id"
136144
@select="selectedSourceId = source.id"
137145
@suspend="handleVideoSuspend(source)" />
138146
</div>
147+
139148
<NcEmptyContent v-else :name="t('talk_desktop', 'Loading …')">
140149
<template #icon>
141150
<NcLoadingIcon />
142151
</template>
143152
</NcEmptyContent>
153+
154+
<template #actions>
155+
<NcCheckboxRadioSwitch v-if="sources && livePreviewAvailable"
156+
v-model="livePreview"
157+
type="switch"
158+
class="capture-mode-switch">
159+
{{ t('talk_desktop', 'Live preview') }}
160+
</NcCheckboxRadioSwitch>
161+
<NcDialogButton :icon="IconCancel" :label="t('talk_desktop', 'Cancel')" @click="handleCancel" />
162+
<NcDialogButton :icon="IconMonitorShare"
163+
:label="t('talk_desktop', 'Share screen')"
164+
type="primary"
165+
:disabled="!selectedSourceId"
166+
@click="handleSubmit" />
167+
</template>
144168
</NcDialog>
145169
</template>
146170

147171
<style scoped lang="scss">
148172
.capture-source-grid {
149173
display: grid;
150-
grid-template-columns: repeat(3, minmax(0, 1fr));
174+
/* 280 is approximately 1/3 of the default large dialog size */
175+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
151176
grid-gap: calc(var(--default-grid-baseline) * 2);
152177
width: 100%;
178+
padding: calc(2 * var(--default-grid-baseline));
179+
}
180+
181+
.capture-source-section-heading {
182+
grid-column: 1 / -1;
183+
font-size: 18px;
184+
text-align: center;
185+
margin-block: calc(2 * var(--default-grid-baseline)) 0;
186+
}
187+
188+
.capture-mode-switch {
189+
margin-inline-end: auto;
153190
}
154191
</style>

src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@
55

66
<script setup lang="ts">
77
import type { ScreensharingSource } from './screensharing.types.ts'
8+
import { t } from '@nextcloud/l10n'
89
import IconMonitor from 'vue-material-design-icons/Monitor.vue'
910
import IconApplicationOutline from 'vue-material-design-icons/ApplicationOutline.vue'
1011
import IconVolumeHigh from 'vue-material-design-icons/VolumeHigh.vue'
1112
import DesktopMediaSourcePreviewLive from './DesktopMediaSourcePreviewLive.vue'
1213
13-
// On Wayland getting each stream for the live preview requests user to select the source via system dialog again
14-
// Instead - show static images.
15-
// See: https://github.com/electron/electron/issues/27732
16-
const previewType = window.systemInfo.isWayland ? 'thumbnail' : 'live'
17-
1814
defineProps<{
1915
source: ScreensharingSource
16+
live: boolean
2017
selected: boolean
2118
}>()
2219
@@ -35,15 +32,17 @@ const emit = defineEmits<{
3532
:checked="selected"
3633
@change="emit('select')">
3734

38-
<DesktopMediaSourcePreviewLive v-if="previewType === 'live'"
35+
<DesktopMediaSourcePreviewLive v-if="live"
3936
class="capture-source__preview"
4037
:media-source-id="source.id"
4138
@suspend="emit('suspend')" />
42-
<img v-else-if="previewType === 'thumbnail' && source.thumbnail"
39+
<img v-else-if="source.thumbnail"
4340
alt=""
4441
:src="source.thumbnail"
4542
class="capture-source__preview">
46-
<span v-else class="capture-source__preview" />
43+
<span v-else class="capture-source__preview capture-source__preview-unavailable">
44+
{{ t('talk_desktop', 'Preview is not available') }}
45+
</span>
4746

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

6160
<style scoped lang="scss">
6261
.capture-source {
62+
border-radius: var(--border-radius-element);
63+
padding: calc(2 * var(--default-grid-baseline));
6364
display: flex;
6465
flex-direction: column;
6566
gap: var(--default-grid-baseline);
@@ -74,31 +75,42 @@ const emit = defineEmits<{
7475
margin: 4px 14px;
7576
}
7677
77-
&__preview {
78-
aspect-ratio: 16 / 9;
79-
object-fit: contain;
80-
width: calc(100% - 4px - 4px);
81-
flex: 1 0;
82-
margin: 4px auto;
83-
border-radius: var(--border-radius-large);
84-
}
78+
&__preview {
79+
aspect-ratio: 16 / 9;
80+
object-fit: contain;
81+
width: 100%;
82+
border-radius: var(--border-radius-small);
83+
flex: 1 0;
84+
}
8585
86-
&:focus &__preview,
87-
&:hover &__preview {
88-
box-shadow: 0 0 0 2px var(--color-primary-element);
89-
background-color: var(--color-background-hover);
86+
&__preview-unavailable {
87+
background-color: var(--color-background-hover);
88+
display: grid;
89+
place-content: center;
90+
color: var(--color-text-maxcontrast);
91+
font-size: 120%;
92+
}
93+
94+
&:focus,
95+
&:hover {
96+
background-color: var(--color-background-hover);
97+
outline: 2px solid var(--color-primary-element);
98+
outline-offset: 2px;
9099
}
91100
92-
&:has(&__input:checked) &__preview {
93-
box-shadow: 0 0 0 4px var(--color-primary-element);
94-
background-color: var(--color-background-hover);
101+
&:has(&__input:checked) {
102+
background-color: var(--color-primary-element);
103+
color: var(--color-primary-text);
104+
105+
.capture-source__caption-text {
106+
font-weight: bolder;
107+
}
95108
}
96109
97110
&__caption {
98111
display: flex;
99-
gap: var(--default-grid-baseline);
112+
gap: 1ch;
100113
align-items: center;
101-
padding: var(--default-grid-baseline);
102114
}
103115
104116
&__caption-icon {

src/talk/renderer/screensharing/DesktopMediaSourcePreviewLive.vue

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
-->
55

66
<script setup lang="ts">
7-
import { onBeforeUnmount, onMounted, ref } from 'vue'
87
import type { ScreensharingSourceId } from './screensharing.types.ts'
8+
import { onBeforeUnmount, onMounted, ref } from 'vue'
9+
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
910
1011
const props = defineProps<{
1112
mediaSourceId: ScreensharingSourceId
@@ -17,13 +18,14 @@ const emit = defineEmits<{
1718
1819
const videoElement = ref<HTMLVideoElement | null>(null)
1920
let stream: MediaStream | null = null
21+
const isReady = ref(false)
2022
2123
/**
2224
* Get the stream for the media source
2325
* @param mediaSourceId - The media source ID
2426
*/
2527
function getStreamForMediaSource(mediaSourceId: ScreensharingSourceId) {
26-
const MAX_PREVIEW_SIZE = 320
28+
const MAX_PREVIEW_SIZE = 1024
2729
// Special case for sharing all the screens with desktop audio in Electron
2830
// In this case, it must have exactly these constraints
2931
// "entire-desktop:0:0" is a custom sourceId for this specific case
@@ -97,7 +99,8 @@ onBeforeUnmount(() => {
9799
* @param event - The event
98100
*/
99101
function onLoadedMetadata(event: Event) {
100-
(event.target as HTMLVideoElement).play()
102+
isReady.value = true
103+
;(event.target as HTMLVideoElement).play()
101104
}
102105
103106
/**
@@ -112,8 +115,36 @@ function onSuspend() {
112115
</script>
113116

114117
<template>
115-
<video ref="videoElement"
116-
muted
117-
@loadedmetadata="onLoadedMetadata"
118-
@suspend="onSuspend" />
118+
<div class="live-preview">
119+
<video v-show="isReady"
120+
ref="videoElement"
121+
class="live-preview__video"
122+
muted
123+
@loadedmetadata="onLoadedMetadata"
124+
@suspend="onSuspend" />
125+
<span v-if="!isReady" class="live-preview__placeholder">
126+
<NcLoadingIcon :size="40" />
127+
</span>
128+
</div>
119129
</template>
130+
131+
<style scoped>
132+
.live-preview {
133+
max-width: 100%;
134+
}
135+
136+
.live-preview__video {
137+
width: 100%;
138+
height: 100%;
139+
}
140+
141+
.live-preview__placeholder {
142+
border-radius: var(--border-radius-small);
143+
background-color: var(--color-background-hover);
144+
color: var(--color-text-maxcontrast);
145+
display: grid;
146+
place-content: center;
147+
width: 100%;
148+
height: 100%;
149+
}
150+
</style>

0 commit comments

Comments
 (0)