From 62b35a76537883fc1eb2c828df4e76b1d40ff127 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 13 Aug 2024 18:49:41 +0100 Subject: [PATCH 01/11] Using unified volmeters callback instead of individual ones --- app/services/app/app.ts | 1 + app/services/audio/audio.ts | 67 +++++++++++++------------------------ 2 files changed, 25 insertions(+), 43 deletions(-) diff --git a/app/services/app/app.ts b/app/services/app/app.ts index bf2d6dd9493e..58a0fff6cd2f 100644 --- a/app/services/app/app.ts +++ b/app/services/app/app.ts @@ -203,6 +203,7 @@ export class AppService extends StatefulService { await this.gameOverlayService.destroy(); await this.fileManagerService.flushAll(); obs.NodeObs.RemoveSourceCallback(); + obs.NodeObs.RemoveVolmeterCallback(); obs.NodeObs.OBS_service_removeCallback(); obs.IPC.disconnect(); this.crashReporterService.endShutdown(); diff --git a/app/services/audio/audio.ts b/app/services/audio/audio.ts index b35425771499..370d0ffee210 100644 --- a/app/services/audio/audio.ts +++ b/app/services/audio/audio.ts @@ -11,7 +11,6 @@ import { IAudioSource, IAudioSourceApi, IAudioSourcesState, IFader, IVolmeter } import { EDeviceType, HardwareService, IDevice } from 'services/hardware'; import { $t } from 'services/i18n'; import { ipcMain, ipcRenderer } from 'electron'; -import without from 'lodash/without'; import { ViewHandler } from 'services/core'; export enum E_AUDIO_CHANNELS { @@ -25,9 +24,7 @@ export enum E_AUDIO_CHANNELS { interface IAudioSourceData { fader?: obs.IFader; volmeter?: obs.IVolmeter; - callbackInfo?: obs.ICallbackData; stream?: Observable; - timeoutId?: number; isControlledViaObs?: boolean; } @@ -36,6 +33,13 @@ interface IVolmeterMessageChannel { port: MessagePort; } +interface IObsVolmeterCallbackInfo { + sourceName: string; + magnitude: number[]; + peak: number[]; + inputPeak: number[]; +} + class AudioViews extends ViewHandler { get sourcesForCurrentScene(): AudioSource[] { return this.getSourcesForScene(this.getServiceViews(ScenesService).activeSceneId); @@ -85,6 +89,10 @@ export class AudioService extends StatefulService { } protected init() { + obs.NodeObs.RegisterVolmeterCallback((objs: IObsVolmeterCallbackInfo[]) => + this.handleVolmeterCallback(objs), + ); + this.sourcesService.sourceAdded.subscribe(sourceModel => { const source = this.sourcesService.views.getSource(sourceModel.sourceId); if (!source.audio) return; @@ -264,6 +272,19 @@ export class AudioService extends StatefulService { this.audioSourceUpdated.next(this.state.audioSources[sourceId]); } + private handleVolmeterCallback(objs: IObsVolmeterCallbackInfo[]) { + objs.forEach(info => { + const source = this.views.getSource(info.sourceName); + // A source we don't care about + if (!source) { + return; + } + + const volmeter: IVolmeter = info; + this.sendVolmeterData(info.sourceName, volmeter); + }); + } + private createAudioSource(source: Source) { this.sourceData[source.sourceId] = {}; @@ -275,47 +296,9 @@ export class AudioService extends StatefulService { obsFader.attach(source.getObsInput()); this.sourceData[source.sourceId].fader = obsFader; - this.initVolmeterStream(source.sourceId); this.ADD_AUDIO_SOURCE(this.generateAudioSourceData(source.sourceId)); } - private initVolmeterStream(sourceId: string) { - let gotEvent = false; - let lastVolmeterValue: IVolmeter; - this.sourceData[sourceId].callbackInfo = this.sourceData[sourceId].volmeter.addCallback( - (magnitude: number[], peak: number[], inputPeak: number[]) => { - const volmeter: IVolmeter = { magnitude, peak, inputPeak }; - - this.sendVolmeterData(sourceId, volmeter); - lastVolmeterValue = volmeter; - gotEvent = true; - }, - ); - - /* This is useful for media sources since the volmeter will abruptly stop - * sending events in the case of hiding the source. It might be better - * to eventually just hide the mixer item as well though */ - const volmeterCheck = () => { - if (!this.sourceData[sourceId]) return; - - if (!gotEvent && lastVolmeterValue) { - const channelsCount = lastVolmeterValue.peak.length; - const channelsValue = Array(channelsCount).fill(-60); - this.sendVolmeterData(sourceId, { - ...lastVolmeterValue, - magnitude: channelsValue, - peak: channelsValue, - inputPeak: channelsValue, - }); - } - - gotEvent = false; - this.sourceData[sourceId].timeoutId = window.setTimeout(volmeterCheck, 100); - }; - - volmeterCheck(); - } - private sendVolmeterData(sourceId: string, data: IVolmeter) { if (this.volmeterMessageChannels[sourceId]) { this.volmeterMessageChannels[sourceId].forEach(c => c.port.postMessage(data)); @@ -325,10 +308,8 @@ export class AudioService extends StatefulService { private removeAudioSource(sourceId: string) { this.sourceData[sourceId].fader.detach(); this.sourceData[sourceId].fader.destroy(); - this.sourceData[sourceId].volmeter.removeCallback(this.sourceData[sourceId].callbackInfo); this.sourceData[sourceId].volmeter.detach(); this.sourceData[sourceId].volmeter.destroy(); - if (this.sourceData[sourceId].timeoutId) clearTimeout(this.sourceData[sourceId].timeoutId); delete this.sourceData[sourceId]; this.REMOVE_AUDIO_SOURCE(sourceId); } From be7f3edd6cc299be1f8abd8fb80bd7c80db96971 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Fri, 11 Oct 2024 15:48:22 +0100 Subject: [PATCH 02/11] WHIP service support --- app/i18n/en-US/settings.json | 3 ++- app/services/settings/streaming/stream-settings.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/i18n/en-US/settings.json b/app/i18n/en-US/settings.json index 094d817c84eb..38539db5cccc 100644 --- a/app/i18n/en-US/settings.json +++ b/app/i18n/en-US/settings.json @@ -261,5 +261,6 @@ "Idle": "Idle", "Please connect platforms directly from Streamlabs Desktop instead of adding Streamlabs Multistream as a custom destination": "Please connect platforms directly from Streamlabs Desktop instead of adding Streamlabs Multistream as a custom destination", "Audio Encoder": "Audio Encoder", - "Additional Settings": "Additional Settings" + "Additional Settings": "Additional Settings", + "Bearer Token": "Bearer Token" } diff --git a/app/services/settings/streaming/stream-settings.ts b/app/services/settings/streaming/stream-settings.ts index abd15911d18b..72ced4463039 100644 --- a/app/services/settings/streaming/stream-settings.ts +++ b/app/services/settings/streaming/stream-settings.ts @@ -77,7 +77,7 @@ interface IStreamSettings extends IStreamSettingsState { key: string; server: string; service: string; - streamType: 'rtmp_common' | 'rtmp_custom'; + streamType: 'rtmp_common' | 'rtmp_custom' | 'whip_custom'; warnBeforeStartingStream: boolean; recordWhenStreaming: boolean; replayBufferWhileStreaming: boolean; @@ -175,7 +175,7 @@ export class StreamSettingsService extends PersistentStatefulService - ['platform', 'key', 'server'].includes(key), + ['platform', 'key', 'server', 'bearer_token'].includes(key), ); if (!mustUpdateObsSettings) return; From c3a8e3be30938bc4c0b47b8cc67e022609de1437 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Mon, 21 Oct 2024 20:17:06 +0100 Subject: [PATCH 03/11] obs-node version 0.25.0, which includes OBS 30 --- scripts/repositories.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/repositories.json b/scripts/repositories.json index d359c79ae2f2..9b09646670c2 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,7 +4,7 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "0.24.43", + "version": "0.25.0", "mac_version": "0.24.43", "win64": true, "osx": true From eee85e03ddde66db03c2eeefe7dd15bac1f1582c Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 22 Oct 2024 19:23:43 +0100 Subject: [PATCH 04/11] OSN node version 0.25.1 --- scripts/repositories.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/repositories.json b/scripts/repositories.json index 9b09646670c2..9fef422287f4 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,7 +4,7 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "0.25.0", + "version": "0.25.1", "mac_version": "0.24.43", "win64": true, "osx": true From 55bd681c1988eb4f410d697f8569bcdca673b090 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Mon, 18 Nov 2024 19:11:08 +0000 Subject: [PATCH 05/11] Implemented custom sources for virtual camera --- .../settings/VirtualWebcamSettings.tsx | 132 +++++++++++++++++- app/i18n/en-US/settings.json | 3 + app/services/settings/settings.ts | 12 +- app/services/virtual-webcam.ts | 23 ++- 4 files changed, 160 insertions(+), 10 deletions(-) diff --git a/app/components/windows/settings/VirtualWebcamSettings.tsx b/app/components/windows/settings/VirtualWebcamSettings.tsx index 88f631c4c584..fa1fd8d17e30 100644 --- a/app/components/windows/settings/VirtualWebcamSettings.tsx +++ b/app/components/windows/settings/VirtualWebcamSettings.tsx @@ -7,15 +7,49 @@ import cx from 'classnames'; import { $t } from 'services/i18n'; import Translate from 'components/shared/translate'; import { getOS, OS } from 'util/operating-systems'; +import { Services } from 'components-react/service-provider'; +import { Multiselect } from 'vue-multiselect'; +import { VCamOutputType } from 'obs-studio-node'; -@Component({}) +@Component({ + components: { + Multiselect + }, +}) export default class AppearanceSettings extends Vue { @Inject() virtualWebcamService: VirtualWebcamService; installStatus: EVirtualWebcamPluginInstallStatus = null; + outputTypeOptions = [ + {name: $t('Program (default)'), id: VCamOutputType.ProgramView}, + // {name: $t('Preview'), id: VCamOutputType.PreviewOutput}, // VCam for studio mode, is not implemented right now + {name: $t('Scene'), id: VCamOutputType.SceneOutput}, + {name: $t('Source'), id: VCamOutputType.SourceOutput} + ]; + outputTypeValue: {name: string, id: VCamOutputType} = this.outputTypeOptions[0]; + + outputSelectionOptions = [{name: "None", id: ""}]; + outputSelectionValue: {name: string, id: string} = this.outputSelectionOptions[0]; + + scenesService = Services.ScenesService; + sourcesService = Services.SourcesService; + settingsService = Services.SettingsService; created() { this.checkInstalled(); + + const outputType: VCamOutputType = this.settingsService.findSettingValue(this.settingsService.views.virtualWebcamSettings, 'OutputType', 'OutputType'); + const outputTypeIndex = this.outputTypeOptions.findIndex(val => 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() { @@ -154,12 +188,59 @@ export default class AppearanceSettings extends Vue { } } + onOutputTypeChange(value: {name: string, id: number}) { + this.outputTypeValue = value; + + 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; + + this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); + this.virtualWebcamService.update(this.outputTypeValue.id, this.outputSelectionValue.id); + } + render() { return (
- {$t('This is an experimental feature.')}

{$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.', @@ -167,6 +248,51 @@ export default class AppearanceSettings extends Vue {

+ + {/* -- 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 && @@ -174,4 +300,4 @@ export default class AppearanceSettings extends Vue {
); } -} +} \ No newline at end of file diff --git a/app/i18n/en-US/settings.json b/app/i18n/en-US/settings.json index 38539db5cccc..d19e6ee290be 100644 --- a/app/i18n/en-US/settings.json +++ b/app/i18n/en-US/settings.json @@ -141,6 +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", + "Program (default)": "Program (default)", "Use custom resolution": "Use custom resolution", "Enable Designer Mode": "Enable Designer Mode", "Get Support": "Get Support", diff --git a/app/services/settings/settings.ts b/app/services/settings/settings.ts index 7f67d86f9b18..1d5c63297a41 100644 --- a/app/services/settings/settings.ts +++ b/app/services/settings/settings.ts @@ -124,6 +124,10 @@ export interface ISettingsValues { NewSocketLoopEnable: boolean; LowLatencyEnable: boolean; }; + 'Virtual Webcam': { + OutputType: number; + OutputSelection: string; + }; } export interface ISettingsSubCategory { nameSubCategory: string; @@ -131,8 +135,6 @@ export interface ISettingsSubCategory { parameters: TObsFormData; } -declare type TSettingsFormData = Dictionary; - export enum ESettingsCategoryType { Untabbed = 0, Tabbed = 1, @@ -242,6 +244,10 @@ class SettingsViews extends ViewHandler { return null; } + + get virtualWebcamSettings() { + return this.state['Virtual Webcam'].formData; + } } export class SettingsService extends StatefulService { @@ -397,6 +403,8 @@ export class SettingsService extends StatefulService { let categories: string[] = obs.NodeObs.OBS_settings_getListCategories(); // insert 'Multistreaming' after 'General' categories.splice(1, 0, 'Multistreaming'); + // Deleting 'Virtual Webcam' category to add it below to position properly + categories = categories.filter(category => category !== 'Virtual Webcam'); categories = categories.concat([ 'Scene Collections', 'Notifications', diff --git a/app/services/virtual-webcam.ts b/app/services/virtual-webcam.ts index 5e409c4b868c..f985b5699912 100644 --- a/app/services/virtual-webcam.ts +++ b/app/services/virtual-webcam.ts @@ -6,9 +6,10 @@ import path from 'path'; import { getChecksum } from 'util/requests'; import { byOS, OS } from 'util/operating-systems'; import { Inject } from 'services/core/injector'; -import { UsageStatisticsService } from 'services/usage-statistics'; +import { UsageStatisticsService, SourcesService } from 'app-services'; import * as remote from '@electron/remote'; import { Subject } from 'rxjs'; +import { ESourceOutputFlags, VCamOutputType } from 'obs-studio-node'; const PLUGIN_PLIST_PATH = '/Library/CoreMediaIO/Plug-Ins/DAL/vcam-plugin.plugin/Contents/Info.plist'; @@ -28,6 +29,7 @@ interface IVirtualWebcamServiceState { export class VirtualWebcamService extends StatefulService { @Inject() usageStatisticsService: UsageStatisticsService; + @Inject() sourcesService: SourcesService; static initialState: IVirtualWebcamServiceState = { running: false }; @@ -88,8 +90,8 @@ export class VirtualWebcamService extends StatefulService + source.type !== 'scene' && source.getObsInput().outputFlags & ESourceOutputFlags.Video, + ); + } + @mutation() private SET_RUNNING(running: boolean) { this.state.running = running; From e9e4fffec98f7f266d091f68ac70b29809600822 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 19 Nov 2024 12:48:57 +0000 Subject: [PATCH 06/11] OSN version updated to temporary vcam-custom-sources-1 --- scripts/repositories.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/repositories.json b/scripts/repositories.json index 9fef422287f4..d636f21652ab 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,7 +4,7 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "0.25.1", + "version": "vcam-custom-sources-1", "mac_version": "0.24.43", "win64": true, "osx": true From b15c6b74973911196f5486a8626cdd81fa83457d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 12 Dec 2024 14:03:01 -0800 Subject: [PATCH 07/11] Update repositories.json --- scripts/repositories.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/repositories.json b/scripts/repositories.json index d636f21652ab..d86f74e7dc19 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,8 +4,8 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "vcam-custom-sources-1", - "mac_version": "0.24.43", + "version": "0.25.7", + "mac_version": "0.25.7", "win64": true, "osx": true }, From eccdde734dd10b4e2cc562301d49eb5d9871cc29 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Fri, 20 Dec 2024 18:09:59 +0000 Subject: [PATCH 08/11] Disable node-obs settings --- .../settings/VirtualWebcamSettings.tsx | 26 +++++++++++-------- app/services/settings/settings.ts | 4 --- scripts/repositories.json | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/components/windows/settings/VirtualWebcamSettings.tsx b/app/components/windows/settings/VirtualWebcamSettings.tsx index fa1fd8d17e30..326b9555ac21 100644 --- a/app/components/windows/settings/VirtualWebcamSettings.tsx +++ b/app/components/windows/settings/VirtualWebcamSettings.tsx @@ -16,7 +16,7 @@ import { VCamOutputType } from 'obs-studio-node'; Multiselect }, }) -export default class AppearanceSettings extends Vue { +export default class VirtualCamSettings extends Vue { @Inject() virtualWebcamService: VirtualWebcamService; installStatus: EVirtualWebcamPluginInstallStatus = null; @@ -33,11 +33,12 @@ export default class AppearanceSettings extends Vue { scenesService = Services.ScenesService; sourcesService = Services.SourcesService; - settingsService = Services.SettingsService; created() { this.checkInstalled(); + // TODO: reimplement with settings in RealmDB + /* const outputType: VCamOutputType = this.settingsService.findSettingValue(this.settingsService.views.virtualWebcamSettings, 'OutputType', 'OutputType'); const outputTypeIndex = this.outputTypeOptions.findIndex(val => val.id === outputType); @@ -49,7 +50,7 @@ export default class AppearanceSettings extends Vue { } this.onOutputTypeChange(this.outputTypeValue); - + */ } install() { @@ -191,8 +192,10 @@ export default class AppearanceSettings extends Vue { onOutputTypeChange(value: {name: string, id: number}) { this.outputTypeValue = value; - this.settingsService.setSettingValue('Virtual Webcam', 'OutputType', value.id); - const settingsOutputSelection: string = this.settingsService.findSettingValue(this.settingsService.views.virtualWebcamSettings, 'OutputSelection', 'OutputSelection'); + // 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) => ({ @@ -201,30 +204,30 @@ export default class AppearanceSettings extends Vue { })); this.outputSelectionOptions = scenes; - const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === settingsOutputSelection); + 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.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); + 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.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); this.virtualWebcamService.update(VCamOutputType.SourceOutput, this.outputSelectionValue.id); } else { - this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', ""); + //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', ""); this.virtualWebcamService.update(value.id, ""); } } @@ -232,7 +235,8 @@ export default class AppearanceSettings extends Vue { onOutputSelectionChange(value: {name: string, id: string}) { this.outputSelectionValue = value; - this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); + // TODO: RealmDB settings + //this.settingsService.setSettingValue('Virtual Webcam', 'OutputSelection', this.outputSelectionValue.id); this.virtualWebcamService.update(this.outputTypeValue.id, this.outputSelectionValue.id); } diff --git a/app/services/settings/settings.ts b/app/services/settings/settings.ts index 1d5c63297a41..9d3ea243ab98 100644 --- a/app/services/settings/settings.ts +++ b/app/services/settings/settings.ts @@ -124,10 +124,6 @@ export interface ISettingsValues { NewSocketLoopEnable: boolean; LowLatencyEnable: boolean; }; - 'Virtual Webcam': { - OutputType: number; - OutputSelection: string; - }; } export interface ISettingsSubCategory { nameSubCategory: string; diff --git a/scripts/repositories.json b/scripts/repositories.json index 08b0e3b831ae..65316e274ff9 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,7 +4,7 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "0.25.7", + "version": "vcam-custom-source-2", "mac_version": "0.25.7", "win64": true, "osx": true From 74d27747c4a92a9e82d77baa4d502b1415969703 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Wed, 8 Jan 2025 19:50:46 +0000 Subject: [PATCH 09/11] Migrated to Realm VCam custom source settings --- .../settings/VirtualWebcamSettings.tsx | 26 +++++---------- app/services/virtual-webcam.ts | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/app/components/windows/settings/VirtualWebcamSettings.tsx b/app/components/windows/settings/VirtualWebcamSettings.tsx index 326b9555ac21..16311d28a9b7 100644 --- a/app/components/windows/settings/VirtualWebcamSettings.tsx +++ b/app/components/windows/settings/VirtualWebcamSettings.tsx @@ -37,20 +37,18 @@ export default class VirtualCamSettings extends Vue { created() { this.checkInstalled(); - // TODO: reimplement with settings in RealmDB - /* - const outputType: VCamOutputType = this.settingsService.findSettingValue(this.settingsService.views.virtualWebcamSettings, 'OutputType', 'OutputType'); + console.info("!!!", this.virtualWebcamService.outputType(), this.virtualWebcamService.outputSelection()); + + const outputType: VCamOutputType = this.virtualWebcamService.outputType(); const outputTypeIndex = this.outputTypeOptions.findIndex(val => 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() { @@ -192,11 +190,6 @@ export default class VirtualCamSettings extends Vue { 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, @@ -204,39 +197,33 @@ export default class VirtualCamSettings extends Vue { })); this.outputSelectionOptions = scenes; - const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === "" /*settingsOutputSelection*/); + const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === this.virtualWebcamService.outputSelection()); 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*/); + const outputSelectionIndex = this.outputSelectionOptions.findIndex(val => val.id === this.virtualWebcamService.outputSelection()); 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); } @@ -276,6 +263,9 @@ export default class VirtualCamSettings extends Vue { /> + {(this.outputTypeValue.id === VCamOutputType.ProgramView) && +
} + {(this.outputTypeValue.id === VCamOutputType.SceneOutput || this.outputTypeValue.id === VCamOutputType.SourceOutput) &&

{$t('Output selection')}

diff --git a/app/services/virtual-webcam.ts b/app/services/virtual-webcam.ts index f985b5699912..5a73b55ae582 100644 --- a/app/services/virtual-webcam.ts +++ b/app/services/virtual-webcam.ts @@ -10,6 +10,8 @@ import { UsageStatisticsService, SourcesService } from 'app-services'; import * as remote from '@electron/remote'; import { Subject } from 'rxjs'; import { ESourceOutputFlags, VCamOutputType } from 'obs-studio-node'; +import { RealmObject } from './realm'; +import { ObjectSchema } from 'realm'; const PLUGIN_PLIST_PATH = '/Library/CoreMediaIO/Plug-Ins/DAL/vcam-plugin.plugin/Contents/Info.plist'; @@ -27,9 +29,26 @@ interface IVirtualWebcamServiceState { running: boolean; } +class VirtualCamServicePersistentState extends RealmObject { + // Naming of these fields is taken from OBS for reference + outputType: VCamOutputType; + outputSelection: string; + + static schema: ObjectSchema = { + name: 'VirtualCamServicePersistentState', + properties: { + outputType: { type: 'int', default: VCamOutputType.ProgramView }, + outputSelection: { type: 'string', default: '' }, + }, + }; +} + +VirtualCamServicePersistentState.register({ persist: true }); + export class VirtualWebcamService extends StatefulService { @Inject() usageStatisticsService: UsageStatisticsService; @Inject() sourcesService: SourcesService; + virtualCamSettings = VirtualCamServicePersistentState.inject(); static initialState: IVirtualWebcamServiceState = { running: false }; @@ -115,9 +134,23 @@ export class VirtualWebcamService extends StatefulService { + this.virtualCamSettings.deepPatch({ + outputType: type, + outputSelection: name, + }); + }); obs.NodeObs.OBS_service_updateVirtualCam(type, name); } + outputType(): VCamOutputType { + return this.virtualCamSettings.outputType; + } + + outputSelection(): string { + return this.virtualCamSettings.outputSelection; + } + getVideoSources() { return this.sourcesService.views.sources.filter( source => From f00f63e76d7266e9376720ee2c1403704bcfe4f2 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Wed, 8 Jan 2025 20:00:36 +0000 Subject: [PATCH 10/11] Code cleanup --- app/components/windows/settings/VirtualWebcamSettings.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/components/windows/settings/VirtualWebcamSettings.tsx b/app/components/windows/settings/VirtualWebcamSettings.tsx index 16311d28a9b7..2e2e52a5b896 100644 --- a/app/components/windows/settings/VirtualWebcamSettings.tsx +++ b/app/components/windows/settings/VirtualWebcamSettings.tsx @@ -37,8 +37,6 @@ export default class VirtualCamSettings extends Vue { created() { this.checkInstalled(); - console.info("!!!", this.virtualWebcamService.outputType(), this.virtualWebcamService.outputSelection()); - const outputType: VCamOutputType = this.virtualWebcamService.outputType(); const outputTypeIndex = this.outputTypeOptions.findIndex(val => val.id === outputType); From 0d2323441a5c05c35919074334568899c25263d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Fri, 10 Jan 2025 14:31:46 +0000 Subject: [PATCH 11/11] Got rid of StatefulService in virtual-webcam --- .../settings/VirtualWebcamSettings.tsx | 31 +++++++---- app/services/hotkeys.ts | 4 +- app/services/virtual-webcam.ts | 55 +++++++++++-------- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/app/components/windows/settings/VirtualWebcamSettings.tsx b/app/components/windows/settings/VirtualWebcamSettings.tsx index 2e2e52a5b896..7b0751ebee4f 100644 --- a/app/components/windows/settings/VirtualWebcamSettings.tsx +++ b/app/components/windows/settings/VirtualWebcamSettings.tsx @@ -10,6 +10,8 @@ import { getOS, OS } from 'util/operating-systems'; import { Services } from 'components-react/service-provider'; import { Multiselect } from 'vue-multiselect'; import { VCamOutputType } from 'obs-studio-node'; +import { ObjectChangeSet } from 'realm'; +import { DefaultObject } from 'realm/dist/public-types/schema'; @Component({ components: { @@ -34,11 +36,12 @@ export default class VirtualCamSettings extends Vue { scenesService = Services.ScenesService; sourcesService = Services.SourcesService; + isVCamRunning: boolean = false; + created() { this.checkInstalled(); - const outputType: VCamOutputType = this.virtualWebcamService.outputType(); - const outputTypeIndex = this.outputTypeOptions.findIndex(val => val.id === outputType); + const outputTypeIndex = this.outputTypeOptions.findIndex(val => val.id === this.virtualWebcamService.outputType); if (outputTypeIndex !== -1) { this.outputTypeValue = this.outputTypeOptions[outputTypeIndex]; @@ -47,6 +50,12 @@ export default class VirtualCamSettings extends Vue { } this.onOutputTypeChange(this.outputTypeValue); + + // TODO: after migrating this component to React, the listener should be removed and useRealmObject used instead + const listener = (_o: DefaultObject, changes: ObjectChangeSet) => { + this.isVCamRunning = this.virtualWebcamService.isRunning; + }; + this.virtualWebcamService.ephemeralState.realmModel.addListener(listener); } install() { @@ -75,7 +84,7 @@ export default class VirtualCamSettings extends Vue { } get running() { - return this.virtualWebcamService.state.running; + return this.isVCamRunning; } needsInstallSection(isUpdate: boolean) { @@ -110,8 +119,8 @@ export default class VirtualCamSettings extends Vue { } isInstalledSection() { - const buttonText = this.running ? $t('Stop Virtual Webcam') : $t('Start Virtual Webcam'); - const statusText = this.running + const buttonText = this.isVCamRunning ? $t('Stop Virtual Webcam') : $t('Start Virtual Webcam'); + const statusText = this.isVCamRunning ? $t('Virtual webcam is Running') : $t('Virtual webcam is Offline'); @@ -124,7 +133,7 @@ export default class VirtualCamSettings extends Vue { scopedSlots={{ status: (text: string) => { return ( - + {text} ); @@ -133,9 +142,9 @@ export default class VirtualCamSettings extends Vue { />

@@ -163,7 +172,7 @@ export default class VirtualCamSettings extends Vue {