qfix: add support for recording videos from desktop (#8306)

This commit is contained in:
Denis Tingaikin 2025-03-21 15:49:42 +03:00 committed by GitHub
parent e70c86f86f
commit c12fee1bb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 79 additions and 10 deletions

View File

@ -115,6 +115,7 @@ function hookOpenWindow (window: BrowserWindow): void {
devTools: true,
sandbox: false,
partition: sessionPartition,
nodeIntegration: true,
preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js'),
additionalArguments: [
`--open=${encodeURI(
@ -192,6 +193,7 @@ const createWindow = async (): Promise<void> => {
webPreferences: {
devTools: true,
sandbox: false,
nodeIntegration: true,
backgroundThrottling: false,
partition: sessionPartition,
preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js')

View File

@ -19,10 +19,11 @@ import { workbenchId, logOut } from '@hcengineering/workbench'
import { isOwnerOrMaintainer } from '@hcengineering/core'
import { configurePlatform } from './platform'
import { defineScreenShare } from './screenShare'
import { defineScreenShare, defineScreenRecorder } from './screenShare'
import { IPCMainExposed } from './types'
defineScreenShare()
defineScreenRecorder()
void configurePlatform().then(() => {
createApp(document.body)

View File

@ -5,6 +5,54 @@ import { showPopup } from '@hcengineering/ui'
import { Track, LocalTrack, LocalAudioTrack, LocalVideoTrack, ParticipantEvent, TrackInvalidError, ScreenShareCaptureOptions, DeviceUnsupportedError, ScreenSharePresets } from 'livekit-client'
import { IPCMainExposed } from './types'
import { setMetadata } from '@hcengineering/platform'
import recordPlugin from '@hcengineering/recorder'
export async function getMediaStream (opts?: DisplayMediaStreamOptions): Promise<MediaStream> {
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')
}
if (navigator.mediaDevices.getDisplayMedia === undefined) {
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 {
setMetadata(recordPlugin.metadata.GetCustomMediaStream, getMediaStream)
}
export function defineScreenShare (): void {
setCustomCreateScreenTracks(async function electronCreateScreenTracks (options?: ScreenShareCaptureOptions) {

View File

@ -91,8 +91,10 @@
if (state === RecordingState.Inactive) {
try {
await createScreenRecorder()
} catch {
} catch (err) {
console.log('cant create screen recorder', err)
distpacher('close', true)
return
}
await showCountdown()
recorder?.start()

View File

@ -16,6 +16,8 @@
import { type Resources } from '@hcengineering/platform'
import { record } from './recording'
export { ScreenRecorder } from './screen-recorder'
export default async (): Promise<Resources> => ({
function: {
Record: record

View File

@ -13,8 +13,10 @@
// limitations under the License.
//
import plugin from '@hcengineering/recorder'
import { Recorder } from './recorder'
import { TusUploader, type Uploader, type Options } from './uploader'
import { getMetadata } from '@hcengineering/platform'
export class ScreenRecorder {
private readonly recorder: Recorder
@ -29,11 +31,20 @@ export class ScreenRecorder {
let width = 0
let height = 0
const combinedStream = new MediaStream()
const displayStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: opts.fps ?? 30 },
audio: true
const getMediaStream =
getMetadata(plugin.metadata.GetCustomMediaStream) ??
(async (op) => await navigator.mediaDevices.getDisplayMedia(op))
const displayStream = await getMediaStream({
video: { frameRate: opts.fps ?? 30 }
})
const microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true })
try {
const microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true })
microphoneStream.getAudioTracks().forEach((track) => {
combinedStream.addTrack(track)
})
} catch (err) {
console.warn('microphone is disabled', err)
}
displayStream.getVideoTracks().forEach((track) => {
combinedStream.addTrack(track)
width = Math.max(track.getSettings().width ?? width, width)
@ -42,9 +53,6 @@ export class ScreenRecorder {
displayStream.getAudioTracks().forEach((track) => {
combinedStream.addTrack(track)
})
microphoneStream.getAudioTracks().forEach((track) => {
combinedStream.addTrack(track)
})
const recorder = new Recorder(combinedStream)
const uploader = new TusUploader(recorder.asStream(), { ...opts, metadata: { resolution: width + ':' + height } })

View File

@ -19,6 +19,11 @@ import { type UploadHandler } from '@hcengineering/uploader'
*/
export const recorderId = 'recorder' as Plugin
/**
* @public
*/
export type GetMediaStream = (options?: DisplayMediaStreamOptions) => Promise<MediaStream>
/**
* @public
*/
@ -27,7 +32,8 @@ const recordPlugin = plugin(recorderId, {
Record: '' as Asset
},
metadata: {
StreamUrl: '' as Metadata<string>
StreamUrl: '' as Metadata<string>,
GetCustomMediaStream: '' as Metadata<GetMediaStream>
},
string: {
Pause: '' as IntlString,