From fbc0e60b29a76cbd55ac0d2be529b43b038acf1d Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Wed, 19 Mar 2025 14:23:14 +0000 Subject: [PATCH 1/9] Enhanced broadcasting support --- app/components-react/shared/NewBadge.tsx | 6 +++++- .../windows/go-live/platforms/TwitchEditStreamInfo.tsx | 10 ++++++++++ .../windows/go-live/useGoLiveSettings.ts | 6 ++++++ app/i18n/en-US/streaming.json | 4 +++- app/services/dismissables.ts | 1 + app/services/platforms/twitch.ts | 6 ++++++ app/services/settings/settings.ts | 8 ++++++++ 7 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/components-react/shared/NewBadge.tsx b/app/components-react/shared/NewBadge.tsx index 2354eea1f199..58118af59b4d 100644 --- a/app/components-react/shared/NewBadge.tsx +++ b/app/components-react/shared/NewBadge.tsx @@ -7,6 +7,9 @@ import cx from 'classnames'; import { $t } from 'services/i18n'; interface INewButtonProps { + // children is a special React property which allows access any child elements including text + children?: string; + dismissableKey: EDismissable; size?: 'standard' | 'small'; absolute?: boolean; @@ -14,6 +17,7 @@ interface INewButtonProps { } export default function NewButton({ + children = "", dismissableKey, size = 'standard', absolute = false, @@ -37,7 +41,7 @@ export default function NewButton({ )} style={style} > - {$t('New')} + {children.length !== 0 ? $t(children) : $t('New')} ); } diff --git a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx index 8c484cb0bb2a..ce784cea4488 100644 --- a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx @@ -16,6 +16,9 @@ import AiHighlighterToggle from '../AiHighlighterToggle'; import { Services } from 'components-react/service-provider'; import { EAvailableFeatures } from 'services/incremental-rollout'; +import Badge from 'components-react/shared/NewBadge'; +import { EDismissable } from 'services/dismissables'; + export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { const twSettings = p.value; const aiHighlighterFeatureEnabled = Services.IncrementalRolloutService.views.featureIsEnabled( @@ -25,6 +28,7 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { p.onChange({ ...twSettings, ...patch }); } + const enhancedBroadcastingTooltipText = $t('Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup.'); const bind = createBinding(twSettings, updatedSettings => updateSettings(updatedSettings)); const optionalFields = ( @@ -34,6 +38,12 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { + +
+ + Beta +
+
); return ( diff --git a/app/components-react/windows/go-live/useGoLiveSettings.ts b/app/components-react/windows/go-live/useGoLiveSettings.ts index 0bb2133eaabb..c9ba7cbed519 100644 --- a/app/components-react/windows/go-live/useGoLiveSettings.ts +++ b/app/components-react/windows/go-live/useGoLiveSettings.ts @@ -49,6 +49,12 @@ class GoLiveSettingsState extends StreamInfoView { * Update top level settings */ updateSettings(patch: Partial) { + if (patch.platforms?.twitch) { + Services.SettingsService.actions.setEnhancedBroadcasting( + patch.platforms.twitch.isEnhancedBroadcasting, + ); + } + const newSettings = { ...this.state, ...patch }; // we should re-calculate common fields before applying new settings const platforms = this.getViewFromState(newSettings).applyCommonFields(newSettings.platforms); diff --git a/app/i18n/en-US/streaming.json b/app/i18n/en-US/streaming.json index 0d8c57b9106d..741e486fd92d 100644 --- a/app/i18n/en-US/streaming.json +++ b/app/i18n/en-US/streaming.json @@ -288,5 +288,7 @@ "Please try again. If the issue persists, you can stream directly to a single platform instead or click the button below to bypass and go live.": "Please try again. If the issue persists, you can stream directly to a single platform instead or click the button below to bypass and go live.", "Issues": "Issues", "Multistream Error": "Multistream Error", - "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again.": "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again." + "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again.": "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again.", + "Enhanced broadcasting": "Enhanced broadcasting", + "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup.": "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup." } diff --git a/app/services/dismissables.ts b/app/services/dismissables.ts index b232d49e679c..6e8a0b5633e5 100644 --- a/app/services/dismissables.ts +++ b/app/services/dismissables.ts @@ -17,6 +17,7 @@ export enum EDismissable { LoginPrompt = 'login_prompt', TikTokRejected = 'tiktok_rejected', TikTokEligible = 'tiktok_eligible', + EnhancedBroadcasting = 'enhanced_broadcasting', } interface IDismissablesServiceState { diff --git a/app/services/platforms/twitch.ts b/app/services/platforms/twitch.ts index 16925e78ba7e..d269516816e0 100644 --- a/app/services/platforms/twitch.ts +++ b/app/services/platforms/twitch.ts @@ -10,6 +10,7 @@ import { HostsService } from 'services/hosts'; import { Inject } from 'services/core/injector'; import { authorizedHeaders, jfetch } from 'util/requests'; import { UserService } from 'services/user'; +import { SettingsService } from 'services/settings'; import { TTwitchOAuthScope, TwitchTagsService } from './twitch/index'; import { platformAuthorizedRequest } from './utils'; import { CustomizationService } from 'services/customization'; @@ -36,6 +37,7 @@ export interface ITwitchStartStreamOptions { mode?: TOutputOrientation; contentClassificationLabels: string[]; isBrandedContent: boolean; + isEnhancedBroadcasting: boolean; } export interface ITwitchChannelInfo extends ITwitchStartStreamOptions { @@ -81,6 +83,7 @@ export class TwitchService @Inject() twitchTagsService: TwitchTagsService; @Inject() twitchContentClassificationService: TwitchContentClassificationService; @Inject() notificationsService: NotificationsService; + @Inject() settingsService: SettingsService; static initialState: ITwitchServiceState = { ...BasePlatformService.initialState, @@ -94,6 +97,7 @@ export class TwitchService tags: [], contentClassificationLabels: [], isBrandedContent: false, + isEnhancedBroadcasting: false, }, }; @@ -302,11 +306,13 @@ export class TwitchService ? this.twitchTagsService.views.tags : []; this.SET_PREPOPULATED(true); + this.SET_STREAM_SETTINGS({ tags, title: channelInfo.title, game: channelInfo.game, isBrandedContent: channelInfo.is_branded_content, + isEnhancedBroadcasting: this.settingsService.isEnhancedBroadcasting(), contentClassificationLabels: channelInfo.content_classification_labels, }); } diff --git a/app/services/settings/settings.ts b/app/services/settings/settings.ts index 9d3ea243ab98..e8dfb102e7e4 100644 --- a/app/services/settings/settings.ts +++ b/app/services/settings/settings.ts @@ -689,6 +689,14 @@ export class SettingsService extends StatefulService { } } + isEnhancedBroadcasting() { + return obs.NodeObs.OBS_settings_isEnhancedBroadcasting(); + } + + setEnhancedBroadcasting(enable: boolean) { + obs.NodeObs.OBS_settings_setEnhancedBroadcasting(enable); + } + @mutation() SET_SETTINGS(settingsData: ISettingsServiceState) { this.state = Object.assign({}, this.state, settingsData); From 70d24943a279bd1f65138c51af384c230025517d Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Thu, 20 Mar 2025 16:15:08 +0000 Subject: [PATCH 2/9] Fixed available enhanced broadcasting checkbox in advanced mode when multiple platforms enabled --- app/components-react/windows/go-live/PlatformSettings.tsx | 3 +++ .../windows/go-live/platforms/PlatformSettingsLayout.tsx | 1 + .../windows/go-live/platforms/TwitchEditStreamInfo.tsx | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components-react/windows/go-live/PlatformSettings.tsx b/app/components-react/windows/go-live/PlatformSettings.tsx index b77aa55928e0..86b0d2f581aa 100644 --- a/app/components-react/windows/go-live/PlatformSettings.tsx +++ b/app/components-react/windows/go-live/PlatformSettings.tsx @@ -61,6 +61,9 @@ export default function PlatformSettings() { get value() { return getDefined(settings.platforms[platform]); }, + get enabledPlatformsCount() { + return enabledPlatforms.length; + }, onChange(newSettings) { updatePlatform(platform, newSettings); }, diff --git a/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx b/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx index 0efbbe59e47b..292c2f78c00e 100644 --- a/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx +++ b/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx @@ -46,4 +46,5 @@ export interface IPlatformComponentParams { layoutMode: TLayoutMode; isUpdateMode?: boolean; isScheduleMode?: boolean; + enabledPlatformsCount?: number; } diff --git a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx index ce784cea4488..8f8bc0b1727e 100644 --- a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx @@ -38,12 +38,12 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { - + {p.enabledPlatformsCount === 1 &&
Beta
-
+
} ); return ( From f2421763dab84a073c9e224d74fc668d8e9e70a8 Mon Sep 17 00:00:00 2001 From: mhoyer-streamlabs Date: Fri, 21 Mar 2025 16:22:11 -0500 Subject: [PATCH 3/9] Use default audio device when configured device is missing. (#5333) Replace missing configured audio input device with the default. If an audio input capture source is configured with a device that becomes unavailable, replace it with the default audio input device when loading sources. Remove some unnecessary code. Default is always available so process accordingly. Revert "Remove some unnecessary code." This reverts commit 7b60ef086d30a12b533f553549c5b646cb64bfa5. Remove some unneccesary code. Default is always available so process accordingly. --- app/services/sources/sources.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/services/sources/sources.ts b/app/services/sources/sources.ts index 1efbe9e043ea..ee5174b4570f 100644 --- a/app/services/sources/sources.ts +++ b/app/services/sources/sources.ts @@ -444,8 +444,14 @@ export class SourcesService extends StatefulService { const deviceOption = (deviceProp as IObsListInput).options.find( opt => opt.value === deviceProp.value, ); - - if (deviceOption) { + if (!deviceOption) { + const updateSettings = source.getSettings(); + updateSettings['device_id'] = 'default'; + source.updateSettings(updateSettings); + this.usageStatisticsService.recordAnalyticsEvent('MicrophoneUse', { + device: 'Default', + }); + } else { this.usageStatisticsService.recordAnalyticsEvent('MicrophoneUse', { device: deviceOption.description, }); From 1549625843e560bb22ac1f34a270c187bd3265c1 Mon Sep 17 00:00:00 2001 From: Micheline Wu <69046953+michelinewu@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:00:58 -0400 Subject: [PATCH 4/9] Migrate virtual camera to react with source options. (#5335) * WIP: migrate virtual camera form to react. * WIP: Virtual Webcam component. * Fixes for virtual camera form. --- app/app-services.ts | 2 + .../windows/settings/VirtualWebcam.m.less | 11 + .../windows/settings/VirtualWebcam.tsx | 263 +++++++++++++++ .../windows/settings/pages.ts | 2 +- app/components/windows/settings/Settings.vue | 1 - .../windows/settings/Settings.vue.ts | 4 +- .../settings/VirtualWebcamSettings.m.less | 5 - .../settings/VirtualWebcamSettings.tsx | 307 ------------------ app/i18n/en-US/settings.json | 5 +- app/i18n/en-US/sources.json | 5 +- app/services/audio/audio.ts | 1 - app/services/settings/settings.ts | 5 +- app/services/virtual-webcam.ts | 151 ++++++--- 13 files changed, 400 insertions(+), 362 deletions(-) create mode 100644 app/components-react/windows/settings/VirtualWebcam.m.less create mode 100644 app/components-react/windows/settings/VirtualWebcam.tsx delete mode 100644 app/components/windows/settings/VirtualWebcamSettings.m.less delete mode 100644 app/components/windows/settings/VirtualWebcamSettings.tsx diff --git a/app/app-services.ts b/app/app-services.ts index 5f9777cd7560..368f2c7ff359 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -62,6 +62,7 @@ export { VideoSettingsService } from 'services/settings-v2/video'; export { SettingsManagerService } from 'services/settings-manager'; export { MarkersService } from 'services/markers'; export { RealmService } from 'services/realm'; +import { VirtualWebcamService } from 'services/virtual-webcam'; // ONLINE SERVICES export { UserService } from './services/user'; @@ -291,4 +292,5 @@ export const AppServices = { RealmService, RemoteControlService, UrlService, + VirtualWebcamService, }; diff --git a/app/components-react/windows/settings/VirtualWebcam.m.less b/app/components-react/windows/settings/VirtualWebcam.m.less new file mode 100644 index 000000000000..a1a002902c34 --- /dev/null +++ b/app/components-react/windows/settings/VirtualWebcam.m.less @@ -0,0 +1,11 @@ +@import '../../../styles/index.less'; + +.virtual-webcam { + .running { + color: var(--teal); + } + + .description { + margin-bottom: 16px; + } +} diff --git a/app/components-react/windows/settings/VirtualWebcam.tsx b/app/components-react/windows/settings/VirtualWebcam.tsx new file mode 100644 index 000000000000..bd9d46620f89 --- /dev/null +++ b/app/components-react/windows/settings/VirtualWebcam.tsx @@ -0,0 +1,263 @@ +import React, { useMemo, useEffect } from 'react'; +import { useVuex } from 'components-react/hooks'; +import { getOS, OS } from 'util/operating-systems'; +import { $t } from 'services/i18n'; +import { EVirtualWebcamPluginInstallStatus } from 'services/virtual-webcam'; +import { Services } from 'components-react/service-provider'; +import Translate from 'components-react/shared/Translate'; +import { VCamOutputType } from 'obs-studio-node'; +import { ObsSettingsSection } from './ObsSettings'; +import Form from 'components-react/shared/inputs/Form'; +import { ListInput } from 'components-react/shared/inputs/ListInput'; +import { Button } from 'antd'; +import styles from './VirtualWebcam.m.less'; +import cx from 'classnames'; + +export function VirtualWebcamSettings() { + const { VirtualWebcamService, ScenesService, SourcesService } = Services; + + useEffect(() => { + let mounted = true; + if (mounted) { + VirtualWebcamService.actions.setInstallStatus(); + } + + return () => { + mounted = false; + }; + }, []); + + const v = useVuex(() => ({ + running: VirtualWebcamService.views.running, + outputType: VirtualWebcamService.views.outputType, + outputSelection: VirtualWebcamService.views.outputSelection, + installStatus: VirtualWebcamService.views.installStatus, + update: VirtualWebcamService.actions.update, + })); + + const OUTPUT_TYPE_OPTIONS = [ + { label: $t('Program (default)'), value: VCamOutputType.ProgramView.toString() }, + // {label: $t('Preview'), value: VCamOutputType.PreviewOutput}, // VCam for studio mode, is not implemented right now + { label: $t('Scene'), value: VCamOutputType.SceneOutput.toString() }, + { label: $t('Source'), value: VCamOutputType.SourceOutput.toString() }, + ]; + + const outputSelectionOptions = useMemo(() => { + // set the options based on the selected virtual cam + let options = [{ label: 'None', value: '' }]; + + if (v.outputType === VCamOutputType.SceneOutput.toString()) { + options = ScenesService.views.scenes.map(scene => ({ + label: scene.name, + value: scene.id, + })); + } + + if (v.outputType === VCamOutputType.SourceOutput.toString()) { + options = SourcesService.views + .getSources() + .filter(source => source.type !== 'scene' && source.video) + .map(source => ({ + label: source.name, + value: source.sourceId, + })); + } + + return options; + }, [v.outputType, v.outputSelection]); + + const outputSelectionValue = useMemo(() => { + if (!outputSelectionOptions.length) return { label: 'None', value: '' }; + + const outputSelection = + outputSelectionOptions.find(o => o.value === v.outputSelection) ?? outputSelectionOptions[0]; + + return outputSelection; + }, [v.outputSelection, outputSelectionOptions]); + + const isInstalled = v.installStatus === EVirtualWebcamPluginInstallStatus.Installed; + + const showOutputSelection = + isInstalled && + (v.outputType === VCamOutputType.SceneOutput.toString() || + v.outputType === VCamOutputType.SourceOutput.toString()); + + const showOutputLabel = + v.outputType === VCamOutputType.SceneOutput.toString() + ? $t('Output Scene') + : $t('Output Source'); + + function onSelectType(value: string, label: string) { + v.update((value as unknown) as VCamOutputType, label); + } + + function onSelectSelection(value: string) { + v.update((v.outputType as unknown) as VCamOutputType, value); + } + + return ( +
+ +
+ {$t( + 'Virtual Webcam allows you to display your scenes from Streamlabs Desktop in video conferencing software. Streamlabs Desktop will appear as a Webcam that can be selected in most video conferencing apps.', + )} +
+
+ + {/* MANAGE TARGET */} + {isInstalled && ( + +
+ { + onSelectType(val, opts.labelrender); + }} + allowClear={false} + style={{ width: '100%' }} + /> + +
+ )} + + {showOutputSelection && ( + +
+ + +
+ )} + + {/* START/STOP */} + {isInstalled && } + + {/* MANAGE INSTALLATION */} + {!isInstalled && ( + + )} + {isInstalled && } +
+ ); +} + +function ManageVirtualWebcam(p: { isRunning: boolean }) { + const { VirtualWebcamService } = Services; + + const buttonText = p.isRunning ? $t('Stop Virtual Webcam') : $t('Start Virtual Webcam'); + const statusText = p.isRunning + ? $t('Virtual webcam is Running') + : $t('Virtual webcam is Offline'); + + function handleStartStop() { + if (p.isRunning) { + VirtualWebcamService.actions.stop(); + } else { + VirtualWebcamService.actions.start(); + } + } + + return ( + +
+

+ { + return ( + + {text} + + ); + }, + }} + /> +

