Skip to content

Commit

Permalink
Collab cam desktop guest (stream-labs#4237)
Browse files Browse the repository at this point in the history
* protocol link join

* changes

* disonnect all guests

* fix mutation and reset socket connection

* guest join modal

* add translations

* hide pieces of guest UI

* fix race conditions during startup

* add icon to source selector

* do nothing if no source

* allow disconnect and add indication that user is in guest state

* fix eslint
  • Loading branch information
avacreeth authored Jul 22, 2022
1 parent e5c336a commit ce69114
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 85 deletions.
13 changes: 13 additions & 0 deletions app/components-react/editor/elements/SourceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import styles from './SceneSelector.m.less';
import Scrollable from 'components-react/shared/Scrollable';
import HelpTip from 'components-react/shared/HelpTip';
import Translate from 'components-react/shared/Translate';
import { GuestCamService } from 'app-services';

interface ISourceMetadata {
id: string;
Expand All @@ -30,6 +31,7 @@ interface ISourceMetadata {
isLocked: boolean;
isStreamVisible: boolean;
isRecordingVisible: boolean;
isGuestCamActive: boolean;
isFolder: boolean;
canShowActions: boolean;
parentId?: string;
Expand All @@ -42,6 +44,7 @@ class SourceSelectorModule {
private editorCommandsService = inject(EditorCommandsService);
private streamingService = inject(StreamingService);
private audioService = inject(AudioService);
private guestCamService = inject(GuestCamService);

sourcesTooltip = $t('The building blocks of your scene. Also contains widgets.');
addSourceTooltip = $t('Add a new Source to your Scene. Includes widgets.');
Expand Down Expand Up @@ -81,6 +84,7 @@ class SourceSelectorModule {
selectiveRecordingEnabled={this.selectiveRecordingEnabled}
isStreamVisible={sceneNode.isStreamVisible}
isRecordingVisible={sceneNode.isRecordingVisible}
isGuestCamActive={sceneNode.isGuestCamActive}
cycleSelectiveRecording={() => this.cycleSelectiveRecording(sceneNode.id)}
ref={this.nodeRefs[sceneNode.id]}
onDoubleClick={() => this.sourceProperties(sceneNode.id)}
Expand All @@ -104,6 +108,12 @@ class SourceSelectorModule {
const isLocked = itemsForNode.every(i => i.locked);
const isRecordingVisible = itemsForNode.every(i => i.recordingVisible);
const isStreamVisible = itemsForNode.every(i => i.streamVisible);
const isGuestCamActive = itemsForNode.some(i => {
return (
this.sourcesService.state.sources[i.sourceId].type === 'mediasoupconnector' &&
this.guestCamService.state.guestInfo
);
});

const isFolder = !isItem(node);
return {
Expand All @@ -114,6 +124,7 @@ class SourceSelectorModule {
isLocked,
isRecordingVisible,
isStreamVisible,
isGuestCamActive,
parentId: node.parentId,
canShowActions: itemsForNode.length > 0,
isFolder,
Expand Down Expand Up @@ -556,6 +567,7 @@ const TreeNode = React.forwardRef(
isStreamVisible: boolean;
isRecordingVisible: boolean;
selectiveRecordingEnabled: boolean;
isGuestCamActive: boolean;
canShowActions: boolean;
toggleVisibility: (ev: unknown) => unknown;
toggleLock: (ev: unknown) => unknown;
Expand Down Expand Up @@ -584,6 +596,7 @@ const TreeNode = React.forwardRef(
<span className={styles.sourceTitle}>{p.title}</span>
{p.canShowActions && (
<>
{p.isGuestCamActive && <i className="fa fa-signal" style={{ color: 'var(--teal)' }} />}
{p.selectiveRecordingEnabled && (
<Tooltip title={selectiveRecordingMetadata().tooltip} placement="left">
<i
Expand Down
126 changes: 92 additions & 34 deletions app/components-react/windows/GuestCamProperties.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { ExclamationCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import * as remote from '@electron/remote';
import { Alert, Button, Modal, Tabs, Tooltip } from 'antd';
import { useVuex } from 'components-react/hooks';
import { Spinner } from 'components-react/pages/Loader';
import { Services } from 'components-react/service-provider';
import Display from 'components-react/shared/Display';
import { ListInput, SliderInput, TextInput } from 'components-react/shared/inputs';
import Form from 'components-react/shared/inputs/Form';
import { ModalLayout } from 'components-react/shared/ModalLayout';
import React, { useMemo, useState } from 'react';
import { EDismissable } from 'services/dismissables';
import { EDeviceType } from 'services/hardware';
import { $t } from 'services/i18n';
import * as remote from '@electron/remote';
import { Spinner } from 'components-react/pages/Loader';
import { byOS, OS } from 'util/operating-systems';
import { TSourceType } from 'services/sources';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { EDismissable } from 'services/dismissables';
import { byOS, OS } from 'util/operating-systems';

export default function GuestCamProperties() {
const {
Expand Down Expand Up @@ -42,6 +42,8 @@ export default function GuestCamProperties() {
guestInfo,
volume,
showFirstTimeModal,
joinAsGuest,
hostName,
} = useVuex(() => ({
produceOk: GuestCamService.state.produceOk,
visible: GuestCamService.views.guestVisible,
Expand All @@ -62,6 +64,8 @@ export default function GuestCamProperties() {
guestInfo: GuestCamService.state.guestInfo,
volume: GuestCamService.views.deflection,
showFirstTimeModal: DismissablesService.views.shouldShow(EDismissable.GuestCamFirstTimeModal),
joinAsGuest: !!GuestCamService.state.joinAsGuestHash,
hostName: GuestCamService.state.hostName,
}));
const [regeneratingLink, setRegeneratingLink] = useState(false);

Expand All @@ -78,6 +82,16 @@ export default function GuestCamProperties() {
EditorCommandsService.actions.executeCommand('SetDeflectionCommand', source.sourceId, val);
}

function getModalContent() {
if (joinAsGuest) {
return <JoinAsGuestModalContent />;
} else if (showFirstTimeModal) {
return <FirstTimeModalContent />;
} else {
return <EveryTimeModalContent />;
}
}

return (
<ModalLayout scrollable>
<Tabs destroyInactiveTabPane={true} defaultActiveKey="guest-settings">
Expand Down Expand Up @@ -138,12 +152,27 @@ export default function GuestCamProperties() {
<h3>{$t('Source: %{sourceName}', { sourceName: source?.name })}</h3>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ flexGrow: 1 }}>
<TextInput
readOnly
value={inviteUrl}
label={$t('Invite URL')}
style={{ width: '100%', margin: '10px 0 20px' }}
/>
<div style={{ height: 32, margin: '10px 0 10px' }}>
{joinAsGuest ? (
<div>
<b>{$t('Connected To Host:')}</b> <span style={{ color: 'var(--title)' }}>{hostName}</span>
<Tooltip
title={$t(
"You are connected as a guest using someone else's invite link. To leave, click the Disconnect button.",
)}
>
<QuestionCircleOutlined style={{ marginLeft: 6 }} />
</Tooltip>
</div>
) : (
<TextInput
readOnly
value={inviteUrl}
label={$t('Invite URL')}
style={{ width: '100%' }}
/>
)}
</div>
<SliderInput
label={$t('Volume')}
value={volume}
Expand All @@ -153,7 +182,7 @@ export default function GuestCamProperties() {
debounce={500}
step={0.01}
tipFormatter={v => `${(v * 100).toFixed(0)}%`}
style={{ width: '100%', margin: '10px 0' }}
style={{ width: '100%', margin: '20px 0' }}
/>
</div>
<div style={{ width: 350, marginLeft: 20 }}>
Expand All @@ -162,33 +191,41 @@ export default function GuestCamProperties() {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
margin: '10px 0 20px',
margin: '10px 0',
height: 32,
}}
>
<Tooltip title={$t('Copied!')} trigger="click">
<Button
onClick={() => remote.clipboard.writeText(inviteUrl)}
style={{ width: 160 }}
>
{$t('Copy Link')}
</Button>
</Tooltip>
<Button
disabled={regeneratingLink}
onClick={regenerateLink}
style={{ width: 160 }}
>
{$t('Generate a new link')}
{regeneratingLink && (
<i className="fa fa-spinner fa-pulse" style={{ marginLeft: 8 }} />
)}
</Button>
{joinAsGuest ? (
<></>
) : (
<>
<Tooltip title={$t('Copied!')} trigger="click">
<Button
onClick={() => remote.clipboard.writeText(inviteUrl)}
style={{ width: 160 }}
>
{$t('Copy Link')}
</Button>
</Tooltip>
<Button
disabled={regeneratingLink}
onClick={regenerateLink}
style={{ width: 160 }}
>
{$t('Generate a new link')}
{regeneratingLink && (
<i className="fa fa-spinner fa-pulse" style={{ marginLeft: 8 }} />
)}
</Button>
</>
)}
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
margin: '20px 0 0',
}}
>
<Button
Expand All @@ -202,7 +239,7 @@ export default function GuestCamProperties() {
<button
className="button button--soft-warning"
style={{ width: 160 }}
disabled={!guestInfo}
disabled={!guestInfo && !joinAsGuest}
onClick={() => GuestCamService.actions.disconnectGuest()}
>
{$t('Disconnect')}
Expand All @@ -225,7 +262,11 @@ export default function GuestCamProperties() {
}}
>
<Spinner />
<div style={{ textAlign: 'center' }}>{$t('Waiting for guest to join')}</div>
<div style={{ textAlign: 'center' }}>
{joinAsGuest
? $t('Waiting for host to begin')
: $t('Waiting for guest to join')}
</div>
</div>
)}
</div>
Expand All @@ -244,7 +285,7 @@ export default function GuestCamProperties() {
}}
onCancel={() => WindowsService.actions.closeChildWindow()}
>
{showFirstTimeModal ? <FirstTimeModalContent /> : <EveryTimeModalContent />}
{getModalContent()}
</Modal>
</ModalLayout>
);
Expand Down Expand Up @@ -297,3 +338,20 @@ function FirstTimeModalContent() {
</>
);
}

function JoinAsGuestModalContent() {
const { GuestCamService } = Services;
const { hostName } = useVuex(() => ({ hostName: GuestCamService.state.hostName }));

return (
<>
<h2>{$t("You're about to join %{name}", { name: hostName })}</h2>
<p>
{$t(
"%{name} has invited you to join their stream. When you're ready to join, click the button below.",
{ name: hostName },
)}
</p>
</>
);
}
7 changes: 6 additions & 1 deletion app/i18n/en-US/guest-cam.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@
"Don't share your invite link with anyone you don't want on your stream. You can invalidate an old link by generating a new one. Do not show this window on stream.": "Don't share your invite link with anyone you don't want on your stream. You can invalidate an old link by generating a new one. Do not show this window on stream.",
"Start Collab Cam": "Start Collab Cam",
"Collab Cam is not yet sending your video and audio to guests. Start Collab Cam?": "Collab Cam is not yet sending your video and audio to guests. Start Collab Cam?",
"Copy Link": "Copy Link"
"Copy Link": "Copy Link",
"You're about to join %{name}": "You're about to join %{name}",
"%{name} has invited you to join their stream. When you're ready to join, click the button below.": "%{name} has invited you to join their stream. When you're ready to join, click the button below.",
"Waiting for host to begin": "Waiting for host to begin",
"Connected To Host:": "Connected To Host:",
"You are connected as a guest using someone else's invite link. To leave, click the Disconnect button.": "You are connected as a guest using someone else's invite link. To leave, click the Disconnect button."
}
Loading

0 comments on commit ce69114

Please sign in to comment.