mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-10 01:15:03 +00:00
UBERF-10517 Fix screen recording in desktop app (#8861)
This commit is contained in:
parent
58fc4453b4
commit
ff4b03f596
@ -19,11 +19,11 @@ import { workbenchId, logOut } from '@hcengineering/workbench'
|
|||||||
|
|
||||||
import { isOwnerOrMaintainer } from '@hcengineering/core'
|
import { isOwnerOrMaintainer } from '@hcengineering/core'
|
||||||
import { configurePlatform } from './platform'
|
import { configurePlatform } from './platform'
|
||||||
import { defineScreenShare, defineScreenRecorder } from './screenShare'
|
import { defineScreenShare, defineGetDisplayMedia } from './screenShare'
|
||||||
import { IPCMainExposed } from './types'
|
import { IPCMainExposed } from './types'
|
||||||
|
|
||||||
defineScreenShare()
|
defineScreenShare()
|
||||||
defineScreenRecorder()
|
defineGetDisplayMedia()
|
||||||
|
|
||||||
void configurePlatform().then(() => {
|
void configurePlatform().then(() => {
|
||||||
createApp(document.body)
|
createApp(document.body)
|
||||||
|
@ -5,53 +5,56 @@ import { showPopup } from '@hcengineering/ui'
|
|||||||
import { Track, LocalTrack, LocalAudioTrack, LocalVideoTrack, ParticipantEvent, TrackInvalidError, ScreenShareCaptureOptions, DeviceUnsupportedError, ScreenSharePresets } from 'livekit-client'
|
import { Track, LocalTrack, LocalAudioTrack, LocalVideoTrack, ParticipantEvent, TrackInvalidError, ScreenShareCaptureOptions, DeviceUnsupportedError, ScreenSharePresets } from 'livekit-client'
|
||||||
|
|
||||||
import { IPCMainExposed } from './types'
|
import { IPCMainExposed } from './types'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
|
||||||
import recordPlugin from '@hcengineering/recorder'
|
|
||||||
|
|
||||||
export async function getMediaStream (opts?: DisplayMediaStreamOptions): Promise<MediaStream> {
|
export function defineGetDisplayMedia (): void {
|
||||||
if (opts === undefined) {
|
if (navigator?.mediaDevices === undefined) {
|
||||||
throw new Error('opts must be provided')
|
console.warn('mediaDevices API not available')
|
||||||
}
|
return
|
||||||
const ipcMain = (window as any).electron as IPCMainExposed
|
|
||||||
const sources = await ipcMain.getScreenSources()
|
|
||||||
|
|
||||||
const hasAccess = await ipcMain.getScreenAccess()
|
|
||||||
if (!hasAccess) {
|
|
||||||
log.error('No screen access granted')
|
|
||||||
throw new Error('No screen access granted')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigator.mediaDevices.getDisplayMedia === undefined) {
|
if (navigator.mediaDevices.getDisplayMedia === undefined) {
|
||||||
throw new DeviceUnsupportedError('getDisplayMedia not supported')
|
throw new DeviceUnsupportedError('getDisplayMedia not supported')
|
||||||
}
|
}
|
||||||
return await new Promise<MediaStream>((resolve, reject) => {
|
|
||||||
showPopup(
|
|
||||||
love.component.SelectScreenSourcePopup,
|
|
||||||
{
|
|
||||||
sources
|
|
||||||
},
|
|
||||||
'top',
|
|
||||||
() => {
|
|
||||||
reject(new Error('No source selected'))
|
|
||||||
},
|
|
||||||
(val) => {
|
|
||||||
if (val != null) {
|
|
||||||
opts.video = {
|
|
||||||
mandatory: {
|
|
||||||
...(typeof opts.video === 'boolean' ? {} : opts.video),
|
|
||||||
chromeMediaSource: 'desktop',
|
|
||||||
chromeMediaSourceId: val
|
|
||||||
}
|
|
||||||
} as any
|
|
||||||
resolve(window.navigator.mediaDevices.getUserMedia(opts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defineScreenRecorder (): void {
|
navigator.mediaDevices.getDisplayMedia = async (opts?: DisplayMediaStreamOptions): Promise<MediaStream> => {
|
||||||
setMetadata(recordPlugin.metadata.GetCustomMediaStream, getMediaStream)
|
if (opts === undefined) {
|
||||||
|
throw new Error('opts must be provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipcMain = (window as any).electron as IPCMainExposed
|
||||||
|
const sources = await ipcMain.getScreenSources()
|
||||||
|
|
||||||
|
const hasAccess = await ipcMain.getScreenAccess()
|
||||||
|
if (!hasAccess) {
|
||||||
|
log.error('No screen access granted')
|
||||||
|
throw new Error('No screen access granted')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await new Promise<MediaStream>((resolve, reject) => {
|
||||||
|
showPopup(
|
||||||
|
love.component.SelectScreenSourcePopup,
|
||||||
|
{
|
||||||
|
sources
|
||||||
|
},
|
||||||
|
'top',
|
||||||
|
() => {
|
||||||
|
reject(new Error('No source selected'))
|
||||||
|
},
|
||||||
|
(val) => {
|
||||||
|
if (val != null) {
|
||||||
|
opts.video = {
|
||||||
|
mandatory: {
|
||||||
|
...(typeof opts.video === 'boolean' ? {} : opts.video),
|
||||||
|
chromeMediaSource: 'desktop',
|
||||||
|
chromeMediaSourceId: val
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
resolve(window.navigator.mediaDevices.getUserMedia(opts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineScreenShare (): void {
|
export function defineScreenShare (): void {
|
||||||
|
@ -5,7 +5,7 @@ services:
|
|||||||
- 'huly.local:host-gateway'
|
- 'huly.local:host-gateway'
|
||||||
container_name: stream
|
container_name: stream
|
||||||
environment:
|
environment:
|
||||||
- STREAM_ENDPOINT_URL=s3://huly.local:9000
|
- STREAM_ENDPOINT_URL=datalake://huly.local:4030
|
||||||
- STREAM_INSECURE=true
|
- STREAM_INSECURE=true
|
||||||
- STREAM_SERVER_SECRET=secret
|
- STREAM_SERVER_SECRET=secret
|
||||||
- AWS_ACCESS_KEY_ID=minioadmin
|
- AWS_ACCESS_KEY_ID=minioadmin
|
||||||
|
@ -436,8 +436,8 @@ lk.on(RoomEvent.Connected, () => {
|
|||||||
|
|
||||||
const session = useMedia({
|
const session = useMedia({
|
||||||
state: {
|
state: {
|
||||||
camera: { enabled: false },
|
camera: current?.type === RoomType.Video ? { enabled: false } : undefined,
|
||||||
microphone: current?.type === RoomType.Video ? { enabled: false } : undefined
|
microphone: { enabled: false }
|
||||||
},
|
},
|
||||||
autoDestroy: false
|
autoDestroy: false
|
||||||
})
|
})
|
||||||
|
@ -64,12 +64,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if stream !== null}
|
<div class="container">
|
||||||
<!-- svelte-ignore a11y-media-has-caption -->
|
{#if stream !== null}
|
||||||
<video bind:this={video} width="100%" height="100%" autoplay muted disablepictureinpicture />
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
{/if}
|
<video bind:this={video} width="100%" height="100%" autoplay muted disablepictureinpicture />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
padding: 0.375rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
video {
|
video {
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
|
@ -101,9 +101,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if selected}
|
{#if selected}
|
||||||
<div class="preview">
|
<MediaPopupCamPreview {selected} />
|
||||||
<MediaPopupCamPreview {selected} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<MediaPopupItem
|
<MediaPopupItem
|
||||||
@ -126,14 +124,4 @@
|
|||||||
color: var(--theme-state-positive-color);
|
color: var(--theme-state-positive-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
padding: 0.375rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -196,3 +196,14 @@ export async function getMicrophoneStream (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDisplayMedia (constraints: MediaStreamConstraints): Promise<MediaStream> {
|
||||||
|
if (
|
||||||
|
navigator?.mediaDevices?.getDisplayMedia !== undefined &&
|
||||||
|
typeof navigator.mediaDevices.getDisplayMedia === 'function'
|
||||||
|
) {
|
||||||
|
return await navigator.mediaDevices.getDisplayMedia(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('getDisplayMedia not supported')
|
||||||
|
}
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
style="padding-left: 0.25rem"
|
style="padding-left: 0.25rem"
|
||||||
on:click={handleStartRecording}
|
on:click={handleStartRecording}
|
||||||
>
|
>
|
||||||
<Icon icon={IconRecordOn} iconProps={{ fill: 'var(--theme-state-negative-color)' }} size="small" />
|
<Icon icon={IconRecordOn} iconProps={{ fill: 'var(--theme-dark-color)' }} size="small" />
|
||||||
<Icon icon={IconRec} iconProps={{ fill: 'var(--theme-dark-color)' }} size="small" />
|
<Icon icon={IconRec} iconProps={{ fill: 'var(--theme-dark-color)' }} size="small" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { getDisplayMedia } from '@hcengineering/media'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
import presentation from '@hcengineering/presentation'
|
import presentation from '@hcengineering/presentation'
|
||||||
import { showPopup } from '@hcengineering/ui'
|
import { showPopup } from '@hcengineering/ui'
|
||||||
@ -69,7 +70,7 @@ export async function startRecording (options: RecordingOptions): Promise<void>
|
|||||||
|
|
||||||
let displayStream: MediaStream
|
let displayStream: MediaStream
|
||||||
try {
|
try {
|
||||||
displayStream = await navigator.mediaDevices.getDisplayMedia({
|
displayStream = await getDisplayMedia({
|
||||||
video: {
|
video: {
|
||||||
frameRate: { ideal: fps ?? 30 }
|
frameRate: { ideal: fps ?? 30 }
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,6 @@ import { type UploadHandler } from '@hcengineering/uploader'
|
|||||||
*/
|
*/
|
||||||
export const recorderId = 'recorder' as Plugin
|
export const recorderId = 'recorder' as Plugin
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type GetMediaStream = (options?: DisplayMediaStreamOptions) => Promise<MediaStream>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -34,8 +29,7 @@ const recordPlugin = plugin(recorderId, {
|
|||||||
Record: '' as Asset
|
Record: '' as Asset
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
StreamUrl: '' as Metadata<string>,
|
StreamUrl: '' as Metadata<string>
|
||||||
GetCustomMediaStream: '' as Metadata<GetMediaStream>
|
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
Drive: '' as Ref<Drive>
|
Drive: '' as Ref<Drive>
|
||||||
|
Loading…
Reference in New Issue
Block a user