Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VCam custom sources #5220

Open
wants to merge 14 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 134 additions & 4 deletions app/components/windows/settings/VirtualWebcamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,50 @@ 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({})
export default class AppearanceSettings extends Vue {
@Component({
components: {
Multiselect
},
})
export default class VirtualCamSettings 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;

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);

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() {
Expand Down Expand Up @@ -154,24 +189,119 @@ export default class AppearanceSettings 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,
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 (
<div>
<div class="section">
<div class="section-content">
<b>{$t('This is an experimental feature.')}</b>
<p>
{$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.',
)}
</p>
</div>
</div>

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

<div class="section">
<div class="section-content">
<p>{$t('Output type')}</p>
<Multiselect
value={this.outputTypeValue}
options={this.outputTypeOptions}
trackBy="id"
label="name"
onSelect={this.onOutputTypeChange}
singleSelect={true}
allowEmpty={false}
selectLabel={""}
selectedLabel={""}
deselectLabel={""}
showLabels={false}
searchable={false}
internalSearch={false}
showPointer={true}
/>
</div>

{(this.outputTypeValue.id === VCamOutputType.SceneOutput || this.outputTypeValue.id === VCamOutputType.SourceOutput) &&
<div>
<p style={{ marginTop: '30px' }}>{$t('Output selection')}</p>
<Multiselect
value={this.outputSelectionValue}
options={this.outputSelectionOptions}
trackBy="id"
label="name"
onSelect={this.onOutputSelectionChange}
singleSelect={true}
allowEmpty={false}
selectLabel={""}
selectedLabel={""}
deselectLabel={""}
showLabels={false}
searchable={false}
internalSearch={false}
showPointer={true}
/>
</div>}
</div>
{this.installStatus && this.getSection(this.installStatus)}
{this.installStatus &&
this.installStatus !== EVirtualWebcamPluginInstallStatus.NotPresent &&
this.uninstallSection()}
</div>
);
}
}
}
6 changes: 5 additions & 1 deletion app/i18n/en-US/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -261,5 +264,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"
}
8 changes: 6 additions & 2 deletions app/services/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,6 @@ export interface ISettingsSubCategory {
parameters: TObsFormData;
}

declare type TSettingsFormData = Dictionary<ISettingsSubCategory[]>;

export enum ESettingsCategoryType {
Untabbed = 0,
Tabbed = 1,
Expand Down Expand Up @@ -242,6 +240,10 @@ class SettingsViews extends ViewHandler<ISettingsServiceState> {

return null;
}

get virtualWebcamSettings() {
return this.state['Virtual Webcam'].formData;
}
}

export class SettingsService extends StatefulService<ISettingsServiceState> {
Expand Down Expand Up @@ -397,6 +399,8 @@ export class SettingsService extends StatefulService<ISettingsServiceState> {
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',
Expand Down
4 changes: 2 additions & 2 deletions app/services/settings/streaming/stream-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,7 +175,7 @@ export class StreamSettingsService extends PersistentStatefulService<IStreamSett

// We need to refresh the data in case there are additional fields
const mustUpdateObsSettings = Object.keys(patch).find(key =>
['platform', 'key', 'server'].includes(key),
['platform', 'key', 'server', 'bearer_token'].includes(key),
);

if (!mustUpdateObsSettings) return;
Expand Down
23 changes: 18 additions & 5 deletions app/services/virtual-webcam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,7 @@ interface IVirtualWebcamServiceState {

export class VirtualWebcamService extends StatefulService<IVirtualWebcamServiceState> {
@Inject() usageStatisticsService: UsageStatisticsService;
@Inject() sourcesService: SourcesService;

static initialState: IVirtualWebcamServiceState = { running: false };

Expand Down Expand Up @@ -88,8 +90,8 @@ export class VirtualWebcamService extends StatefulService<IVirtualWebcamServiceS
start() {
if (this.state.running) return;

obs.NodeObs.OBS_service_createVirtualWebcam('Streamlabs Desktop Virtual Webcam');
obs.NodeObs.OBS_service_startVirtualWebcam();
//obs.NodeObs.OBS_service_createVirtualWebcam('Streamlabs Desktop Virtual Webcam');
obs.NodeObs.OBS_service_startVirtualCam();

this.SET_RUNNING(true);
this.runningChanged.next(true);
Expand All @@ -100,8 +102,8 @@ export class VirtualWebcamService extends StatefulService<IVirtualWebcamServiceS
stop() {
if (!this.state.running) return;

obs.NodeObs.OBS_service_stopVirtualWebcam();
obs.NodeObs.OBS_service_removeVirtualWebcam();
obs.NodeObs.OBS_service_stopVirtualCam();
//obs.NodeObs.OBS_service_removeVirtualWebcam();

this.SET_RUNNING(false);
this.runningChanged.next(false);
Expand All @@ -112,6 +114,17 @@ export class VirtualWebcamService extends StatefulService<IVirtualWebcamServiceS
return getChecksum(internalPlistPath);
}

update(type: VCamOutputType, name: string) {
obs.NodeObs.OBS_service_updateVirtualCam(type, name);
}

getVideoSources() {
return this.sourcesService.views.sources.filter(
source =>
source.type !== 'scene' && source.getObsInput().outputFlags & ESourceOutputFlags.Video,
);
}

@mutation()
private SET_RUNNING(running: boolean) {
this.state.running = running;
Expand Down
4 changes: 2 additions & 2 deletions scripts/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "0.25.3",
"mac_version": "0.25.3",
"version": "0.25.8",
"mac_version": "0.25.7",
"win64": true,
"osx": true
},
Expand Down
Loading