Native macOS Sequoia screen sharing
This commit is contained in:
parent
2640c34bd3
commit
326f90bb75
6 changed files with 184 additions and 22 deletions
|
@ -63,6 +63,28 @@ Signal Desktop makes use of the following open source projects.
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
IN THE SOFTWARE.
|
IN THE SOFTWARE.
|
||||||
|
|
||||||
|
## @indutny/mac-screen-share
|
||||||
|
|
||||||
|
Copyright Fedor Indutny, 2024.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
## @indutny/range-finder
|
## @indutny/range-finder
|
||||||
|
|
||||||
Copyright Fedor Indutny, 2022.
|
Copyright Fedor Indutny, 2022.
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -14,6 +14,7 @@
|
||||||
"@formatjs/icu-messageformat-parser": "2.3.0",
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
"@formatjs/intl-localematcher": "0.2.32",
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
"@indutny/dicer": "0.3.2",
|
"@indutny/dicer": "0.3.2",
|
||||||
|
"@indutny/mac-screen-share": "1.0.3",
|
||||||
"@indutny/range-finder": "1.3.4",
|
"@indutny/range-finder": "1.3.4",
|
||||||
"@indutny/simple-windows-notifications": "2.0.6",
|
"@indutny/simple-windows-notifications": "2.0.6",
|
||||||
"@indutny/sneequals": "4.0.0",
|
"@indutny/sneequals": "4.0.0",
|
||||||
|
@ -4062,6 +4063,17 @@
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@indutny/mac-screen-share": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@indutny/mac-screen-share/-/mac-screen-share-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-oBeZQGw4mvY654pc4muQUzttZvNBlnwU0ywMDFkNGYO1UdH1H20Fx+XGZbtDl1t7827U+swRNajCROuzQztUrw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"node-addon-api": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@indutny/parallel-prettier": {
|
"node_modules/@indutny/parallel-prettier": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@indutny/parallel-prettier/-/parallel-prettier-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@indutny/parallel-prettier/-/parallel-prettier-3.0.0.tgz",
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
"@formatjs/icu-messageformat-parser": "2.3.0",
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
"@formatjs/intl-localematcher": "0.2.32",
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
"@indutny/dicer": "0.3.2",
|
"@indutny/dicer": "0.3.2",
|
||||||
|
"@indutny/mac-screen-share": "1.0.3",
|
||||||
"@indutny/range-finder": "1.3.4",
|
"@indutny/range-finder": "1.3.4",
|
||||||
"@indutny/simple-windows-notifications": "2.0.6",
|
"@indutny/simple-windows-notifications": "2.0.6",
|
||||||
"@indutny/sneequals": "4.0.0",
|
"@indutny/sneequals": "4.0.0",
|
||||||
|
@ -580,8 +581,10 @@
|
||||||
"node_modules/@signalapp/ringrtc/build/${platform}/*${arch}*.node",
|
"node_modules/@signalapp/ringrtc/build/${platform}/*${arch}*.node",
|
||||||
"node_modules/mac-screen-capture-permissions/build/Release/*.node",
|
"node_modules/mac-screen-capture-permissions/build/Release/*.node",
|
||||||
"node_modules/@indutny/simple-windows-notifications/build/Release/*.node",
|
"node_modules/@indutny/simple-windows-notifications/build/Release/*.node",
|
||||||
|
"node_modules/@indutny/mac-screen-share/build/Release/*.node",
|
||||||
"node_modules/fs-xattr/build/Release/*.node",
|
"node_modules/fs-xattr/build/Release/*.node",
|
||||||
"!node_modules/@indutny/simple-windows-notifications/*.cpp",
|
"!node_modules/@indutny/simple-windows-notifications/*.cpp",
|
||||||
|
"!node_modules/@indutny/mac-screen-share/*.{mm,h}",
|
||||||
"!node_modules/libheif-js/libheif",
|
"!node_modules/libheif-js/libheif",
|
||||||
"!node_modules/libheif-js/libheif-wasm/libheif-bundle.mjs",
|
"!node_modules/libheif-js/libheif-wasm/libheif-bundle.mjs",
|
||||||
"!node_modules/libheif-js/libheif-wasm/*.wasm",
|
"!node_modules/libheif-js/libheif-wasm/*.wasm",
|
||||||
|
|
|
@ -35,6 +35,7 @@ const bundleDefaults = {
|
||||||
'@signalapp/libsignal-client/zkgroup',
|
'@signalapp/libsignal-client/zkgroup',
|
||||||
'@signalapp/ringrtc',
|
'@signalapp/ringrtc',
|
||||||
'@signalapp/better-sqlite3',
|
'@signalapp/better-sqlite3',
|
||||||
|
'@indutny/mac-screen-share',
|
||||||
'electron',
|
'electron',
|
||||||
'fs-xattr',
|
'fs-xattr',
|
||||||
'fsevents',
|
'fsevents',
|
||||||
|
|
|
@ -185,7 +185,6 @@ export type ActiveCallStateType = {
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
presentingSource?: PresentedSource;
|
presentingSource?: PresentedSource;
|
||||||
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
|
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
|
||||||
capturerBaton?: DesktopCapturerBaton;
|
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
|
@ -216,6 +215,7 @@ export type CallingStateType = MediaDeviceSettings & {
|
||||||
adhocCalls: AdhocCallsType;
|
adhocCalls: AdhocCallsType;
|
||||||
callLinks: CallLinksByRoomIdType;
|
callLinks: CallLinksByRoomIdType;
|
||||||
activeCallState?: ActiveCallStateType | WaitingCallStateType;
|
activeCallState?: ActiveCallStateType | WaitingCallStateType;
|
||||||
|
capturerBaton?: DesktopCapturerBaton;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AcceptCallType = ReadonlyDeep<{
|
export type AcceptCallType = ReadonlyDeep<{
|
||||||
|
@ -649,6 +649,7 @@ const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||||
const SET_OUTGOING_RING = 'calling/SET_OUTGOING_RING';
|
const SET_OUTGOING_RING = 'calling/SET_OUTGOING_RING';
|
||||||
const SET_PRESENTING = 'calling/SET_PRESENTING';
|
const SET_PRESENTING = 'calling/SET_PRESENTING';
|
||||||
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
||||||
|
const SET_CAPTURER_BATON = 'calling/SET_CAPTURER_BATON';
|
||||||
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
|
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
|
||||||
'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
|
'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
|
||||||
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
||||||
|
@ -897,10 +898,14 @@ type SetPresentingSourcesActionType = ReadonlyDeep<{
|
||||||
type: 'calling/SET_PRESENTING_SOURCES';
|
type: 'calling/SET_PRESENTING_SOURCES';
|
||||||
payload: {
|
payload: {
|
||||||
presentableSources: ReadonlyArray<PresentableSource>;
|
presentableSources: ReadonlyArray<PresentableSource>;
|
||||||
capturerBaton: DesktopCapturerBaton;
|
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type SetCapturerBatonActionType = ReadonlyDeep<{
|
||||||
|
type: 'calling/SET_CAPTURER_BATON';
|
||||||
|
payload: DesktopCapturerBaton;
|
||||||
|
}>;
|
||||||
|
|
||||||
type SetOutgoingRingActionType = ReadonlyDeep<{
|
type SetOutgoingRingActionType = ReadonlyDeep<{
|
||||||
type: 'calling/SET_OUTGOING_RING';
|
type: 'calling/SET_OUTGOING_RING';
|
||||||
payload: boolean;
|
payload: boolean;
|
||||||
|
@ -977,6 +982,7 @@ export type CallingActionType =
|
||||||
| ReturnToActiveCallActionType
|
| ReturnToActiveCallActionType
|
||||||
| SendGroupCallReactionActionType
|
| SendGroupCallReactionActionType
|
||||||
| SelectPresentingSourceActionType
|
| SelectPresentingSourceActionType
|
||||||
|
| SetCapturerBatonActionType
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
| SetPresentingSourcesActionType
|
| SetPresentingSourcesActionType
|
||||||
|
@ -1296,6 +1302,7 @@ function getPresentingSources(): ThunkAction<
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
unknown,
|
unknown,
|
||||||
|
| SetCapturerBatonActionType
|
||||||
| SetPresentingSourcesActionType
|
| SetPresentingSourcesActionType
|
||||||
| ToggleNeedsScreenRecordingPermissionsActionType
|
| ToggleNeedsScreenRecordingPermissionsActionType
|
||||||
> {
|
> {
|
||||||
|
@ -1318,7 +1325,7 @@ function getPresentingSources(): ThunkAction<
|
||||||
onPresentableSources(presentableSources) {
|
onPresentableSources(presentableSources) {
|
||||||
if (needsPermission) {
|
if (needsPermission) {
|
||||||
// Abort
|
// Abort
|
||||||
capturer.selectSource(undefined);
|
capturer.abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1326,7 +1333,6 @@ function getPresentingSources(): ThunkAction<
|
||||||
type: SET_PRESENTING_SOURCES,
|
type: SET_PRESENTING_SOURCES,
|
||||||
payload: {
|
payload: {
|
||||||
presentableSources,
|
presentableSources,
|
||||||
capturerBaton: capturer.baton,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1353,6 +1359,11 @@ function getPresentingSources(): ThunkAction<
|
||||||
});
|
});
|
||||||
globalCapturers.set(capturer.baton, capturer);
|
globalCapturers.set(capturer.baton, capturer);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_CAPTURER_BATON,
|
||||||
|
payload: capturer.baton,
|
||||||
|
});
|
||||||
|
|
||||||
if (needsPermission) {
|
if (needsPermission) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
|
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
|
||||||
|
@ -2753,6 +2764,25 @@ function mergeCallWithGroupCallLookups({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortCapturer(
|
||||||
|
state: Readonly<CallingStateType>
|
||||||
|
): Readonly<CallingStateType> {
|
||||||
|
const { capturerBaton } = state;
|
||||||
|
if (capturerBaton == null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel source selection if running
|
||||||
|
const capturer = globalCapturers.get(capturerBaton);
|
||||||
|
strictAssert(capturer != null, 'Capturer reference exists, but not capturer');
|
||||||
|
capturer.abort();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
capturerBaton: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: Readonly<CallingStateType> = getEmptyState(),
|
state: Readonly<CallingStateType> = getEmptyState(),
|
||||||
action: Readonly<CallingActionType>
|
action: Readonly<CallingActionType>
|
||||||
|
@ -2972,17 +3002,22 @@ export function reducer(
|
||||||
action.type === HANG_UP ||
|
action.type === HANG_UP ||
|
||||||
action.type === CLOSE_NEED_PERMISSION_SCREEN
|
action.type === CLOSE_NEED_PERMISSION_SCREEN
|
||||||
) {
|
) {
|
||||||
const activeCall = getActiveCall(state);
|
const updatedState = abortCapturer(state);
|
||||||
|
const activeCall = getActiveCall(updatedState);
|
||||||
if (!activeCall) {
|
if (!activeCall) {
|
||||||
log.warn(`${action.type}: No active call to remove`);
|
log.warn(`${action.type}: No active call to remove`);
|
||||||
return state;
|
return updatedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct:
|
case CallMode.Direct:
|
||||||
return removeConversationFromState(state, activeCall.conversationId);
|
return removeConversationFromState(
|
||||||
|
updatedState,
|
||||||
|
activeCall.conversationId
|
||||||
|
);
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
case CallMode.Adhoc:
|
case CallMode.Adhoc:
|
||||||
return omit(state, 'activeCallState');
|
return omit(updatedState, 'activeCallState');
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(activeCall);
|
throw missingCaseError(activeCall);
|
||||||
}
|
}
|
||||||
|
@ -3471,6 +3506,13 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_CAPTURER_BATON) {
|
||||||
|
return {
|
||||||
|
...abortCapturer(state),
|
||||||
|
capturerBaton: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
action.type === SEND_GROUP_CALL_REACTION ||
|
action.type === SEND_GROUP_CALL_REACTION ||
|
||||||
action.type === GROUP_CALL_REACTIONS_RECEIVED
|
action.type === GROUP_CALL_REACTIONS_RECEIVED
|
||||||
|
@ -3753,25 +3795,19 @@ export function reducer(
|
||||||
|
|
||||||
if (action.type === SET_PRESENTING) {
|
if (action.type === SET_PRESENTING) {
|
||||||
const { activeCallState } = state;
|
const { activeCallState } = state;
|
||||||
|
|
||||||
if (activeCallState?.state !== 'Active') {
|
if (activeCallState?.state !== 'Active') {
|
||||||
log.warn('Cannot toggle presenting when there is no active call');
|
log.warn('Cannot toggle presenting when there is no active call');
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel source selection if running
|
|
||||||
const { capturerBaton } = activeCallState;
|
|
||||||
if (capturerBaton != null) {
|
|
||||||
const capturer = globalCapturers.get(capturerBaton);
|
|
||||||
capturer?.selectSource(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
capturerBaton: undefined,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
...activeCallState,
|
...activeCallState,
|
||||||
presentingSource: action.payload,
|
presentingSource: action.payload,
|
||||||
presentingSourcesAvailable: undefined,
|
presentingSourcesAvailable: undefined,
|
||||||
capturerBaton: undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3788,19 +3824,18 @@ export function reducer(
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
...activeCallState,
|
...activeCallState,
|
||||||
presentingSourcesAvailable: action.payload.presentableSources,
|
presentingSourcesAvailable: action.payload.presentableSources,
|
||||||
capturerBaton: action.payload.capturerBaton,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === SELECT_PRESENTING_SOURCE) {
|
if (action.type === SELECT_PRESENTING_SOURCE) {
|
||||||
const { activeCallState } = state;
|
const { activeCallState, capturerBaton } = state;
|
||||||
if (activeCallState?.state !== 'Active') {
|
if (activeCallState?.state !== 'Active') {
|
||||||
log.warn('Cannot set presenting sources when there is no active call');
|
log.warn('Cannot set presenting sources when there is no active call');
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { capturerBaton, presentingSourcesAvailable } = activeCallState;
|
const { presentingSourcesAvailable } = activeCallState;
|
||||||
if (!capturerBaton || !presentingSourcesAvailable) {
|
if (!capturerBaton || !presentingSourcesAvailable) {
|
||||||
log.warn(
|
log.warn(
|
||||||
'Cannot set presenting sources when there is no presenting modal'
|
'Cannot set presenting sources when there is no presenting modal'
|
||||||
|
@ -3817,13 +3852,13 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
capturerBaton: undefined,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
...activeCallState,
|
...activeCallState,
|
||||||
presentingSource: presentingSourcesAvailable.find(
|
presentingSource: presentingSourcesAvailable.find(
|
||||||
source => source.id === action.payload
|
source => source.id === action.payload
|
||||||
),
|
),
|
||||||
presentingSourcesAvailable: undefined,
|
presentingSourcesAvailable: undefined,
|
||||||
capturerBaton: undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright 2024 Signal Messenger, LLC
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
import { ipcRenderer, type DesktopCapturerSource } from 'electron';
|
import { ipcRenderer, type DesktopCapturerSource } from 'electron';
|
||||||
|
import * as macScreenShare from '@indutny/mac-screen-share';
|
||||||
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
|
@ -15,6 +17,14 @@ import {
|
||||||
import { strictAssert } from './assert';
|
import { strictAssert } from './assert';
|
||||||
import { explodePromise } from './explodePromise';
|
import { explodePromise } from './explodePromise';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
|
import { drop } from './drop';
|
||||||
|
|
||||||
|
// Chrome-only API for now, thus a declaration:
|
||||||
|
declare class MediaStreamTrackGenerator extends MediaStreamTrack {
|
||||||
|
constructor(options: { kind: 'video' });
|
||||||
|
|
||||||
|
public writable: WritableStream;
|
||||||
|
}
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
RequestingMedia = 'RequestingMedia',
|
RequestingMedia = 'RequestingMedia',
|
||||||
|
@ -24,6 +34,9 @@ enum Step {
|
||||||
// Skipped on macOS Sequoia
|
// Skipped on macOS Sequoia
|
||||||
SelectingSource = 'SelectingSource',
|
SelectingSource = 'SelectingSource',
|
||||||
SelectedSource = 'SelectedSource',
|
SelectedSource = 'SelectedSource',
|
||||||
|
|
||||||
|
// macOS Sequoia
|
||||||
|
NativeMacOS = 'NativeMacOS',
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = Readonly<
|
type State = Readonly<
|
||||||
|
@ -41,6 +54,10 @@ type State = Readonly<
|
||||||
step: Step.SelectedSource;
|
step: Step.SelectedSource;
|
||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
step: Step.NativeMacOS;
|
||||||
|
stream: macScreenShare.Stream;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
step: Step.Done;
|
step: Step.Done;
|
||||||
}
|
}
|
||||||
|
@ -80,10 +97,29 @@ export class DesktopCapturer {
|
||||||
DesktopCapturer.initialize();
|
DesktopCapturer.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = { step: Step.RequestingMedia, promise: this.getStream() };
|
if (macScreenShare.isSupported) {
|
||||||
|
this.state = {
|
||||||
|
step: Step.NativeMacOS,
|
||||||
|
stream: this.getNativeMacOSStream(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.state = { step: Step.RequestingMedia, promise: this.getStream() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectSource(id: string | undefined): void {
|
public abort(): void {
|
||||||
|
if (this.state.step === Step.NativeMacOS) {
|
||||||
|
this.state.stream.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.step === Step.SelectingSource) {
|
||||||
|
this.state.onSource(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = { step: Step.Error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectSource(id: string): void {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
this.state.step === Step.SelectingSource,
|
this.state.step === Step.SelectingSource,
|
||||||
`Invalid state in "selectSource" ${this.state.step}`
|
`Invalid state in "selectSource" ${this.state.step}`
|
||||||
|
@ -177,6 +213,59 @@ export class DesktopCapturer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNativeMacOSStream(): macScreenShare.Stream {
|
||||||
|
const track = new MediaStreamTrackGenerator({ kind: 'video' });
|
||||||
|
const writer = track.writable.getWriter();
|
||||||
|
|
||||||
|
const mediaStream = new MediaStream();
|
||||||
|
mediaStream.addTrack(track);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
const stream = new macScreenShare.Stream({
|
||||||
|
width: REQUESTED_VIDEO_WIDTH,
|
||||||
|
height: REQUESTED_VIDEO_HEIGHT,
|
||||||
|
frameRate: REQUESTED_VIDEO_FRAMERATE,
|
||||||
|
|
||||||
|
onStart: () => {
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
this.options.onMediaStream(mediaStream);
|
||||||
|
},
|
||||||
|
onStop() {
|
||||||
|
if (!isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning = false;
|
||||||
|
|
||||||
|
if (track.readyState === 'ended') {
|
||||||
|
stream.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(writer.close());
|
||||||
|
},
|
||||||
|
onFrame(frame, width, height) {
|
||||||
|
if (!isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (track.readyState === 'ended') {
|
||||||
|
stream.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoFrame = new VideoFrame(frame, {
|
||||||
|
format: 'NV12',
|
||||||
|
codedWidth: width,
|
||||||
|
codedHeight: height,
|
||||||
|
timestamp: 0,
|
||||||
|
});
|
||||||
|
drop(writer.write(videoFrame));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
private translateSourceName(source: DesktopCapturerSource): string {
|
private translateSourceName(source: DesktopCapturerSource): string {
|
||||||
const { i18n } = this.options;
|
const { i18n } = this.options;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue