Skip to content

Commit

Permalink
WIP
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 Dec 23, 2024
1 parent 81c6b4f commit 7239513
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 93 deletions.
13 changes: 13 additions & 0 deletions src/talk/renderer/DesktopHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ function handleGlobalEscape(event) {
}
}

/**
*
*/
async function handleScreenshare() {
console.log(await window.OCA.Talk.Desktop.getDesktopMediaSource())
}

onMounted(() => {
window.addEventListener('keydown', handleGlobalEscape, { capture: true })
})
Expand All @@ -68,6 +75,12 @@ onUnmounted(() => {

<div class="spacer" />

<div class="header__item" data-theme-dark>
<button @click="handleScreenshare">
Screenshare
</button>
</div>

<div class="header__item" data-theme-dark>
<MainMenu />
</div>
Expand Down
60 changes: 49 additions & 11 deletions src/talk/renderer/screensharing/DesktopMediaSourceDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
import IconMonitor from 'vue-material-design-icons/Monitor.vue'
import IconMonitorShare from '@mdi/svg/svg/monitor-share.svg?raw'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import { translate as t } from '@nextcloud/l10n'
import DesktopMediaSourcePreview from './DesktopMediaSourcePreview.vue'
import type { ScreensharingSource, ScreensharingSourceId } from './screensharing.types.ts'
import DesktopMediaSourcePreviewVideo from './DesktopMediaSourcePreviewVideo.vue'

const emit = defineEmits<{
(event: 'submit', sourceId: ScreensharingSourceId): void
(event: 'cancel'): void
}>()

const RE_REQUEST_SOURCES_TIMEOUT = 1000
const RE_REQUEST_SOURCES_TIMEOUT = 5000

// On Wayland getting each stream for the live preview requests user to select the source via system dialog again
// Instead - show static images.
Expand All @@ -29,6 +31,11 @@ const previewType = window.systemInfo.isWayland ? 'thumbnail' : 'live'
const selectedSourceId = ref<ScreensharingSourceId | null>(null)
const sources = ref<ScreensharingSource[] | null>(null)

const selectedSource = computed(() => {
console.log('changed', sources.value, selectedSourceId.value)
return sources.value?.find((source) => source.id === selectedSourceId.value)
})

const handleSubmit = () => emit('submit', selectedSourceId.value!)
const handleCancel = () => emit('cancel')

Expand Down Expand Up @@ -107,9 +114,9 @@ onMounted(async () => {
await requestDesktopCapturerSources()

// Preselect the first media source if any
if (!selectedSourceId.value) {
selectedSourceId.value = sources.value?.[0]?.id ?? null
}
// if (!selectedSourceId.value) {
// selectedSourceId.value = sources.value?.[0]?.id ?? null

Check failure on line 118 in src/talk/renderer/screensharing/DesktopMediaSourceDialog.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Unexpected tab character
// }

if (previewType === 'live') {
scheduleRequestDesktopCaprutererSources()
Expand All @@ -128,13 +135,24 @@ onBeforeUnmount(() => {
size="normal"
:buttons="dialogButtons"
@update:open="handleCancel">
<div v-if="sources" class="capture-source-grid">
<DesktopMediaSourcePreview v-for="source in sources"
:key="source.id"
:source="source"
:selected="selectedSourceId === source.id"
@select="selectedSourceId = source.id"
@suspend="handleVideoSuspend(source)" />
<div v-if="sources" class="capture-source-container">
<div v-if="previewType === 'live'" class="capture-source-preview">
<!-- @vue-expect-error Vue 2 doesn't understand that selectedSourceId is not undefined here -->
<DesktopMediaSourcePreviewVideo v-if="selectedSourceId" :key="selectedSource.id" :media-source-id="selectedSource.id" />
<NcEmptyContent v-else :name="t('talk_desktop', 'Choose source')">
<template #icon>
<IconMonitor />
</template>
</NcEmptyContent>
</div>
<div class="capture-source-grid">
<DesktopMediaSourcePreview v-for="source in sources"
:key="source.id"
:source="source"
:selected="selectedSourceId === source.id"
@select="selectedSourceId = source.id"
@suspend="handleVideoSuspend(source)" />
</div>
</div>
<NcEmptyContent v-else :name="t('talk_desktop', 'Loading …')">
<template #icon>
Expand All @@ -145,6 +163,26 @@ onBeforeUnmount(() => {
</template>

<style scoped lang="scss">
.capture-source-container {
display: flex;
flex-direction: column;
gap: calc(var(--default-grid-baseline) * 2);
}

.capture-source-preview {
aspect-ratio: 16 / 9;
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-background-darker);
border-radius: var(--border-radius-container);
}

.capture-source-preview :deep(video) {
aspect-ratio: 16 / 9;
object-fit: scale-down;
}

.capture-source-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
Expand Down
89 changes: 7 additions & 82 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@
-->

<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 DesktopMediaSourcePreviewVideo from './DesktopMediaSourcePreviewVideo.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 previewType = window.systemInfo.isWayland ? 'thumbnail' : 'live'
const previewType = 'thumbnail'

const videoElement = ref<HTMLVideoElement | null>(null)

const props = defineProps<{
defineProps<{
source: ScreensharingSource
selected: boolean
}>()
Expand All @@ -26,78 +25,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() {
videoElement.value!.srcObject = await getStreamForMediaSource(props.source.id)
}

/**
* Release the video source
*/
function releaseVideoSource() {
const stream = videoElement.value!.srcObject! as MediaStream
for (const track of stream.getTracks()) {
track.stop()
}
}

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

onBeforeUnmount(() => {
// Release the stream, otherwise it is still captured even if no video element is using it
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 @@ -109,11 +36,9 @@ function onLoadedMetadata(event: Event) {
:checked="selected"
@change="emit('select')">

<video v-if="previewType === 'live'"
ref="videoElement"
<DesktopMediaSourcePreviewVideo v-if="previewType === 'live'"

Check failure on line 39 in src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

Argument of type '{ source: ScreensharingSource; class: string; onSuspend: any; }' is not assignable to parameter of type 'Readonly<Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_TypePropsToOption<__VLS_Props>>>, never>> & Record<...>'.

Check failure on line 39 in src/talk/renderer/screensharing/DesktopMediaSourcePreview.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

This comparison appears to be unintentional because the types '"thumbnail"' and '"live"' have no overlap.
class="capture-source__preview"
muted
@loadedmetadata="onLoadedMetadata"
:source="source"
@suspend="emit('suspend')" />
<img v-else-if="previewType === 'thumbnail' && source.thumbnail"
alt=""
Expand Down
114 changes: 114 additions & 0 deletions src/talk/renderer/screensharing/DesktopMediaSourcePreviewVideo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

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

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

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

const videoElement = ref<HTMLVideoElement | 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() {
videoElement.value!.srcObject = await getStreamForMediaSource(props.mediaSourceId)
}

/**
* Release the video source
*/
function releaseVideoSource() {
const stream = videoElement.value!.srcObject! as MediaStream
for (const track of stream.getTracks()) {
track.stop()
}
videoElement.value!.srcObject = null
}

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

onBeforeUnmount(() => {
// Release the stream, otherwise it is still captured even if no video element is using it
releaseVideoSource()
})

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

/**
*
*/
function onSuspend() {
if (videoElement.value!.srcObject) {
emit('suspend')
}
}
</script>

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

<style scoped lang="scss">

</style>

0 comments on commit 7239513

Please sign in to comment.