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

fix(screensharing): picker performance and UI improvements #1003

Merged
merged 10 commits into from
Jan 31, 2025
Prev Previous commit
Next Next commit
perf(screensharing): make live preview optional
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
  • Loading branch information
ShGKme committed Jan 31, 2025

Verified

This commit was signed with the committer’s verified signature.
mchack-work Michael Cardell Widerkrantz
commit 33d9507725a4e4fb4a132f3e31ff000553c50656
5 changes: 2 additions & 3 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ const { createAuthenticationWindow } = require('./authentication/authentication.
const { openLoginWebView } = require('./authentication/login.window.js')
const { createHelpWindow } = require('./help/help.window.js')
const { createUpgradeWindow } = require('./upgrade/upgrade.window.js')
const { systemInfo, isLinux, isMac, isWayland, isWindows } = require('./app/system.utils.ts')
const { systemInfo, isLinux, isMac, isWindows } = require('./app/system.utils.ts')
const { createTalkWindow } = require('./talk/talk.window.js')
const { createWelcomeWindow } = require('./welcome/welcome.window.js')
const { installVueDevtools } = require('./install-vue-devtools.js')
@@ -94,8 +94,7 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => {
return null
}

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

const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
73 changes: 55 additions & 18 deletions src/talk/renderer/screensharing/DesktopMediaSourceDialog.vue
Original file line number Diff line number Diff line change
@@ -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 { useWindowFocus } from '@vueuse/core'
import DesktopMediaSourcePreview from './DesktopMediaSourcePreview.vue'
@@ -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.
@@ -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 isWindowFocused = useWindowFocus()
watch(isWindowFocused, requestDesktopCapturerSources)
@@ -125,30 +119,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-fit, 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
@@ -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
}>()

@@ -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"
@@ -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);
@@ -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 {
45 changes: 38 additions & 7 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreviewLive.vue
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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()
}

/**
@@ -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>