Skip to content

Commit ff4b03f

Browse files
authored
UBERF-10517 Fix screen recording in desktop app (#8861)
1 parent 58fc445 commit ff4b03f

File tree

10 files changed

+80
-71
lines changed

10 files changed

+80
-71
lines changed

desktop/src/ui/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { workbenchId, logOut } from '@hcengineering/workbench'
1919

2020
import { isOwnerOrMaintainer } from '@hcengineering/core'
2121
import { configurePlatform } from './platform'
22-
import { defineScreenShare, defineScreenRecorder } from './screenShare'
22+
import { defineScreenShare, defineGetDisplayMedia } from './screenShare'
2323
import { IPCMainExposed } from './types'
2424

2525
defineScreenShare()
26-
defineScreenRecorder()
26+
defineGetDisplayMedia()
2727

2828
void configurePlatform().then(() => {
2929
createApp(document.body)

desktop/src/ui/screenShare.ts

+43-40
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,56 @@ import { showPopup } from '@hcengineering/ui'
55
import { Track, LocalTrack, LocalAudioTrack, LocalVideoTrack, ParticipantEvent, TrackInvalidError, ScreenShareCaptureOptions, DeviceUnsupportedError, ScreenSharePresets } from 'livekit-client'
66

77
import { IPCMainExposed } from './types'
8-
import { setMetadata } from '@hcengineering/platform'
9-
import recordPlugin from '@hcengineering/recorder'
108

11-
export async function getMediaStream (opts?: DisplayMediaStreamOptions): Promise<MediaStream> {
12-
if (opts === undefined) {
13-
throw new Error('opts must be provided')
14-
}
15-
const ipcMain = (window as any).electron as IPCMainExposed
16-
const sources = await ipcMain.getScreenSources()
17-
18-
const hasAccess = await ipcMain.getScreenAccess()
19-
if (!hasAccess) {
20-
log.error('No screen access granted')
21-
throw new Error('No screen access granted')
9+
export function defineGetDisplayMedia (): void {
10+
if (navigator?.mediaDevices === undefined) {
11+
console.warn('mediaDevices API not available')
12+
return
2213
}
2314

2415
if (navigator.mediaDevices.getDisplayMedia === undefined) {
2516
throw new DeviceUnsupportedError('getDisplayMedia not supported')
2617
}
27-
return await new Promise<MediaStream>((resolve, reject) => {
28-
showPopup(
29-
love.component.SelectScreenSourcePopup,
30-
{
31-
sources
32-
},
33-
'top',
34-
() => {
35-
reject(new Error('No source selected'))
36-
},
37-
(val) => {
38-
if (val != null) {
39-
opts.video = {
40-
mandatory: {
41-
...(typeof opts.video === 'boolean' ? {} : opts.video),
42-
chromeMediaSource: 'desktop',
43-
chromeMediaSourceId: val
44-
}
45-
} as any
46-
resolve(window.navigator.mediaDevices.getUserMedia(opts))
47-
}
48-
}
49-
)
50-
})
51-
}
5218

53-
export function defineScreenRecorder (): void {
54-
setMetadata(recordPlugin.metadata.GetCustomMediaStream, getMediaStream)
19+
navigator.mediaDevices.getDisplayMedia = async (opts?: DisplayMediaStreamOptions): Promise<MediaStream> => {
20+
if (opts === undefined) {
21+
throw new Error('opts must be provided')
22+
}
23+
24+
const ipcMain = (window as any).electron as IPCMainExposed
25+
const sources = await ipcMain.getScreenSources()
26+
27+
const hasAccess = await ipcMain.getScreenAccess()
28+
if (!hasAccess) {
29+
log.error('No screen access granted')
30+
throw new Error('No screen access granted')
31+
}
32+
33+
return await new Promise<MediaStream>((resolve, reject) => {
34+
showPopup(
35+
love.component.SelectScreenSourcePopup,
36+
{
37+
sources
38+
},
39+
'top',
40+
() => {
41+
reject(new Error('No source selected'))
42+
},
43+
(val) => {
44+
if (val != null) {
45+
opts.video = {
46+
mandatory: {
47+
...(typeof opts.video === 'boolean' ? {} : opts.video),
48+
chromeMediaSource: 'desktop',
49+
chromeMediaSourceId: val
50+
}
51+
} as any
52+
resolve(window.navigator.mediaDevices.getUserMedia(opts))
53+
}
54+
}
55+
)
56+
})
57+
}
5558
}
5659

5760
export function defineScreenShare (): void {

dev/docker-compose.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ services:
55
- 'huly.local:host-gateway'
66
container_name: stream
77
environment:
8-
- STREAM_ENDPOINT_URL=s3://huly.local:9000
8+
- STREAM_ENDPOINT_URL=datalake://huly.local:4030
99
- STREAM_INSECURE=true
1010
- STREAM_SERVER_SECRET=secret
1111
- AWS_ACCESS_KEY_ID=minioadmin

plugins/love-resources/src/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ lk.on(RoomEvent.Connected, () => {
436436

437437
const session = useMedia({
438438
state: {
439-
camera: { enabled: false },
440-
microphone: current?.type === RoomType.Video ? { enabled: false } : undefined
439+
camera: current?.type === RoomType.Video ? { enabled: false } : undefined,
440+
microphone: { enabled: false }
441441
},
442442
autoDestroy: false
443443
})

plugins/media-resources/src/components/MediaPopupCamPreview.svelte

+16-4
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,24 @@
6464
}
6565
</script>
6666

67-
{#if stream !== null}
68-
<!-- svelte-ignore a11y-media-has-caption -->
69-
<video bind:this={video} width="100%" height="100%" autoplay muted disablepictureinpicture />
70-
{/if}
67+
<div class="container">
68+
{#if stream !== null}
69+
<!-- svelte-ignore a11y-media-has-caption -->
70+
<video bind:this={video} width="100%" height="100%" autoplay muted disablepictureinpicture />
71+
{/if}
72+
</div>
7173

7274
<style lang="scss">
75+
.container {
76+
padding: 0.375rem;
77+
border-radius: 0.375rem;
78+
width: 100%;
79+
80+
display: flex;
81+
align-items: center;
82+
justify-content: center;
83+
}
84+
7385
video {
7486
border-radius: inherit;
7587
transform: rotateY(180deg);

plugins/media-resources/src/components/MediaPopupCamSelector.svelte

+1-13
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@
101101
{/each}
102102

103103
{#if selected}
104-
<div class="preview">
105-
<MediaPopupCamPreview {selected} />
106-
</div>
104+
<MediaPopupCamPreview {selected} />
107105
{/if}
108106
{:else}
109107
<MediaPopupItem
@@ -126,14 +124,4 @@
126124
color: var(--theme-state-positive-color);
127125
}
128126
}
129-
130-
.preview {
131-
padding: 0.375rem;
132-
border-radius: 0.375rem;
133-
width: 100%;
134-
135-
display: flex;
136-
align-items: center;
137-
justify-content: center;
138-
}
139127
</style>

plugins/media/src/utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,14 @@ export async function getMicrophoneStream (
196196
return null
197197
}
198198
}
199+
200+
export async function getDisplayMedia (constraints: MediaStreamConstraints): Promise<MediaStream> {
201+
if (
202+
navigator?.mediaDevices?.getDisplayMedia !== undefined &&
203+
typeof navigator.mediaDevices.getDisplayMedia === 'function'
204+
) {
205+
return await navigator.mediaDevices.getDisplayMedia(constraints)
206+
}
207+
208+
throw new Error('getDisplayMedia not supported')
209+
}

plugins/recorder-resources/src/components/RecorderExt.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
style="padding-left: 0.25rem"
5555
on:click={handleStartRecording}
5656
>
57-
<Icon icon={IconRecordOn} iconProps={{ fill: 'var(--theme-state-negative-color)' }} size="small" />
57+
<Icon icon={IconRecordOn} iconProps={{ fill: 'var(--theme-dark-color)' }} size="small" />
5858
<Icon icon={IconRec} iconProps={{ fill: 'var(--theme-dark-color)' }} size="small" />
5959
</button>
6060
{/if}

plugins/recorder-resources/src/recording.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414
//
1515

16+
import { getDisplayMedia } from '@hcengineering/media'
1617
import { getMetadata } from '@hcengineering/platform'
1718
import presentation from '@hcengineering/presentation'
1819
import { showPopup } from '@hcengineering/ui'
@@ -69,7 +70,7 @@ export async function startRecording (options: RecordingOptions): Promise<void>
6970

7071
let displayStream: MediaStream
7172
try {
72-
displayStream = await navigator.mediaDevices.getDisplayMedia({
73+
displayStream = await getDisplayMedia({
7374
video: {
7475
frameRate: { ideal: fps ?? 30 }
7576
}

plugins/recorder/src/index.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ import { type UploadHandler } from '@hcengineering/uploader'
2121
*/
2222
export const recorderId = 'recorder' as Plugin
2323

24-
/**
25-
* @public
26-
*/
27-
export type GetMediaStream = (options?: DisplayMediaStreamOptions) => Promise<MediaStream>
28-
2924
/**
3025
* @public
3126
*/
@@ -34,8 +29,7 @@ const recordPlugin = plugin(recorderId, {
3429
Record: '' as Asset
3530
},
3631
metadata: {
37-
StreamUrl: '' as Metadata<string>,
38-
GetCustomMediaStream: '' as Metadata<GetMediaStream>
32+
StreamUrl: '' as Metadata<string>
3933
},
4034
space: {
4135
Drive: '' as Ref<Drive>

0 commit comments

Comments
 (0)