+ + {getOS() === OS.Mac && ( +

+ {$t( + 'If the virtual webcam does not appear in other applications, you may need to restart your computer.', + )} +

+ )} +
+
+ ); +} + +function InstallVirtualWebcam(p: { isUpdate: boolean; name: string }) { + const message = p.isUpdate + ? $t( + 'The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.', + ) + : $t('Virtual Webcam requires administrator privileges to be installed on your system.'); + + const buttonText = p.isUpdate ? $t('Update Virtual Webcam') : $t('Install Virtual Webcam'); + + function handleInstall() { + if (p.isUpdate) { + const type = Services.VirtualWebcamService.views.outputType; + + Services.VirtualWebcamService.actions.update((type as unknown) as VCamOutputType, p.name); + } else { + Services.VirtualWebcamService.actions.install(); + } + } + + return ( + +

{message}

+ +
+ ); +} + +function UninstallVirtualWebcam() { + function handleUninstall() { + Services.VirtualWebcamService.actions.uninstall(); + } + + return ( + +

+ {$t('Uninstalling Virtual Webcam will remove it as a device option in other applications.')} +

+ +
+ ); +} + +VirtualWebcamSettings.page = 'Virtual Webcam'; diff --git a/app/components-react/windows/settings/pages.ts b/app/components-react/windows/settings/pages.ts index fe1b336c0c12..0a6cea411361 100644 --- a/app/components-react/windows/settings/pages.ts +++ b/app/components-react/windows/settings/pages.ts @@ -10,7 +10,7 @@ export * from './Advanced'; // 'Notifications', export * from './Appearance'; export * from './RemoteControl'; -// 'VirtualWebcam', +export * from './VirtualWebcam'; export * from './GameOverlay'; export * from './Support'; export * from './Experimental'; diff --git a/app/components/windows/settings/Settings.vue b/app/components/windows/settings/Settings.vue index 7f914bc465f5..f5e91dbefe09 100644 --- a/app/components/windows/settings/Settings.vue +++ b/app/components/windows/settings/Settings.vue @@ -94,7 +94,6 @@ - val.id === outputType); - - if (outputTypeIndex !== -1) { - this.outputTypeValue = this.outputTypeOptions[outputTypeIndex]; - } else { - this.outputTypeValue = this.outputTypeOptions[0]; - this.settingsService.setSettingValue('Virtual Webcam', 'OutputType', VCamOutputType.ProgramView); - } - - this.onOutputTypeChange(this.outputTypeValue); - */ - } - - install() { - // Intentionally synchronous. Call is blocking until user action in the worker - // process, so we don't want the user doing anything else. - this.virtualWebcamService.install(); - this.checkInstalled(); - } - - uninstall() { - // Intentionally synchronous for the same reasons as above. - this.virtualWebcamService.uninstall(); - this.checkInstalled(); - } - - start() { - this.virtualWebcamService.actions.start(); - } - - stop() { - this.virtualWebcamService.actions.stop(); - } - - async checkInstalled() { - this.installStatus = await this.virtualWebcamService.getInstallStatus(); - } - - get running() { - return this.virtualWebcamService.state.running; - } - - needsInstallSection(isUpdate: boolean) { - let message: string; - - // This is an if statement because ESLint literally doesn't know how to format as a ternary - if (isUpdate) { - message = $t( - 'The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.', - ); - } else { - message = $t( - 'Virtual Webcam requires administrator privileges to be installed on your system.', - ); - } - const buttonText = isUpdate ? $t('Update Virtual Webcam') : $t('Install Virtual Webcam'); - - return ( -
-
-

{message}

- -
-
- ); - } - - isInstalledSection() { - const buttonText = this.running ? $t('Stop Virtual Webcam') : $t('Start Virtual Webcam'); - const statusText = this.running - ? $t('Virtual webcam is Running') - : $t('Virtual webcam is Offline'); - - return ( -
-
-

- { - return ( - - {text} - - ); - }, - }} - /> -

- - {getOS() === OS.Mac && ( -

- {$t( - 'If the virtual webcam does not appear in other applications, you may need to restart your computer.', - )} -

- )} -
-
- ); - } - - uninstallSection() { - return ( -
-
-

- {$t( - 'Uninstalling Virtual Webcam will remove it as a device option in other applications.', - )} -

- -
-
- ); - } - - getSection(status: EVirtualWebcamPluginInstallStatus) { - if (status === EVirtualWebcamPluginInstallStatus.NotPresent) { - return this.needsInstallSection(false); - } - if (status === EVirtualWebcamPluginInstallStatus.Outdated) { - return this.needsInstallSection(true); - } - if (status === EVirtualWebcamPluginInstallStatus.Installed) { - return this.isInstalledSection(); - } - } - - onOutputTypeChange(value: {name: string, id: number}) { - this.outputTypeValue = value; - - // TODO: RealmDB settings - - //this.settingsService.setSettingValue('Virtual Webcam', 'OutputType', value.id); - //const settingsOutputSelection: string = this.settingsService.findSettingValue(this.settingsService.views.virtualWebcamSettings, 'OutputSelection', 'OutputSelection'); - - if (value.id == VCamOutputType.SceneOutput) { - const scenes = this.scenesService.views.scenes.map((scene) => ({ - name: scene.name, - id: scene.id, - })); - - this.outputSelectionOptions = scenes; - const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === "" /*settingsOutputSelection*/); - if (outputSelectionIndex !== -1) { - this.outputSelectionValue = scenes[outputSelectionIndex]; - } else { - this.outputSelectionValue = scenes[0]; - } - - //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); - this.virtualWebcamService.update(VCamOutputType.SceneOutput, this.outputSelectionValue.id); - } else if (value.id == VCamOutputType.SourceOutput) { - const sources = this.virtualWebcamService.getVideoSources().map(source => ({name: source.name, id: source.sourceId})); - - this.outputSelectionOptions = sources; - const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === "" /*settingsOutputSelection*/); - if (outputSelectionIndex !== -1) { - this.outputSelectionValue = sources[outputSelectionIndex]; - } else { - this.outputSelectionValue = sources[0]; - } - - //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); - this.virtualWebcamService.update(VCamOutputType.SourceOutput, this.outputSelectionValue.id); - } else { - //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', ""); - this.virtualWebcamService.update(value.id, ""); - } - } - - onOutputSelectionChange(value: {name: string, id: string}) { - this.outputSelectionValue = value; - - // TODO: RealmDB settings - //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); - this.virtualWebcamService.update(this.outputTypeValue.id, this.outputSelectionValue.id); - } - - render() { - return ( -
-
-
-

- {$t( - 'Virtual Webcam allows you to display your scenes from Streamlabs Desktop in video conferencing software. Streamlabs Desktop will appear as a Webcam that can be selected in most video conferencing apps.', - )} -

-
-
- - {/* -- TODO: prettify me!!!! -- */} - -
-
-

{$t('Output type')}

- -
- - {(this.outputTypeValue.id === VCamOutputType.SceneOutput || this.outputTypeValue.id === VCamOutputType.SourceOutput) && -
-

{$t('Output selection')}

- -
} -
- {this.installStatus && this.getSection(this.installStatus)} - {this.installStatus && - this.installStatus !== EVirtualWebcamPluginInstallStatus.NotPresent && - this.uninstallSection()} -
- ); - } -} \ No newline at end of file diff --git a/app/i18n/en-US/settings.json b/app/i18n/en-US/settings.json index d19e6ee290be..182bf7da739b 100644 --- a/app/i18n/en-US/settings.json +++ b/app/i18n/en-US/settings.json @@ -141,8 +141,9 @@ "Virtual Webcam allows you to display your scenes from Streamlabs Desktop in video conferencing software. Streamlabs Desktop will appear as a Webcam that can be selected in most video conferencing apps.": "Virtual Webcam allows you to display your scenes from Streamlabs Desktop in video conferencing software. Streamlabs Desktop will appear as a Webcam that can be selected in most video conferencing apps.", "Uninstalling Virtual Webcam will remove it as a device option in other applications.": "Uninstalling Virtual Webcam will remove it as a device option in other applications.", "Uninstall Virtual Webcam": "Uninstall Virtual Webcam", - "Output type": "Output type", - "Output selection": "Output selection", + "Output Type": "Output Type", + "Output Selection": "Output Selection", + "Output Source": "Output Source", "Program (default)": "Program (default)", "Use custom resolution": "Use custom resolution", "Enable Designer Mode": "Enable Designer Mode", diff --git a/app/i18n/en-US/sources.json b/app/i18n/en-US/sources.json index 93f307082417..989230706693 100644 --- a/app/i18n/en-US/sources.json +++ b/app/i18n/en-US/sources.json @@ -428,5 +428,8 @@ "New Tips": "New Tips", "All-Time Top Tipper": "All-Time Top Tipper", "Weekly Top Tipper": "Weekly Top Tipper", - "And more": "And more" + "And more": "And more", + "The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.": "The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.", + "Virtual camera output selection": "Virtual camera output selection", + "Virtual camera output type": "Virtual camera output type" } diff --git a/app/services/audio/audio.ts b/app/services/audio/audio.ts index 370d0ffee210..03078b167d02 100644 --- a/app/services/audio/audio.ts +++ b/app/services/audio/audio.ts @@ -80,7 +80,6 @@ export class AudioService extends StatefulService { sourceData: Dictionary = {}; @Inject() private sourcesService: SourcesService; - @Inject() private scenesService: ScenesService; @Inject() private windowsService: WindowsService; @Inject() private hardwareService: HardwareService; diff --git a/app/services/settings/settings.ts b/app/services/settings/settings.ts index 6958e4d6ac3e..c584474cc094 100644 --- a/app/services/settings/settings.ts +++ b/app/services/settings/settings.ts @@ -241,10 +241,6 @@ class SettingsViews extends ViewHandler { return null; } - - get virtualWebcamSettings() { - return this.state['Virtual Webcam'].formData; - } } export class SettingsService extends StatefulService { @@ -351,6 +347,7 @@ export class SettingsService extends StatefulService { this.getCategories().forEach((categoryName: keyof ISettingsServiceState) => { settingsFormData[categoryName] = this.fetchSettingsFromObs(categoryName); }); + this.SET_SETTINGS(settingsFormData); } diff --git a/app/services/virtual-webcam.ts b/app/services/virtual-webcam.ts index f985b5699912..00c6ec889c07 100644 --- a/app/services/virtual-webcam.ts +++ b/app/services/virtual-webcam.ts @@ -1,11 +1,11 @@ -import { StatefulService, mutation } from 'services/core'; +import { ExecuteInWorkerProcess, StatefulService, ViewHandler, mutation } from 'services/core'; import * as obs from '../../obs-api'; import fs from 'fs'; -import util from 'util'; import path from 'path'; import { getChecksum } from 'util/requests'; import { byOS, OS } from 'util/operating-systems'; import { Inject } from 'services/core/injector'; +import { SettingsService } from 'services/settings'; import { UsageStatisticsService, SourcesService } from 'app-services'; import * as remote from '@electron/remote'; import { Subject } from 'rxjs'; @@ -23,49 +23,82 @@ export enum EVirtualWebcamPluginInstallStatus { Outdated = 'outdated', } +export type TVirtualWebcamPluginInstallStatus = + | keyof typeof EVirtualWebcamPluginInstallStatus + | null; + interface IVirtualWebcamServiceState { running: boolean; + outputType: VCamOutputType; + outputSelection: string; + installStatus: EVirtualWebcamPluginInstallStatus; } export class VirtualWebcamService extends StatefulService { @Inject() usageStatisticsService: UsageStatisticsService; @Inject() sourcesService: SourcesService; + @Inject() settingsService: SettingsService; - static initialState: IVirtualWebcamServiceState = { running: false }; + static initialState: IVirtualWebcamServiceState = { + running: false, + outputType: VCamOutputType.ProgramView, + outputSelection: '', + installStatus: EVirtualWebcamPluginInstallStatus.NotPresent, + }; runningChanged = new Subject(); + installStatusChanged = new Subject(); + + protected init(): void { + this.setInstallStatus(); + } + + get views() { + return new VirtualWebcamViews(this.state); + } - getInstallStatus(): Promise { + /** + * Set the virtual camera install status + * @remark This method wraps getting the install status in a try/catch block + * to prevent infinite loading from errors + */ + @ExecuteInWorkerProcess() + setInstallStatus() { + try { + const installStatus = this.getInstallStatus(); + this.SET_INSTALL_STATUS(installStatus); + } catch (error: unknown) { + console.error('Error resolving install status:', error); + this.SET_INSTALL_STATUS(EVirtualWebcamPluginInstallStatus.NotPresent); + } + + this.installStatusChanged.next(this.state.installStatus); + } + + @ExecuteInWorkerProcess() + getInstallStatus(): EVirtualWebcamPluginInstallStatus { return byOS({ - [OS.Mac]: async () => { - return util - .promisify(fs.exists)(PLUGIN_PLIST_PATH) - .then(async exists => { - if (exists) { - try { - const latest = await this.getCurrentChecksum(); - const installed = await getChecksum(PLUGIN_PLIST_PATH); - - if (latest === installed) { - return EVirtualWebcamPluginInstallStatus.Installed; - } - - return EVirtualWebcamPluginInstallStatus.Outdated; - } catch (e: unknown) { - console.error('Error comparing checksums on virtual webcam', e); - // Assume outdated - return EVirtualWebcamPluginInstallStatus.Outdated; - } + [OS.Mac]: () => { + try { + const exists = fs.existsSync(PLUGIN_PLIST_PATH); + if (exists) { + const latest = this.getCurrentChecksum(); + const installed = getChecksum(PLUGIN_PLIST_PATH); + + if (latest === installed) { + return EVirtualWebcamPluginInstallStatus.Installed; } - return EVirtualWebcamPluginInstallStatus.NotPresent; - }) - .catch(e => { - console.error('Error checking for presence of virtual webcam', e); - return EVirtualWebcamPluginInstallStatus.NotPresent; - }); + return EVirtualWebcamPluginInstallStatus.Outdated; + } + + return EVirtualWebcamPluginInstallStatus.NotPresent; + } catch (e: unknown) { + console.error('Error comparing checksums on virtual webcam', e); + return EVirtualWebcamPluginInstallStatus.Outdated; + } }, - [OS.Windows]: async () => { + [OS.Windows]: () => { const result = obs.NodeObs.OBS_service_isVirtualCamPluginInstalled(); if (result === obs.EVcamInstalledStatus.Installed) { @@ -79,14 +112,25 @@ export class VirtualWebcamService extends StatefulService - source.type !== 'scene' && source.getObsInput().outputFlags & ESourceOutputFlags.Video, - ); + const outputSelection = type === VCamOutputType.ProgramView ? '' : name; + + if (type !== this.state.outputType) { + this.SET_OUTPUT_TYPE(type); + this.SET_OUTPUT_SELECTION(outputSelection); + } } @mutation() private SET_RUNNING(running: boolean) { this.state.running = running; } + + @mutation() + private SET_OUTPUT_TYPE(type: VCamOutputType) { + this.state.outputType = type; + } + + @mutation() + private SET_OUTPUT_SELECTION(selection: string) { + this.state.outputSelection = selection; + } + + @mutation() + private SET_INSTALL_STATUS(installStatus: EVirtualWebcamPluginInstallStatus) { + this.state.installStatus = installStatus; + } +} +class VirtualWebcamViews extends ViewHandler { + get running() { + return this.state.running; + } + + get outputType() { + return this.state.outputType.toString(); + } + + get outputSelection() { + return this.state.outputSelection; + } + + get installStatus(): EVirtualWebcamPluginInstallStatus { + return this.state.installStatus; + } } From 8b94ab086cfbc8d2b961c3a540bd5c4ed1dcb47b Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Mon, 24 Mar 2025 13:30:47 +0000 Subject: [PATCH 5/9] Fixed translations JSON --- app/i18n/en-US/streaming.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/en-US/streaming.json b/app/i18n/en-US/streaming.json index d9be057fd9ce..693dcd427aea 100644 --- a/app/i18n/en-US/streaming.json +++ b/app/i18n/en-US/streaming.json @@ -290,7 +290,7 @@ "Multistream Error": "Multistream Error", "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again.": "One of destinations might have incomplete permissions. Reconnect the destinations in settings and try again.", "Enhanced broadcasting": "Enhanced broadcasting", - "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup.": "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup." + "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup.": "Enhanced broadcasting automatically optimizes your settings to encode and send multiple video qualities to Twitch. Selecting this option will send basic information about your computer and software setup.", "Failed Checks": "Failed Checks", "Video Transmission": "Video Transmission", "Optimized Settings": "Optimized Settings", From ef570be3bfb79b5e472138ca7f0c1c4aca6fb979 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 24 Mar 2025 11:51:53 -0500 Subject: [PATCH 6/9] Check Replay buffer enabled flag (#5334) * Check Replay buffer enabled flag Updated to check to see if the ReplayBuffer is enabled under General settings tab before starting a replay buffer on the obs service. * Fix linting error --------- Co-authored-by: Sean Beyer --- app/services/streaming/streaming.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/services/streaming/streaming.ts b/app/services/streaming/streaming.ts index f8bfbb3a9476..ab7355f2671d 100644 --- a/app/services/streaming/streaming.ts +++ b/app/services/streaming/streaming.ts @@ -953,8 +953,13 @@ export class StreamingService } const replayWhenStreaming = this.streamSettingsService.settings.replayBufferWhileStreaming; + const isReplayBufferEnabled = this.outputSettingsService.getSettings().replayBuffer.enabled; - if (replayWhenStreaming && this.state.replayBufferStatus === EReplayBufferState.Offline) { + if ( + replayWhenStreaming && + isReplayBufferEnabled && + this.state.replayBufferStatus === EReplayBufferState.Offline + ) { this.startReplayBuffer(); } From 980a0829e3fbd7f8076b0d96b54774c24b36876f Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 25 Mar 2025 14:46:47 +0000 Subject: [PATCH 7/9] Refactored NewBadge to use content instead of children propery --- app/components-react/shared/NewBadge.tsx | 7 +++---- .../windows/go-live/platforms/TwitchEditStreamInfo.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/components-react/shared/NewBadge.tsx b/app/components-react/shared/NewBadge.tsx index 58118af59b4d..fe1921b02365 100644 --- a/app/components-react/shared/NewBadge.tsx +++ b/app/components-react/shared/NewBadge.tsx @@ -7,8 +7,7 @@ import cx from 'classnames'; import { $t } from 'services/i18n'; interface INewButtonProps { - // children is a special React property which allows access any child elements including text - children?: string; + content: string | React.ReactElement; dismissableKey: EDismissable; size?: 'standard' | 'small'; @@ -17,7 +16,7 @@ interface INewButtonProps { } export default function NewButton({ - children = "", + content, dismissableKey, size = 'standard', absolute = false, @@ -41,7 +40,7 @@ export default function NewButton({ )} style={style} > - {children.length !== 0 ? $t(children) : $t('New')} + {typeof content === 'string' ? $t(content) : content} ); } diff --git a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx index 8f8bc0b1727e..694b94a4849e 100644 --- a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx @@ -41,7 +41,7 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { {p.enabledPlatformsCount === 1 &&
- Beta +
} From f19a10a8a53fc5a68f97d8b446dd497ef68e4c25 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 25 Mar 2025 14:58:52 +0000 Subject: [PATCH 8/9] Rename: NewBadge -> DismissableBadge --- app/components-react/index.ts | 4 ++-- .../shared/{NewBadge.m.less => DismissableBadge.m.less} | 2 +- .../shared/{NewBadge.tsx => DismissableBadge.tsx} | 8 ++++---- .../windows/go-live/platforms/TwitchEditStreamInfo.tsx | 2 +- app/components/shared/ReactComponentList.tsx | 4 ++-- app/components/windows/settings/Settings.vue | 2 +- app/components/windows/settings/Settings.vue.ts | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) rename app/components-react/shared/{NewBadge.m.less => DismissableBadge.m.less} (95%) rename app/components-react/shared/{NewBadge.tsx => DismissableBadge.tsx} (87%) diff --git a/app/components-react/index.ts b/app/components-react/index.ts index 39e9f6eafdbd..9e7baaab4b5e 100644 --- a/app/components-react/index.ts +++ b/app/components-react/index.ts @@ -56,7 +56,7 @@ import PlatformAppMainPage from './pages/PlatformAppMainPage'; import PlatformAppPageView from './shared/PlatformAppPageView'; import PlatformAppPopOut from './windows/PlatformAppPopOut'; import RecentEventsWindow from './windows/RecentEvents'; -import NewBadge from './shared/NewBadge'; +import DismissableBadge from './shared/DismissableBadge'; import RecordingHistory from './pages/RecordingHistory'; import UltraIcon from './shared/UltraIcon'; import EditTransform from './windows/EditTransform'; @@ -124,7 +124,7 @@ export const components = { PlatformAppMainPage, PlatformAppPageView, PlatformAppPopOut, - NewBadge, + DismissableBadge, UltraIcon, EditTransform, InstalledApps, diff --git a/app/components-react/shared/NewBadge.m.less b/app/components-react/shared/DismissableBadge.m.less similarity index 95% rename from app/components-react/shared/NewBadge.m.less rename to app/components-react/shared/DismissableBadge.m.less index 06cf975c469c..7a12d0cfb70e 100644 --- a/app/components-react/shared/NewBadge.m.less +++ b/app/components-react/shared/DismissableBadge.m.less @@ -2,7 +2,7 @@ @import '../../styles/mixins'; @import '../../styles/badges'; -.new-badge { +.dismissable-badge { background: var(--new-badge-text); color: var(--titlebar); font-size: 9px; diff --git a/app/components-react/shared/NewBadge.tsx b/app/components-react/shared/DismissableBadge.tsx similarity index 87% rename from app/components-react/shared/NewBadge.tsx rename to app/components-react/shared/DismissableBadge.tsx index fe1921b02365..6947fc827dd3 100644 --- a/app/components-react/shared/NewBadge.tsx +++ b/app/components-react/shared/DismissableBadge.tsx @@ -1,5 +1,5 @@ import React, { CSSProperties } from 'react'; -import styles from './NewBadge.m.less'; +import styles from './DismissableBadge.m.less'; import { EDismissable } from 'services/dismissables'; import { Services } from 'components-react/service-provider'; import { useVuex } from 'components-react/hooks'; @@ -15,8 +15,8 @@ interface INewButtonProps { style?: CSSProperties; } -export default function NewButton({ - content, +export default function DismissableBadge({ + content = 'New', dismissableKey, size = 'standard', absolute = false, @@ -34,7 +34,7 @@ export default function NewButton({
) { diff --git a/app/components/shared/ReactComponentList.tsx b/app/components/shared/ReactComponentList.tsx index 228ca30e2e93..f9820a497aa8 100644 --- a/app/components/shared/ReactComponentList.tsx +++ b/app/components/shared/ReactComponentList.tsx @@ -498,8 +498,8 @@ export class WidgetWindow extends ReactComponent {} }) export class CustomCodeWindow extends ReactComponent {} -@Component({ props: { name: { default: 'NewBadge' } } }) -export class NewBadge extends ReactComponent {} +@Component({ props: { name: { default: 'DismissableBadge' } } }) +export class DismissableBadge extends ReactComponent {} @Component({ props: { name: { default: 'UltraIcon' } } }) export class UltraIcon extends ReactComponent<{ type?: string; diff --git a/app/components/windows/settings/Settings.vue b/app/components/windows/settings/Settings.vue index f5e91dbefe09..760d8113c4e6 100644 --- a/app/components/windows/settings/Settings.vue +++ b/app/components/windows/settings/Settings.vue @@ -34,7 +34,7 @@ >
{{ $t(category) }} -