diff --git a/app/app-services.ts b/app/app-services.ts index 40d44ec8e35f..368f2c7ff359 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -58,6 +58,7 @@ export { HighlighterService } from 'services/highlighter'; export { DiagnosticsService } from 'services/diagnostics'; export { RecordingModeService } from 'services/recording-mode'; export { SideNavService } from 'services/side-nav'; +export { VideoSettingsService } from 'services/settings-v2/video'; export { SettingsManagerService } from 'services/settings-manager'; export { MarkersService } from 'services/markers'; export { RealmService } from 'services/realm'; @@ -199,6 +200,7 @@ import { PlatformAppStoreService } from 'services/platform-app-store'; import { GameOverlayService } from 'services/game-overlay'; import { GuestCamService } from 'services/guest-cam'; import { SideNavService } from './services/side-nav'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { SettingsManagerService } from 'services/settings-manager'; import { DualOutputService } from 'services/dual-output'; import { MarkersService } from 'services/markers'; @@ -282,6 +284,7 @@ export const AppServices = { GuestCamService, HardwareService, SideNavService, + VideoSettingsService, SettingsManagerService, DualOutputService, MarkersService, diff --git a/app/components-react/root/StudioEditor.tsx b/app/components-react/root/StudioEditor.tsx index 5cb8eba195d0..e871017728d5 100644 --- a/app/components-react/root/StudioEditor.tsx +++ b/app/components-react/root/StudioEditor.tsx @@ -6,7 +6,7 @@ import cx from 'classnames'; import Display from 'components-react/shared/Display'; import { $t } from 'services/i18n'; import { ERenderingMode } from '../../../obs-api'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import AutoProgressBar from 'components-react/shared/AutoProgressBar'; import { useSubscription } from 'components-react/hooks/useSubscription'; import { message } from 'antd'; diff --git a/app/components-react/shared/Display.tsx b/app/components-react/shared/Display.tsx index 7281ddafdfbc..2f12ffb8cd34 100644 --- a/app/components-react/shared/Display.tsx +++ b/app/components-react/shared/Display.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { useVuex } from '../hooks'; import { Services } from '../service-provider'; import { Display as OBSDisplay } from '../../services/video'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2/video'; import uuid from 'uuid/v4'; import { useRealmObject } from 'components-react/hooks/realm'; interface DisplayProps { @@ -18,7 +18,7 @@ interface DisplayProps { } export default function Display(props: DisplayProps) { - const { CustomizationService, VideoService } = Services; + const { CustomizationService, VideoSettingsService } = Services; const p = { paddingSize: 0, @@ -30,7 +30,7 @@ export default function Display(props: DisplayProps) { }; const v = useVuex(() => { - const videoSettings = VideoService.baseResolutions[p.type]; + const videoSettings = VideoSettingsService.baseResolutions[p.type]; return { baseResolution: `${videoSettings?.baseWidth}x${videoSettings?.baseHeight}`, diff --git a/app/components-react/shared/DisplaySelector.tsx b/app/components-react/shared/DisplaySelector.tsx index dfa6c69d0dc7..8b96378e2c7c 100644 --- a/app/components-react/shared/DisplaySelector.tsx +++ b/app/components-react/shared/DisplaySelector.tsx @@ -1,10 +1,9 @@ import React, { CSSProperties } from 'react'; import { $t } from 'services/i18n'; import { RadioInput } from './inputs'; -import { TDisplayType } from 'services/video'; -import { platformLabels, TPlatform } from 'services/platforms'; +import { TDisplayType } from 'services/settings-v2'; +import { TPlatform } from 'services/platforms'; import { useGoLiveSettings } from 'components-react/windows/go-live/useGoLiveSettings'; -import { ICustomStreamDestination } from 'services/settings/streaming'; interface IDisplaySelectorProps { title: string; @@ -24,9 +23,6 @@ export default function DisplaySelector(p: IDisplaySelectorProps) { } = useGoLiveSettings(); const setting = p.platform ? platforms[p.platform] : customDestinations[p.index]; - const label = p.platform - ? platformLabels(p.platform) - : (setting as ICustomStreamDestination).name; const displays = [ { @@ -44,7 +40,7 @@ export default function DisplaySelector(p: IDisplaySelectorProps) { data-test="display-input" id={`${p.platform}-display-input`} direction="horizontal" - label={label} + label={p.label} labelAlign="left" labelCol={{ offset: 0 }} colon diff --git a/app/components-react/windows/go-live/useGoLiveSettings.ts b/app/components-react/windows/go-live/useGoLiveSettings.ts index 4681bf93aee7..290d3c19fbe4 100644 --- a/app/components-react/windows/go-live/useGoLiveSettings.ts +++ b/app/components-react/windows/go-live/useGoLiveSettings.ts @@ -11,7 +11,7 @@ import { injectState, useModule } from 'slap'; import { useForm } from '../../shared/inputs/Form'; import { getDefined } from '../../../util/properties-type-guards'; import isEqual from 'lodash/isEqual'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; type TCommonFieldName = 'title' | 'description'; diff --git a/app/components-react/windows/settings/HotkeyGroup.tsx b/app/components-react/windows/settings/HotkeyGroup.tsx index a087d7523b8a..073f800902b4 100644 --- a/app/components-react/windows/settings/HotkeyGroup.tsx +++ b/app/components-react/windows/settings/HotkeyGroup.tsx @@ -3,7 +3,7 @@ import { Collapse } from 'antd'; import cx from 'classnames'; import { IHotkey } from 'services/hotkeys'; import Hotkey from './Hotkey'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import Tabs from 'components-react/shared/Tabs'; import { $t } from 'services/i18n'; diff --git a/app/components-react/windows/settings/Video.tsx b/app/components-react/windows/settings/Video.tsx index a0ab917b50c5..b31ca2facabb 100644 --- a/app/components-react/windows/settings/Video.tsx +++ b/app/components-react/windows/settings/Video.tsx @@ -1,5 +1,6 @@ import * as remote from '@electron/remote'; -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; +import { useModule, injectState } from 'slap'; import { Services } from '../../service-provider'; import { message } from 'antd'; import FormFactory, { TInputValue } from 'components-react/shared/inputs/FormFactory'; @@ -7,13 +8,10 @@ import { EScaleType, EFPSType, IVideoInfo } from '../../../../obs-api'; import { $t } from 'services/i18n'; import styles from './Common.m.less'; import Tabs from 'components-react/shared/Tabs'; -import { invalidFps, IVideoInfoValue, TDisplayType } from 'services/video'; +import { invalidFps, IVideoInfoValue, TDisplayType } from 'services/settings-v2/video'; import { AuthModal } from 'components-react/shared/AuthModal'; import Utils from 'services/utils'; import DualOutputToggle from '../../shared/DualOutputToggle'; -import { ObsSettingsSection } from './ObsSettings'; -import { useRealmObject } from 'components-react/hooks/realm'; -import uniq from 'lodash/uniq'; const CANVAS_RES_OPTIONS = [ { label: '1920x1080', value: '1920x1080' }, @@ -56,65 +54,203 @@ const FPS_OPTIONS = [ { label: '60', value: '60-1' }, ]; -export function VideoSettings() { - const { - DualOutputService, - StreamingService, - WindowsService, - TransitionsService, - UserService, - UsageStatisticsService, - OnboardingService, - SettingsService, - TikTokService, - VideoService, - } = Services; - - const videoSettings = useRealmObject(Services.VideoService.state); - const dualOutputMode = DualOutputService.views.dualOutputMode; - const cantEditFields = StreamingService.views.isStreaming || StreamingService.views.isRecording; - - const [display, setDisplay] = useState('horizontal'); - const [showModal, setShowModal] = useState(false); - const [baseRes, setBaseRes] = useState(videoSettings[display].baseRes); - const [customBaseRes, setCustomBaseRes] = useState(videoSettings[display].baseRes); - const [outputRes, setOutputRes] = useState(videoSettings[display].outputRes); - const [customOutputRes, setCustomOutputRes] = useState(videoSettings[display].outputRes); - const [fpsType, setFPSType] = useState(videoSettings[display].values.fpsType); - - useEffect(() => { - const baseRes = !baseResOptions.find(opt => opt.value === videoSettings[display].baseRes) - ? 'custom' - : videoSettings[display].baseRes; - const outputRes = !outputResOptions.find(opt => opt.value === videoSettings[display].outputRes) - ? 'custom' - : videoSettings[display].outputRes; - setBaseRes(baseRes); - setCustomBaseRes(videoSettings[display].baseRes); - setOutputRes(outputRes); - setCustomOutputRes(videoSettings[display].outputRes); - setFPSType(videoSettings[display].values.fpsType); - }, [display]); - - const values: Dictionary = { - ...videoSettings[display].values, - baseRes, - outputRes, - customBaseRes, - customOutputRes, - fpsType, - }; - - const resolutionValidator = { - message: $t('The resolution must be in the format [width]x[height] (i.e. 1920x1080)'), - pattern: /^[0-9]+x[0-9]+$/, - }; - - const monitorResolutions = useMemo(() => { +class VideoSettingsModule { + service = Services.VideoSettingsService; + userService = Services.UserService; + dualOutputService = Services.DualOutputService; + streamingService = Services.StreamingService; + tiktokService = Services.TikTokService; + + get display(): TDisplayType { + return this.state.display; + } + + get cantEditFields(): boolean { + return this.streamingService.views.isStreaming || this.streamingService.views.isRecording; + } + + get values(): Dictionary { + const display = this.state.display; + const vals = this.service.values[display]; + const baseRes = display !== 'vertical' && this.state?.customBaseRes ? 'custom' : vals.baseRes; + const outputRes = + display !== 'vertical' && this.state?.customOutputRes ? 'custom' : vals.outputRes; + return { + ...vals, + baseRes, + outputRes, + customBaseRes: this.state.customBaseResValue, + customOutputRes: this.state.customOutputResValue, + fpsNum: this.state.fpsNum, + fpsDen: this.state.fpsDen, + fpsInt: this.state.fpsInt, + }; + } + + state = injectState({ + display: 'horizontal' as TDisplayType, + showModal: false, + showDualOutputSettings: this.dualOutputService.views.dualOutputMode, + customBaseRes: !this.baseResOptions.find( + opt => opt.value === this.service.values.horizontal.baseRes, + ), + customOutputRes: !this.outputResOptions.find( + opt => opt.value === this.service.values.horizontal.outputRes, + ), + customBaseResValue: this.service.values.horizontal.baseRes, + customOutputResValue: this.service.values.horizontal.outputRes, + fpsNum: this.service.values.horizontal.fpsNum, + fpsDen: this.service.values.horizontal.fpsDen, + fpsInt: this.service.values.horizontal.fpsNum, + }); + + get metadata() { + return { + baseRes: { + type: 'list', + label: $t('Base (Canvas) Resolution'), + options: this.baseResOptions, + onChange: (val: string) => this.selectResolution('baseRes', val), + disabled: this.cantEditFields, + children: { + customBaseRes: { + type: 'text', + label: $t('Custom Base Resolution'), + rules: [this.resolutionValidator], + onChange: (val: string) => this.setResolution('baseRes', val), + displayed: this.state.customBaseRes, + disabled: this.cantEditFields, + }, + }, + }, + outputRes: { + type: 'list', + label: $t('Output (Scaled) Resolution'), + options: this.outputResOptions, + onChange: (val: string) => this.selectResolution('outputRes', val), + disabled: this.cantEditFields, + children: { + customOutputRes: { + type: 'text', + label: $t('Custom Output Resolution'), + rules: [this.resolutionValidator], + onChange: (val: string) => this.setResolution('outputRes', val), + displayed: this.state.customOutputRes, + disabled: this.cantEditFields, + }, + }, + }, + scaleType: { + type: 'list', + label: $t('Downscale Filter'), + onChange: (val: EScaleType) => this.setScaleType(val), + options: [ + { + label: $t('Bilinear (Fastest, but blurry if scaling)'), + value: EScaleType.Bilinear, + }, + { label: $t('Bicubic (Sharpened scaling, 16 samples)'), value: EScaleType.Bicubic }, + { label: $t('Lanczos (Sharpened scaling, 32 samples)'), value: EScaleType.Lanczos }, + ], + disabled: this.cantEditFields, + }, + fpsType: { + type: 'list', + label: $t('FPS Type'), + onChange: (val: EFPSType) => this.setFPSType(val), + options: [ + { label: $t('Common FPS Values'), value: EFPSType.Common }, + { label: $t('Integer FPS Values'), value: EFPSType.Integer }, + { label: $t('Fractional FPS Values'), value: EFPSType.Fractional }, + ], + disabled: this.cantEditFields, + + children: { + fpsCom: { + type: 'list', + label: $t('Common FPS Values'), + options: FPS_OPTIONS, + onChange: (val: string) => this.setCommonFPS(val), + displayed: this.values.fpsType === EFPSType.Common, + disabled: this.cantEditFields, + }, + fpsInt: { + type: 'number', + label: $t('FPS Value'), + onChange: (val: string) => this.setIntegerFPS(val), + rules: [{ max: 1000, min: 1, message: $t('FPS Value must be between 1 and 1000') }], + displayed: this.values.fpsType === EFPSType.Integer, + disabled: this.cantEditFields, + }, + fpsNum: { + type: 'number', + label: $t('FPS Numerator'), + onChange: (val: string) => this.setFPS('fpsNum', val), + rules: [ + { validator: this.fpsNumValidator.bind(this) }, + { + min: 1, + message: $t('%{fieldName} must be greater than 0', { + fieldName: $t('FPS Numerator'), + }), + }, + ], + displayed: this.values.fpsType === EFPSType.Fractional, + disabled: this.cantEditFields, + }, + fpsDen: { + type: 'number', + label: $t('FPS Denominator'), + onChange: (val: string) => this.setFPS('fpsDen', val), + rules: [ + { validator: this.fpsDenValidator.bind(this) }, + { + min: 1, + message: $t('%{fieldName} must be greater than 0', { + fieldName: $t('FPS Denominator'), + }), + }, + ], + displayed: this.values.fpsType === EFPSType.Fractional, + disabled: this.cantEditFields, + }, + }, + }, + }; + } + + get baseResOptions() { + if (this.state?.display === 'vertical') { + return VERTICAL_CANVAS_OPTIONS; + } + + return CANVAS_RES_OPTIONS.concat(this.monitorResolutions) + .concat(VERTICAL_CANVAS_OPTIONS) + .concat([{ label: $t('Custom'), value: 'custom' }]); + } + + get outputResOptions() { + if (this.state?.display === 'vertical') { + return VERTICAL_OUTPUT_RES_OPTIONS; + } + + const baseRes = `${this.service.state.horizontal.baseWidth}x${this.service.state.horizontal.baseHeight}`; + if (!OUTPUT_RES_OPTIONS.find(opt => opt.value === baseRes)) { + return [{ label: baseRes, value: baseRes }] + .concat(OUTPUT_RES_OPTIONS) + .concat(VERTICAL_OUTPUT_RES_OPTIONS) + .concat([{ label: $t('Custom'), value: 'custom' }]); + } + return OUTPUT_RES_OPTIONS.concat(VERTICAL_OUTPUT_RES_OPTIONS).concat([ + { label: $t('Custom'), value: 'custom' }, + ]); + } + + get monitorResolutions() { const resOptions: { label: string; value: string }[] = []; const displays = remote.screen.getAllDisplays(); - displays.forEach((monitor: Electron.Display) => { - const size = monitor.size; + displays.forEach(display => { + const size = display.size; const res = `${size.width}x${size.height}`; if ( !resOptions.find(opt => opt.value === res) && @@ -124,94 +260,21 @@ export function VideoSettings() { } }); return resOptions; - }, []); - - const baseResOptions = useMemo(() => { - const options = - display === 'vertical' - ? VERTICAL_CANVAS_OPTIONS - : CANVAS_RES_OPTIONS.concat(monitorResolutions) - .concat(VERTICAL_CANVAS_OPTIONS) - .concat([{ label: $t('Custom'), value: 'custom' }]); - - return uniq(options); - }, [display, monitorResolutions]); - - const outputResOptions = useMemo(() => { - const baseRes = `${videoSettings.horizontal.baseWidth}x${videoSettings.horizontal.baseHeight}`; - - const options = - display === 'vertical' - ? VERTICAL_OUTPUT_RES_OPTIONS - : [{ label: baseRes, value: baseRes }] - .concat(OUTPUT_RES_OPTIONS) - .concat(VERTICAL_OUTPUT_RES_OPTIONS) - .concat([{ label: $t('Custom'), value: 'custom' }]); - - return uniq(options); - }, [display, videoSettings]); - - function updateSettings(key: string, val: string | number | EFPSType | EScaleType) { - if (['baseRes', 'outputRes'].includes(key)) { - const [width, height] = (val as string).split('x'); - - const settings = - key === 'baseRes' - ? { - baseWidth: Number(width), - baseHeight: Number(height), - } - : { - outputWidth: Number(width), - outputHeight: Number(height), - }; - - VideoService.actions.updateVideoSettings(settings, display); - return; - } - - VideoService.actions.updateVideoSettings({ [key]: val }, display); } - function onChange(key: keyof IVideoInfo) { - return (val: IVideoInfoValue) => updateSettings(key, val); + get resolutionValidator() { + return { + message: $t('The resolution must be in the format [width]x[height] (i.e. 1920x1080)'), + pattern: /^[0-9]+x[0-9]+$/, + }; } - function selectResolution(key: string, val: string) { - if (key === 'baseRes') { - setBaseRes(val); - if (val === 'custom') { - setCustomBaseRes(''); - return; - } - } - - if (key === 'outputRes') { - setOutputRes(val); - if (val === 'custom') { - setCustomOutputRes(''); - return; - } - } - - updateSettings(key, val); - } - - function setCustomResolution(key: string, val: string) { - if (key === 'baseRes') { - setCustomBaseRes(val); - } else { - setCustomOutputRes(val); - } - updateSettings(key, val); - } - - function fpsNumValidator(rule: unknown, value: string, callback: Function) { - if (Number(value) / Number(videoSettings[display].video.fpsDen) > 1000) { + fpsNumValidator(rule: unknown, value: string, callback: Function) { + if (Number(value) / Number(this.values.fpsDen) > 1000) { callback( $t( 'This number is too large for a FPS Denominator of %{fpsDen}, please decrease it or increase the Denominator', - { fpsDen: videoSettings[display].video.fpsDen }, + { fpsDen: this.values.fpsDen }, ), ); } else { @@ -219,12 +282,12 @@ export function VideoSettings() { } } - function fpsDenValidator(rule: unknown, value: string, callback: Function) { - if (Number(videoSettings[display].video.fpsNum) / Number(value) < 1) { + fpsDenValidator(rule: unknown, value: string, callback: Function) { + if (Number(this.values.fpsNum) / Number(value) < 1) { callback( $t( 'This number is too large for a FPS Numerator of %{fpsNum}, please decrease it or increase the Numerator', - { fpsNum: videoSettings[display].video.fpsNum }, + { fpsNum: this.values.fpsNum }, ), ); } else { @@ -232,17 +295,102 @@ export function VideoSettings() { } } + setResolution(key: string, value: string) { + const display = this.state.display; + if (key === 'outputRes') { + this.state.setCustomOutputResValue(value); + } else if (key === 'baseRes') { + this.state.setCustomBaseResValue(value); + } + + if (this.resolutionValidator.pattern.test(value)) { + const [width, height] = value.split('x'); + const prefix = key === 'baseRes' ? 'base' : 'output'; + + const settings = { + [`${prefix}Width`]: Number(width), + [`${prefix}Height`]: Number(height), + }; + + // set base or output resolutions to vertical dimensions for horizontal display + // when setting vertical dimensions + if (display === 'horizontal') { + const otherPrefix = key === 'baseRes' ? 'output' : 'base'; + const customRes = this.state.customBaseRes || this.state.customOutputRes; + const verticalValues = VERTICAL_CANVAS_OPTIONS.map(option => option.value); + const horizontalValues = CANVAS_RES_OPTIONS.concat(OUTPUT_RES_OPTIONS).map( + option => option.value, + ); + const baseRes = this.values.baseRes.toString(); + const outputRes = this.values.outputRes.toString(); + + const shouldSyncVertical = + !customRes && + verticalValues.includes(value) && + !verticalValues.includes(baseRes) && + !verticalValues.includes(outputRes); + + const shouldSyncHorizontal = + !customRes && + !verticalValues.includes(value) && + !horizontalValues.includes(baseRes) && + !horizontalValues.includes(outputRes); + + if (shouldSyncVertical || shouldSyncHorizontal) { + settings[`${otherPrefix}Width`] = Number(width); + settings[`${otherPrefix}Height`] = Number(height); + } + } + this.service.actions.setSettings(settings, display); + } + } + + selectResolution(key: string, value: string) { + if (value === 'custom') { + this.setCustomResolution(key, true); + this.setResolution(key, ''); + } else { + this.setCustomResolution(key, false); + this.setResolution(key, value); + } + } + + setCustomResolution(key: string, value: boolean) { + if (key === 'baseRes') { + this.state.setCustomBaseRes(value); + } else { + this.state.setCustomOutputRes(value); + } + } + + /** + * Sets the Scale Type + * @remark set the same FPS type for both displays + * If there is a vertical context, update it as well. + * Otherwise, update the vertical display persisted settings. + */ + + setScaleType(value: EScaleType) { + this.service.actions.setVideoSetting('scaleType', value, 'horizontal'); + + if (this.service.contexts.vertical) { + this.service.actions.setVideoSetting('scaleType', value, 'vertical'); + } else { + this.dualOutputService.actions.setVideoSetting({ scaleType: value }, 'vertical'); + } + } + /** * Sets the FPS type * @remark set the same FPS type for both displays * If there is a vertical context, update it as well. * Otherwise, update the vertical display persisted settings. */ - function setFPSTypeData(value: EFPSType) { - setFPSType(value); - updateSettings('fpsType', value); - updateSettings('fpsNum', 30); - updateSettings('fpsDen', 1); + setFPSType(value: EFPSType) { + this.service.actions.setVideoSetting('fpsType', value, 'horizontal'); + this.service.actions.setVideoSetting('fpsNum', 30, 'horizontal'); + this.service.actions.setVideoSetting('fpsDen', 1, 'horizontal'); + this.service.actions.syncFPSSettings(); } /** @@ -251,24 +399,25 @@ export function VideoSettings() { * If there is a vertical context, update it as well. * Otherwise, update the vertical display persisted settings. */ - function setCommonFPS(value: string) { + setCommonFPS(value: string) { const [fpsNum, fpsDen] = value.split('-'); - updateSettings('fpsNum', Number(fpsNum)); - updateSettings('fpsDen', Number(fpsDen)); + this.service.actions.setVideoSetting('fpsNum', Number(fpsNum), 'horizontal'); + this.service.actions.setVideoSetting('fpsDen', Number(fpsDen), 'horizontal'); + this.service.actions.syncFPSSettings(); } - /** * Sets Integer FPS * @remark set the same Integer FPS for both displays * If there is a vertical context, update it as well. * Otherwise, update the vertical display persisted settings. */ - function setIntegerFPS(value: string) { - updateSettings('fpsInt', Number(value)); + setIntegerFPS(value: string) { + this.state.setFpsInt(Number(value)); if (Number(value) > 0 && Number(value) < 1001) { - updateSettings('fpsNum', Number(value)); - updateSettings('fpsDen', 1); + this.service.actions.setVideoSetting('fpsNum', Number(value), 'horizontal'); + this.service.actions.setVideoSetting('fpsDen', 1, 'horizontal'); + this.service.actions.syncFPSSettings(); } } @@ -278,147 +427,64 @@ export function VideoSettings() { * If there is a vertical context, update it as well. * Otherwise, update the vertical display persisted settings. */ - function setFPS(key: 'fpsNum' | 'fpsDen', value: string) { - if ( - !invalidFps(videoSettings[display].video.fpsNum, videoSettings[display].video.fpsDen) && - Number(value) > 0 - ) { - updateSettings(key, Number(value)); + setFPS(key: 'fpsNum' | 'fpsDen', value: string) { + if (key === 'fpsNum') { + this.state.setFpsNum(Number(value)); + } else { + this.state.setFpsDen(Number(value)); + } + if (!invalidFps(this.state.fpsNum, this.state.fpsDen) && Number(value) > 0) { + this.service.actions.setVideoSetting(key, Number(value), 'horizontal'); + this.service.actions.syncFPSSettings(); } } - const metadata = { - baseRes: { - type: 'list', - label: $t('Base (Canvas) Resolution'), - options: baseResOptions, - onChange: (val: string) => selectResolution('baseRes', val), - disabled: cantEditFields, - children: { - customBaseRes: { - type: 'text', - label: $t('Custom Base Resolution'), - rules: [resolutionValidator], - onChange: (val: string) => setCustomResolution('baseRes', val), - displayed: baseRes === 'custom', - disabled: cantEditFields, - }, - }, - }, - outputRes: { - type: 'list', - label: $t('Output (Scaled) Resolution'), - options: outputResOptions, - onChange: (val: string) => selectResolution('outputRes', val), - disabled: cantEditFields, - children: { - customOutputRes: { - type: 'text', - label: $t('Custom Output Resolution'), - rules: [resolutionValidator], - onChange: (val: string) => setCustomResolution('outputRes', val), - displayed: outputRes === 'custom', - disabled: cantEditFields, - }, - }, - }, - scaleType: { - type: 'list', - label: $t('Downscale Filter'), - onChange: (val: EScaleType) => updateSettings('scaleType', val), - options: [ - { - label: $t('Bilinear (Fastest, but blurry if scaling)'), - value: EScaleType.Bilinear, - }, - { label: $t('Bicubic (Sharpened scaling, 16 samples)'), value: EScaleType.Bicubic }, - { label: $t('Lanczos (Sharpened scaling, 32 samples)'), value: EScaleType.Lanczos }, - ], - disabled: cantEditFields, - }, - fpsType: { - type: 'list', - label: $t('FPS Type'), - onChange: (val: EFPSType) => setFPSTypeData(val), - options: [ - { label: $t('Common FPS Values'), value: EFPSType.Common }, - { label: $t('Integer FPS Values'), value: EFPSType.Integer }, - { label: $t('Fractional FPS Values'), value: EFPSType.Fractional }, - ], - disabled: cantEditFields, - children: { - fpsCom: { - type: 'list', - label: $t('Common FPS Values'), - options: FPS_OPTIONS, - onChange: (val: string) => setCommonFPS(val), - displayed: values.fpsType === EFPSType.Common, - disabled: cantEditFields, - }, - fpsInt: { - type: 'number', - label: $t('FPS Value'), - onChange: (val: string) => setIntegerFPS(val), - rules: [{ max: 1000, min: 1, message: $t('FPS Value must be between 1 and 1000') }], - displayed: values.fpsType === EFPSType.Integer, - disabled: cantEditFields, - }, - fpsNum: { - type: 'number', - label: $t('FPS Numerator'), - onChange: (val: string) => setFPS('fpsNum', val), - rules: [ - { validator: fpsNumValidator }, - { - min: 1, - message: $t('%{fieldName} must be greater than 0', { - fieldName: $t('FPS Numerator'), - }), - }, - ], - displayed: values.fpsType === EFPSType.Fractional, - disabled: cantEditFields, - }, - fpsDen: { - type: 'number', - label: $t('FPS Denominator'), - onChange: (val: string) => setFPS('fpsDen', val), - rules: [ - { validator: fpsDenValidator }, - { - min: 1, - message: $t('%{fieldName} must be greater than 0', { - fieldName: $t('FPS Denominator'), - }), - }, - ], - displayed: values.fpsType === EFPSType.Fractional, - disabled: cantEditFields, - }, - }, - }, - }; + onChange(key: keyof IVideoInfo) { + return (val: IVideoInfoValue) => + this.service.actions.setVideoSetting(key, val, this.state.display); + } - function toggleDualOutput(value: boolean) { - if (UserService.isLoggedIn) { - setShowDualOutput(); + setDisplay(display: TDisplayType) { + this.state.setDisplay(display); + + const customBaseRes = !this.baseResOptions.find( + opt => opt.value === this.service.values[display].baseRes, + ); + const customOutputRes = !this.outputResOptions.find( + opt => opt.value === this.service.values[display].outputRes, + ); + this.state.setCustomBaseRes(customBaseRes); + this.state.setCustomOutputRes(customOutputRes); + this.state.setCustomBaseResValue(this.service.values[display].baseRes); + this.state.setCustomOutputResValue(this.service.values[display].outputRes); + this.state.setFpsNum(this.service.values[display].fpsNum); + this.state.setFpsDen(this.service.values[display].fpsDen); + this.state.setFpsInt(this.service.values[display].fpsInt); + } + + toggleDualOutput(value: boolean) { + if (this.userService.isLoggedIn) { + this.setShowDualOutput(); } else { - handleShowModal(value); + this.handleShowModal(value); } } - function setShowDualOutput() { - if (StreamingService.views.isMidStreamMode) { + setShowDualOutput() { + if (Services.StreamingService.views.isMidStreamMode) { message.error({ content: $t('Cannot toggle Dual Output while live.'), }); - } else if (TransitionsService.views.studioMode) { + } else if (Services.TransitionsService.views.studioMode) { message.error({ content: $t('Cannot toggle Dual Output while in Studio Mode.'), }); } else { // show warning message if selective recording is active - if (!dualOutputMode && StreamingService.state.selectiveRecording) { + if ( + !this.dualOutputService.views.dualOutputMode && + Services.StreamingService.state.selectiveRecording + ) { remote.dialog .showMessageBox(Utils.getChildWindow(), { title: 'Vertical Display Disabled', @@ -431,55 +497,74 @@ export function VideoSettings() { } // toggle dual output - DualOutputService.actions.setDualOutputMode(!dualOutputMode); - UsageStatisticsService.recordFeatureUsage('DualOutput'); - UsageStatisticsService.recordAnalyticsEvent('DualOutput', { + this.dualOutputService.actions.setDualOutputMode( + !this.dualOutputService.views.dualOutputMode, + ); + this.state.setShowDualOutputSettings(!this.state.showDualOutputSettings); + Services.UsageStatisticsService.recordFeatureUsage('DualOutput'); + Services.UsageStatisticsService.recordAnalyticsEvent('DualOutput', { type: 'ToggleOnDualOutput', source: 'VideoSettings', - isPrime: UserService.isPrime, - platforms: StreamingService.views.linkedPlatforms, - tiktokStatus: TikTokService.scope, + isPrime: this.userService.isPrime, + platforms: this.streamingService.views.linkedPlatforms, + tiktokStatus: this.tiktokService.scope, }); } } - function handleAuth() { - WindowsService.actions.closeChildWindow(); - UserService.actions.showLogin(); - const onboardingCompleted = OnboardingService.onboardingCompleted.subscribe(() => { - DualOutputService.actions.setDualOutputMode(); - SettingsService.actions.showSettings('Video'); + handleShowModal(status: boolean) { + Services.WindowsService.actions.updateStyleBlockers('child', status); + this.state.setShowModal(status); + } + + handleAuth() { + Services.WindowsService.actions.closeChildWindow(); + this.userService.actions.showLogin(); + const onboardingCompleted = Services.OnboardingService.onboardingCompleted.subscribe(() => { + Services.DualOutputService.actions.setDualOutputMode(); + Services.SettingsService.actions.showSettings('Video'); onboardingCompleted.unsubscribe(); }); } +} - function handleShowModal(status: boolean) { - WindowsService.actions.updateStyleBlockers('child', status); - setShowModal(status); - } +export function VideoSettings() { + const { + values, + metadata, + showDualOutputSettings, + showModal, + cantEditFields, + onChange, + setDisplay, + toggleDualOutput, + handleAuth, + handleShowModal, + } = useModule(VideoSettingsModule); return ( -
+ <>

{$t('Video')}

- {dualOutputMode && } - + {showDualOutputSettings && } + +
- +
-
+ ); } diff --git a/app/services/api/external-api/scenes/scene-item.ts b/app/services/api/external-api/scenes/scene-item.ts index 858626ae1948..bbf3a3b28ab3 100644 --- a/app/services/api/external-api/scenes/scene-item.ts +++ b/app/services/api/external-api/scenes/scene-item.ts @@ -12,7 +12,7 @@ import { getExternalNodeModel, ISceneNodeModel, SceneNode } from './scene-node'; import Utils from '../../../utils'; import { ServiceHelper } from '../../../core'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; /** * Serialized representation of {@link SceneItem}. diff --git a/app/services/api/external-api/scenes/scene-node.ts b/app/services/api/external-api/scenes/scene-node.ts index 068aeed72120..a70524767638 100644 --- a/app/services/api/external-api/scenes/scene-node.ts +++ b/app/services/api/external-api/scenes/scene-node.ts @@ -12,7 +12,7 @@ import { SceneItemFolder } from './scene-item-folder'; import { SceneItem } from './scene-item'; import { ServiceHelper } from 'services'; import { ISerializable } from '../../rpc-api'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { IVideo } from 'obs-studio-node'; /** diff --git a/app/services/api/external-api/scenes/scenes.ts b/app/services/api/external-api/scenes/scenes.ts index ba90da918afc..2d339edde8b4 100644 --- a/app/services/api/external-api/scenes/scenes.ts +++ b/app/services/api/external-api/scenes/scenes.ts @@ -14,7 +14,7 @@ import { Expensive } from 'services/api/external-api-limits'; import { EditorService } from '../../../editor'; import { map } from 'rxjs/operators'; import { SelectionService } from 'services/selection'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; /** * API for scenes management. Contains operations like scene creation, switching diff --git a/app/services/api/external-api/video/video.ts b/app/services/api/external-api/video/video.ts index aafe1fad8cf4..2ea2e88f5126 100644 --- a/app/services/api/external-api/video/video.ts +++ b/app/services/api/external-api/video/video.ts @@ -1,18 +1,18 @@ import { Fallback, Singleton } from 'services/api/external-api'; import { Inject } from 'services/core/injector'; -import { VideoService as InternalVideoSettingsService } from 'services/video'; +import { VideoSettingsService as InternalVideoSettingsService } from 'services/settings-v2'; import { IVideo } from '../../../../../obs-api'; @Singleton() -export class VideoService { +export class VideoSettingsService { @Fallback() @Inject() - private videoService!: InternalVideoSettingsService; + private videoSettingsService!: InternalVideoSettingsService; /* * Returns video contexts */ get contexts(): Dictionary { - return this.videoService.contexts; + return this.videoSettingsService.contexts; } } diff --git a/app/services/app/app.ts b/app/services/app/app.ts index 96dd69a3677f..58a0fff6cd2f 100644 --- a/app/services/app/app.ts +++ b/app/services/app/app.ts @@ -10,6 +10,7 @@ import { TransitionsService } from 'services/transitions'; import { SourcesService } from 'services/sources'; import { ScenesService } from 'services/scenes'; import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { track, UsageStatisticsService } from 'services/usage-statistics'; import { IpcServerService } from 'services/api/ipc-server'; import { TcpServerService } from 'services/api/tcp-server'; @@ -92,6 +93,7 @@ export class AppService extends StatefulService { @Inject() private metricsService: MetricsService; @Inject() private settingsService: SettingsService; @Inject() private usageStatisticsService: UsageStatisticsService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private dualOutputService: DualOutputService; @Inject() private realmService: RealmService; @@ -197,8 +199,8 @@ export class AppService extends StatefulService { await this.sceneCollectionsService.deinitialize(); this.performanceService.stop(); this.transitionsService.shutdown(); + this.videoSettingsService.shutdown(); await this.gameOverlayService.destroy(); - this.videoService.shutdown(); await this.fileManagerService.flushAll(); obs.NodeObs.RemoveSourceCallback(); obs.NodeObs.RemoveVolmeterCallback(); diff --git a/app/services/auto-config/index.ts b/app/services/auto-config/index.ts index e4c9be82d9d4..bb4fe693f892 100644 --- a/app/services/auto-config/index.ts +++ b/app/services/auto-config/index.ts @@ -7,7 +7,7 @@ import { StreamSettingsService } from 'services/settings/streaming'; import { getPlatformService } from 'services/platforms'; import { TwitchService } from 'services/platforms/twitch'; import { YoutubeService } from 'app-services'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { UserService } from 'services/user'; export type TConfigEvent = 'starting_step' | 'progress' | 'stopping_step' | 'error' | 'done'; @@ -26,7 +26,7 @@ export interface IConfigProgress { export class AutoConfigService extends Service { @Inject() streamSettingsService: StreamSettingsService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() userService: UserService; configProgress = new Subject(); @@ -74,8 +74,8 @@ export class AutoConfigService extends Service { // * the base width/height and output width/height. So before running the optimizer, // * confirm that horizontal base width/height and output width/height are on the Video property. // */ - // if (this.videoService.contexts?.vertical) { - // this.videoService.confirmVideoSettingDimensions(); + // if (this.videoSettingsService.contexts?.vertical) { + // this.videoSettingsService.confirmVideoSettingDimensions(); // } // obs.NodeObs.InitializeAutoConfig( @@ -128,7 +128,7 @@ export class AutoConfigService extends Service { obs.NodeObs.TerminateAutoConfig(); // apply optimized settings to the video contexts - this.videoService.migrateAutoConfigSettings(); + this.videoSettingsService.migrateAutoConfigSettings(); } } @@ -140,7 +140,7 @@ export class AutoConfigService extends Service { obs.NodeObs.TerminateAutoConfig(); // apply optimized settings to the video contexts - this.videoService.migrateAutoConfigSettings(); + this.videoSettingsService.migrateAutoConfigSettings(); debounce(() => this.configProgress.next({ ...progress, event: 'done' }), 1000)(); } } diff --git a/app/services/clipboard.ts b/app/services/clipboard.ts index 2bc9f36f3a48..82275d343d32 100644 --- a/app/services/clipboard.ts +++ b/app/services/clipboard.ts @@ -20,7 +20,7 @@ import { EditorCommandsService } from 'services/editor-commands'; import { IFilterData } from 'services/editor-commands/commands/paste-filters'; import { NavigationService } from 'services/navigation'; import { DualOutputService } from './dual-output'; -import { VideoService } from './video'; +import { VideoSettingsService } from './settings-v2'; import { byOS, OS } from 'util/operating-systems'; const { clipboard } = electron; @@ -149,7 +149,7 @@ export class ClipboardService extends StatefulService { @Inject() private editorCommandsService: EditorCommandsService; @Inject() private navigationService: NavigationService; @Inject() private dualOutputService: DualOutputService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; get views() { return new ClipboardViews(this.state); diff --git a/app/services/diagnostics.ts b/app/services/diagnostics.ts index d6a42c9f9f97..f6ac105968bb 100644 --- a/app/services/diagnostics.ts +++ b/app/services/diagnostics.ts @@ -25,14 +25,14 @@ import { RecordingModeService, StreamSettingsService, TransitionsService, - VideoService, + VideoSettingsService, } from 'app-services'; import * as remote from '@electron/remote'; import { AppService } from 'services/app'; import fs from 'fs'; import path from 'path'; import { platformList, TPlatform } from './platforms'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from './settings-v2'; interface IStreamDiagnosticInfo { startTime: number; @@ -158,7 +158,7 @@ export class DiagnosticsService extends PersistentStatefulService { - const setting = this.videoService.formatVideoSettings(display, true); + const setting = this.videoSettingsService.formatVideoSettings(display, true); const maxHeight = display === 'horizontal' ? 1080 : 1280; const minHeight = 720; if (!setting) return; - const outputRes = this.videoService.outputResolutions[display]; + const outputRes = this.videoSettingsService.outputResolutions[display]; const outputAspect = outputRes.outputWidth / outputRes.outputHeight; if (outputAspect < 16 / 9.1 || outputAspect > 16 / 8.9) { @@ -582,7 +582,7 @@ export class DiagnosticsService extends PersistentStatefulService 16 / 8.9) { diff --git a/app/services/dual-output/dual-output.ts b/app/services/dual-output/dual-output.ts index 920fdc96346a..7feb569f0238 100644 --- a/app/services/dual-output/dual-output.ts +++ b/app/services/dual-output/dual-output.ts @@ -1,6 +1,7 @@ import { PersistentStatefulService, InitAfter, Inject, ViewHandler, mutation } from 'services/core'; +import { verticalDisplayData } from '../settings-v2/default-settings-data'; import { ScenesService, SceneItem, TSceneNode } from 'services/scenes'; -import { TDisplayType, VideoService } from 'services/video'; +import { TDisplayType, VideoSettingsService } from 'services/settings-v2/video'; import { TPlatform } from 'services/platforms'; import { EPlaceType } from 'services/editor-commands/commands/reorder-nodes'; import { EditorCommandsService } from 'services/editor-commands'; @@ -16,12 +17,18 @@ import { UserService } from 'services/user'; import { SelectionService, Selection } from 'services/selection'; import { StreamingService } from 'services/streaming'; import { SettingsService } from 'services/settings'; +import { SourcesService, TSourceType } from 'services/sources'; +import { WidgetsService, WidgetType } from 'services/widgets'; import { RunInLoadingMode } from 'services/app/app-decorators'; import compact from 'lodash/compact'; import invert from 'lodash/invert'; import forEachRight from 'lodash/forEachRight'; +import { byOS, OS } from 'util/operating-systems'; +import { DefaultHardwareService } from 'services/hardware/default-hardware'; interface IDisplayVideoSettings { + horizontal: IVideoInfo; + vertical: IVideoInfo; activeDisplays: { horizontal: boolean; vertical: boolean; @@ -48,7 +55,7 @@ export type TDisplayDestinations = { class DualOutputViews extends ViewHandler { @Inject() private scenesService: ScenesService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private sceneCollectionsService: SceneCollectionsService; @Inject() private streamingService: StreamingService; @@ -165,7 +172,7 @@ class DualOutputViews extends ViewHandler { getPlatformContext(platform: TPlatform) { const display = this.getPlatformDisplay(platform); - return this.videoService.state[display]; + return this.videoSettingsService.state[display]; } getPlatformMode(platform: TPlatform): TOutputOrientation { @@ -276,7 +283,7 @@ class DualOutputViews extends ViewHandler { @InitAfter('ScenesService') export class DualOutputService extends PersistentStatefulService { @Inject() private scenesService: ScenesService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private editorCommandsService: EditorCommandsService; @Inject() private sceneCollectionsService: SceneCollectionsService; @Inject() private streamSettingsService: StreamSettingsService; @@ -288,6 +295,8 @@ export class DualOutputService extends PersistentStatefulService { @@ -688,7 +697,7 @@ export class DualOutputService extends PersistentStatefulService, display?: TDisplayType) { + this.SET_VIDEO_SETTING(setting, display); + } + + updateVideoSettings(settings: IVideoInfo, display: TDisplayType = 'horizontal') { + this.UPDATE_VIDEO_SETTING(settings, display); + } + setIsLoading(status: boolean) { this.SET_IS_LOADING(status); } @@ -815,6 +836,19 @@ export class DualOutputService extends PersistentStatefulService, display: TDisplayType = 'vertical') { + this.state.videoSettings[display] = { + ...this.state.videoSettings[display], + ...setting, + }; + } + + @mutation() + private UPDATE_VIDEO_SETTING(setting: IVideoInfo, display: TDisplayType = 'vertical') { + this.state.videoSettings[display] = { ...setting }; + } + @mutation() private SET_IS_LOADING(status: boolean) { this.state = { ...this.state, isLoading: status }; diff --git a/app/services/editor-commands/commands/copy-nodes.ts b/app/services/editor-commands/commands/copy-nodes.ts index e996284951bb..e6fa55598b11 100644 --- a/app/services/editor-commands/commands/copy-nodes.ts +++ b/app/services/editor-commands/commands/copy-nodes.ts @@ -5,7 +5,7 @@ import { ScenesService, TSceneNode } from 'services/scenes'; import compact from 'lodash/compact'; import { $t } from 'services/i18n'; import { DualOutputService } from 'services/dual-output'; -import { TDisplayType, VideoService } from 'services/video'; +import { TDisplayType, VideoSettingsService } from 'services/settings-v2'; import { EditorService } from 'services/editor'; import { SceneCollectionsService } from 'services/scene-collections'; import { cloneDeep } from 'lodash'; @@ -28,7 +28,7 @@ import { cloneDeep } from 'lodash'; export class CopyNodesCommand extends Command { @Inject() scenesService: ScenesService; @Inject() dualOutputService: DualOutputService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() editorService: EditorService; @Inject() sceneCollectionsService: SceneCollectionsService; diff --git a/app/services/editor-commands/commands/crop-items.ts b/app/services/editor-commands/commands/crop-items.ts index 87992616d914..3d3c0766e263 100644 --- a/app/services/editor-commands/commands/crop-items.ts +++ b/app/services/editor-commands/commands/crop-items.ts @@ -1,7 +1,7 @@ import { ModifyTransformCommand } from './modify-transform'; import { Selection } from 'services/selection'; import { $t } from 'services/i18n'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export class CropItemsCommand extends ModifyTransformCommand { /** diff --git a/app/services/editor-commands/commands/modify-transform.ts b/app/services/editor-commands/commands/modify-transform.ts index 1d80529e9ced..a25fc45a7bc5 100644 --- a/app/services/editor-commands/commands/modify-transform.ts +++ b/app/services/editor-commands/commands/modify-transform.ts @@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import { TObsFormData } from 'components/obs/inputs/ObsInput'; import { EditSourcePropertiesCommand } from './edit-source-properties'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export abstract class ModifyTransformCommand extends CombinableCommand { startTransforms: Dictionary = {}; diff --git a/app/services/editor-commands/commands/move-items.ts b/app/services/editor-commands/commands/move-items.ts index a79cdbc15a64..53c23e552f12 100644 --- a/app/services/editor-commands/commands/move-items.ts +++ b/app/services/editor-commands/commands/move-items.ts @@ -1,7 +1,7 @@ import { ModifyTransformCommand } from './modify-transform'; import { Selection } from 'services/selection'; import { $t } from 'services/i18n'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export class MoveItemsCommand extends ModifyTransformCommand { constructor( diff --git a/app/services/editor-commands/commands/resize-items.ts b/app/services/editor-commands/commands/resize-items.ts index f46e2e80677a..a7193349ff93 100644 --- a/app/services/editor-commands/commands/resize-items.ts +++ b/app/services/editor-commands/commands/resize-items.ts @@ -1,7 +1,7 @@ import { ModifyTransformCommand } from './modify-transform'; import { Selection } from 'services/selection'; import { $t } from 'services/i18n'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export class ResizeItemsCommand extends ModifyTransformCommand { constructor( diff --git a/app/services/editor.ts b/app/services/editor.ts index 18c62c4d7316..ccabc6583e1a 100644 --- a/app/services/editor.ts +++ b/app/services/editor.ts @@ -14,7 +14,7 @@ import { mutation } from './core'; import { byOS, OS } from 'util/operating-systems'; import { TcpServerService } from './api/tcp-server'; import { Subject } from 'rxjs'; -import { TDisplayType, VideoService } from './video'; +import { TDisplayType, VideoSettingsService } from './settings-v2'; /** * Examine scene items props @@ -63,7 +63,7 @@ export class EditorService extends StatefulService { @Inject() private customizationService: CustomizationService; @Inject() private editorCommandsService: EditorCommandsService; @Inject() private tcpServerService: TcpServerService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; /** * emit this event when drag or resize have been finished @@ -677,7 +677,7 @@ export class EditorService extends StatefulService { } get baseResolutions() { - return this.videoService.baseResolutions; + return this.videoSettingsService.baseResolutions; } // Using a computed property since it is cached get resizeRegions(): IResizeRegion[] { diff --git a/app/services/hotkeys.ts b/app/services/hotkeys.ts index 12903043ca6d..a0689e2dd7d1 100644 --- a/app/services/hotkeys.ts +++ b/app/services/hotkeys.ts @@ -15,7 +15,7 @@ import { CustomizationService } from './customization'; import { RecentEventsService } from './recent-events'; import { UsageStatisticsService } from './usage-statistics'; import { getOS, OS } from 'util/operating-systems'; -import { TDisplayType } from './video'; +import { TDisplayType } from './settings-v2'; import { VirtualWebcamService } from 'app-services'; function getScenesService(): ScenesService { diff --git a/app/services/performance.ts b/app/services/performance.ts index 7306cd84c415..7f16d61fb28e 100644 --- a/app/services/performance.ts +++ b/app/services/performance.ts @@ -13,7 +13,7 @@ import { JsonrpcService } from './api/jsonrpc'; import { TroubleshooterService, TIssueCode } from 'services/troubleshooter'; import { $t } from 'services/i18n'; import { StreamingService, EStreamingState } from 'services/streaming'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { DualOutputService } from 'services/dual-output'; import { UsageStatisticsService } from './usage-statistics'; @@ -113,7 +113,7 @@ export class PerformanceService extends StatefulService { @Inject() private troubleshooterService: TroubleshooterService; @Inject() private streamingService: StreamingService; @Inject() private usageStatisticsService: UsageStatisticsService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private dualOutputService: DualOutputService; static initialState: IPerformanceState = { @@ -202,10 +202,10 @@ export class PerformanceService extends StatefulService { * Capture some analytics for the entire duration of a stream */ startStreamQualityMonitoring() { - this.streamStartSkippedFrames = this.videoService.contexts.horizontal.skippedFrames; + this.streamStartSkippedFrames = this.videoSettingsService.contexts.horizontal.skippedFrames; this.streamStartLaggedFrames = obs.Global.laggedFrames; this.streamStartRenderedFrames = obs.Global.totalFrames; - this.streamStartEncodedFrames = this.videoService.contexts.horizontal.encodedFrames; + this.streamStartEncodedFrames = this.videoSettingsService.contexts.horizontal.encodedFrames; this.streamStartTime = new Date(); this.historicalCPU = []; } @@ -216,8 +216,10 @@ export class PerformanceService extends StatefulService { (obs.Global.totalFrames - this.streamStartRenderedFrames)) * 100; const streamSkipped = - ((this.videoService.contexts.horizontal.skippedFrames - this.streamStartSkippedFrames) / - (this.videoService.contexts.horizontal.encodedFrames - this.streamStartEncodedFrames)) * + ((this.videoSettingsService.contexts.horizontal.skippedFrames - + this.streamStartSkippedFrames) / + (this.videoSettingsService.contexts.horizontal.encodedFrames - + this.streamStartEncodedFrames)) * 100; const streamDropped = this.state.percentageDroppedFrames; const streamDuration = new Date().getTime() - this.streamStartTime.getTime(); @@ -242,8 +244,8 @@ export class PerformanceService extends StatefulService { const currentStats: IMonitorState = { framesLagged: obs.Global.laggedFrames, framesRendered: obs.Global.totalFrames, - framesSkipped: this.videoService.contexts.horizontal.skippedFrames, - framesEncoded: this.videoService.contexts.horizontal.encodedFrames, + framesSkipped: this.videoSettingsService.contexts.horizontal.skippedFrames, + framesEncoded: this.videoSettingsService.contexts.horizontal.encodedFrames, }; const nextStats = this.nextStats(currentStats); diff --git a/app/services/platform-apps/api/modules/scenes.ts b/app/services/platform-apps/api/modules/scenes.ts index b3a359227ae4..28b0a2c6b156 100644 --- a/app/services/platform-apps/api/modules/scenes.ts +++ b/app/services/platform-apps/api/modules/scenes.ts @@ -14,7 +14,7 @@ import { TSceneNode, EBlendingMethod, } from 'services/scenes'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { IVideo } from 'obs-studio-node'; import { Inject } from 'services/core/injector'; import { Subject } from 'rxjs'; diff --git a/app/services/platform-apps/index.ts b/app/services/platform-apps/index.ts index 98c732b2f8b8..68da7ee3b17b 100644 --- a/app/services/platform-apps/index.ts +++ b/app/services/platform-apps/index.ts @@ -6,7 +6,7 @@ import { Subject } from 'rxjs'; import { IWindowOptions, WindowsService } from 'services/windows'; import { Inject } from 'services/core/injector'; import { EApiPermissions } from './api/modules/module'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { DevServer } from './dev-server'; import { HostsService } from 'services/hosts'; import { authorizedHeaders, handleResponse, jfetch } from 'util/requests'; @@ -170,7 +170,7 @@ class PlatformAppsViews extends ViewHandler { @InitAfter('UserService') export class PlatformAppsService extends StatefulService { @Inject() windowsService: WindowsService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() hostsService: HostsService; @Inject() userService: UserService; @Inject() navigationService: NavigationService; @@ -601,8 +601,8 @@ export class PlatformAppsService extends StatefulService extends Stat @Inject() protected hostsService: HostsService; @Inject() protected streamSettingsService: StreamSettingsService; @Inject() protected dualOutputService: DualOutputService; - @Inject() protected videoService: VideoService; + @Inject() protected videoSettingsService: VideoSettingsService; abstract readonly platform: TPlatform; diff --git a/app/services/platforms/facebook.ts b/app/services/platforms/facebook.ts index a9bf92303771..f2c8eaab609b 100644 --- a/app/services/platforms/facebook.ts +++ b/app/services/platforms/facebook.ts @@ -13,7 +13,7 @@ import { throwStreamError } from 'services/streaming/stream-error'; import { BasePlatformService } from './base-platform'; import { WindowsService } from '../windows'; import { assertIsDefined, getDefined } from '../../util/properties-type-guards'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { TOutputOrientation } from 'services/restream'; import { ENotificationType, NotificationsService } from '../notifications'; import { $t } from '../i18n'; diff --git a/app/services/platforms/index.ts b/app/services/platforms/index.ts index de3df79b2871..46df3c5bf5db 100644 --- a/app/services/platforms/index.ts +++ b/app/services/platforms/index.ts @@ -8,7 +8,7 @@ import { TTwitchOAuthScope } from './twitch/index'; import { IGoLiveSettings } from 'services/streaming'; import { WidgetType } from '../widgets'; import { ITrovoStartStreamOptions, TrovoService } from './trovo'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { $t } from 'services/i18n'; import { KickService, IKickStartStreamOptions } from './kick'; diff --git a/app/services/platforms/instagram.ts b/app/services/platforms/instagram.ts index a7cc02732fbd..aa583ea21ece 100644 --- a/app/services/platforms/instagram.ts +++ b/app/services/platforms/instagram.ts @@ -11,7 +11,7 @@ import { } from '.'; import { BasePlatformService } from './base-platform'; import { IGoLiveSettings } from 'services/streaming'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { WidgetType } from 'services/widgets'; import { InheritMutations, mutation } from 'services/core'; import Utils from 'services/utils'; diff --git a/app/services/platforms/kick.ts b/app/services/platforms/kick.ts index 1830d53d4398..ef9b246eefbc 100644 --- a/app/services/platforms/kick.ts +++ b/app/services/platforms/kick.ts @@ -13,7 +13,7 @@ import { platformAuthorizedRequest } from './utils'; import { IGoLiveSettings } from '../streaming'; import { TOutputOrientation } from 'services/restream'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { I18nService } from 'services/i18n'; import { getDefined } from 'util/properties-type-guards'; import { WindowsService } from 'services/windows'; diff --git a/app/services/platforms/tiktok.ts b/app/services/platforms/tiktok.ts index 92c4f30f93ba..8cbfbd50fa3a 100644 --- a/app/services/platforms/tiktok.ts +++ b/app/services/platforms/tiktok.ts @@ -20,7 +20,7 @@ import { getOS } from 'util/operating-systems'; import { IGoLiveSettings } from '../streaming'; import { TOutputOrientation } from 'services/restream'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { ETikTokErrorTypes, ETikTokLiveScopeReason, diff --git a/app/services/platforms/trovo.ts b/app/services/platforms/trovo.ts index d74a6ad3be3a..bf443c30add4 100644 --- a/app/services/platforms/trovo.ts +++ b/app/services/platforms/trovo.ts @@ -13,7 +13,7 @@ import { platformAuthorizedRequest } from './utils'; import { IGoLiveSettings } from '../streaming'; import { getDefined } from '../../util/properties-type-guards'; import Utils from '../utils'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { TOutputOrientation } from 'services/restream'; import { IVideo } from 'obs-studio-node'; diff --git a/app/services/platforms/twitch.ts b/app/services/platforms/twitch.ts index cf0a022a7338..d269516816e0 100644 --- a/app/services/platforms/twitch.ts +++ b/app/services/platforms/twitch.ts @@ -20,7 +20,7 @@ import { StreamError, throwStreamError, TStreamErrorType } from 'services/stream import { BasePlatformService } from './base-platform'; import Utils from '../utils'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { TOutputOrientation } from 'services/restream'; import { ITwitchContentClassificationLabelsRootResponse, diff --git a/app/services/platforms/twitter.ts b/app/services/platforms/twitter.ts index ceb82ab82af8..eb39128d3655 100644 --- a/app/services/platforms/twitter.ts +++ b/app/services/platforms/twitter.ts @@ -6,7 +6,7 @@ import { throwStreamError } from '../streaming/stream-error'; import { platformAuthorizedRequest } from './utils'; import { IGoLiveSettings } from '../streaming'; import Utils from '../utils'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import { ENotificationType, NotificationsService } from '../notifications'; import { JsonrpcService } from '../api/jsonrpc'; import * as remote from '@electron/remote'; diff --git a/app/services/platforms/youtube.ts b/app/services/platforms/youtube.ts index bcc795abfc01..2c87e3248358 100644 --- a/app/services/platforms/youtube.ts +++ b/app/services/platforms/youtube.ts @@ -14,7 +14,7 @@ import { IGoLiveSettings } from 'services/streaming'; import { I18nService } from 'services/i18n'; import { throwStreamError } from 'services/streaming/stream-error'; import { BasePlatformService } from './base-platform'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2/video'; import { assertIsDefined, getDefined } from 'util/properties-type-guards'; import Utils from '../utils'; import { YoutubeUploader } from './youtube/uploader'; diff --git a/app/services/recording-mode.ts b/app/services/recording-mode.ts index 9fc6b2ecc0ee..fc605b727dc8 100644 --- a/app/services/recording-mode.ts +++ b/app/services/recording-mode.ts @@ -8,7 +8,7 @@ import { ELayout, ELayoutElement, LayoutService } from './layout'; import { ScenesService } from './scenes'; import { EObsSimpleEncoder, SettingsService } from './settings'; import { AnchorPoint, ScalableRectangle } from 'util/ScalableRectangle'; -import { VideoService } from './video'; +import { VideoSettingsService } from './settings-v2/video'; import { ENotificationType, NotificationsService } from 'services/notifications'; import { DefaultHardwareService } from './hardware'; import { RunInLoadingMode } from './app/app-decorators'; @@ -58,7 +58,7 @@ export class RecordingModeService extends PersistentStatefulService { rect.x = 20; - rect.y = this.videoService.baseHeight - 20; + rect.y = this.videoSettingsService.baseHeight - 20; }); item.setTransform({ diff --git a/app/services/restream.ts b/app/services/restream.ts index 2749fb539f55..aeaf4024f761 100644 --- a/app/services/restream.ts +++ b/app/services/restream.ts @@ -14,7 +14,7 @@ import { TikTokService } from './platforms/tiktok'; import { TrovoService } from './platforms/trovo'; import { KickService } from './platforms/kick'; import * as remote from '@electron/remote'; -import { VideoService, TDisplayType } from './video'; +import { VideoSettingsService, TDisplayType } from './settings-v2/video'; import { DualOutputService } from './dual-output'; import { TwitterPlatformService } from './platforms/twitter'; import { InstagramService } from './platforms/instagram'; @@ -58,7 +58,7 @@ export class RestreamService extends StatefulService { @Inject() trovoService: TrovoService; @Inject() kickService: KickService; @Inject() instagramService: InstagramService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() dualOutputService: DualOutputService; @Inject('TwitterPlatformService') twitterService: TwitterPlatformService; @Inject() platformAppsService: PlatformAppsService; diff --git a/app/services/scene-collections/nodes/overlays/game-capture.ts b/app/services/scene-collections/nodes/overlays/game-capture.ts index ca7e70b302dc..9e73713b1bfc 100644 --- a/app/services/scene-collections/nodes/overlays/game-capture.ts +++ b/app/services/scene-collections/nodes/overlays/game-capture.ts @@ -4,7 +4,7 @@ import uniqueId from 'lodash/uniqueId'; import path from 'path'; import fs from 'fs'; import { Inject } from 'services/core'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; interface ISchema { placeholderFile: string; @@ -20,7 +20,7 @@ interface IContext { export class GameCaptureNode extends Node { schemaVersion = 2; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; async save(context: IContext) { let placeholderFile: string; @@ -34,10 +34,12 @@ export class GameCaptureNode extends Node { fs.writeFileSync(destination, fs.readFileSync(settings.user_placeholder_image)); } - const width = this.videoService.baseResolutions[context.sceneItem.display ?? 'horizontal'] - .baseWidth; - const height = this.videoService.baseResolutions[context.sceneItem.display ?? 'vertical'] - .baseHeight; + const width = this.videoSettingsService.baseResolutions[ + context.sceneItem.display ?? 'horizontal' + ].baseWidth; + const height = this.videoSettingsService.baseResolutions[ + context.sceneItem.display ?? 'vertical' + ].baseHeight; this.data = { placeholderFile, diff --git a/app/services/scene-collections/nodes/overlays/scene.ts b/app/services/scene-collections/nodes/overlays/scene.ts index 2c09a06ca5a6..ad9a925b72b5 100644 --- a/app/services/scene-collections/nodes/overlays/scene.ts +++ b/app/services/scene-collections/nodes/overlays/scene.ts @@ -1,7 +1,7 @@ import { Node } from '../node'; import { SceneItem } from 'services/scenes'; import { Inject } from 'services/core'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; interface ISceneNodeSchema { sceneId: string; @@ -17,13 +17,14 @@ interface IContext { export class SceneSourceNode extends Node { schemaVersion = 2; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; async save(context: IContext) { this.data = { sceneId: context.sceneItem.sourceId, - width: this.videoService.baseResolutions[context.sceneItem.display ?? 'horizontal'].baseWidth, - height: this.videoService.baseResolutions[context.sceneItem.display ?? 'horizontal'] + width: this.videoSettingsService.baseResolutions[context.sceneItem.display ?? 'horizontal'] + .baseWidth, + height: this.videoSettingsService.baseResolutions[context.sceneItem.display ?? 'horizontal'] .baseHeight, }; } diff --git a/app/services/scene-collections/nodes/overlays/slots.ts b/app/services/scene-collections/nodes/overlays/slots.ts index 5eea39069445..14a576102645 100644 --- a/app/services/scene-collections/nodes/overlays/slots.ts +++ b/app/services/scene-collections/nodes/overlays/slots.ts @@ -1,6 +1,6 @@ import { ArrayNode } from '../array-node'; import { SceneItem, Scene, TSceneNode, ScenesService } from 'services/scenes'; -import { VideoService, TDisplayType } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { SourcesService, TSourceType } from 'services/sources'; import { SourceFiltersService, TSourceFilterType } from 'services/source-filters'; import { Inject } from 'services/core/injector'; @@ -18,6 +18,7 @@ import { WidgetType } from '../../../widgets'; import { byOS, OS, getOS } from 'util/operating-systems'; import { GameCaptureNode } from './game-capture'; import { Node } from '../node'; +import { TDisplayType } from 'services/settings-v2'; type TContent = | ImageNode @@ -80,7 +81,7 @@ interface IContext { export class SlotsNode extends ArrayNode { schemaVersion = 1; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() sourceFiltersService: SourceFiltersService; @Inject() sourcesService: SourcesService; @Inject() scenesService: ScenesService; @@ -105,10 +106,10 @@ export class SlotsNode extends ArrayNode { id: sceneNode.id, sceneNodeType: 'item', name: sceneNode.name, - x: sceneNode.transform.position.x / this.videoService.baseWidth, - y: sceneNode.transform.position.y / this.videoService.baseHeight, - scaleX: sceneNode.transform.scale.x / this.videoService.baseWidth, - scaleY: sceneNode.transform.scale.y / this.videoService.baseHeight, + x: sceneNode.transform.position.x / this.videoSettingsService.baseWidth, + y: sceneNode.transform.position.y / this.videoSettingsService.baseHeight, + scaleX: sceneNode.transform.scale.x / this.videoSettingsService.baseWidth, + scaleY: sceneNode.transform.scale.y / this.videoSettingsService.baseHeight, crop: sceneNode.transform.crop, rotation: sceneNode.transform.rotation, visible: sceneNode.visible, @@ -271,8 +272,8 @@ export class SlotsNode extends ArrayNode { // Adjust scales by the ratio of the exported base resolution to // the users current base resolution - obj.scaleX *= obj.content.data.width / this.videoService.baseWidth; - obj.scaleY *= obj.content.data.height / this.videoService.baseHeight; + obj.scaleX *= obj.content.data.width / this.videoSettingsService.baseWidth; + obj.scaleY *= obj.content.data.height / this.videoSettingsService.baseHeight; } else { // We will not load this source at all on mac return; @@ -334,8 +335,8 @@ export class SlotsNode extends ArrayNode { // Adjust scales by the ratio of the exported base resolution to // the users current base resolution - obj.scaleX *= obj.content.data.width / this.videoService.baseWidth; - obj.scaleY *= obj.content.data.height / this.videoService.baseHeight; + obj.scaleX *= obj.content.data.width / this.videoSettingsService.baseWidth; + obj.scaleY *= obj.content.data.height / this.videoSettingsService.baseHeight; } this.adjustTransform(sceneItem, obj); @@ -371,8 +372,8 @@ export class SlotsNode extends ArrayNode { if (item.type === 'game_capture') { item.setTransform({ position: { - x: obj.x * this.videoService.baseWidth, - y: obj.y * this.videoService.baseHeight, + x: obj.x * this.videoSettingsService.baseWidth, + y: obj.y * this.videoSettingsService.baseHeight, }, crop: obj.crop, rotation: obj.rotation, @@ -380,12 +381,12 @@ export class SlotsNode extends ArrayNode { } else { item.setTransform({ position: { - x: obj.x * this.videoService.baseWidth, - y: obj.y * this.videoService.baseHeight, + x: obj.x * this.videoSettingsService.baseWidth, + y: obj.y * this.videoSettingsService.baseHeight, }, scale: { - x: obj.scaleX * this.videoService.baseWidth, - y: obj.scaleY * this.videoService.baseHeight, + x: obj.scaleX * this.videoSettingsService.baseWidth, + y: obj.scaleY * this.videoSettingsService.baseHeight, }, crop: obj.crop, rotation: obj.rotation, diff --git a/app/services/scene-collections/nodes/overlays/webcam.ts b/app/services/scene-collections/nodes/overlays/webcam.ts index 9dd8d81d20b7..a47a58dfba69 100644 --- a/app/services/scene-collections/nodes/overlays/webcam.ts +++ b/app/services/scene-collections/nodes/overlays/webcam.ts @@ -1,6 +1,6 @@ import { Node } from '../node'; import { SceneItem } from '../../../scenes'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from '../../../settings-v2/video'; import { SourcesService } from '../../../sources'; import sortBy from 'lodash/sortBy'; import { IListProperty } from '../../../../../obs-api'; @@ -31,7 +31,7 @@ interface IResolution { export class WebcamNode extends Node { schemaVersion = 1; - videoService: VideoService = VideoService.instance; + videoSettingsService: VideoSettingsService = VideoSettingsService.instance; sourcesService: SourcesService = SourcesService.instance; @Inject() private defaultHardwareService: DefaultHardwareService; @@ -39,14 +39,14 @@ export class WebcamNode extends Node { const rect = new ScalableRectangle(context.sceneItem.rectangle); this.data = { - width: rect.scaledWidth / this.videoService.baseWidth, - height: rect.scaledHeight / this.videoService.baseHeight, + width: rect.scaledWidth / this.videoSettingsService.baseWidth, + height: rect.scaledHeight / this.videoSettingsService.baseHeight, }; } async load(context: IContext) { - const targetWidth = this.data.width * this.videoService.baseWidth; - const targetHeight = this.data.height * this.videoService.baseHeight; + const targetWidth = this.data.width * this.videoSettingsService.baseWidth; + const targetHeight = this.data.height * this.videoSettingsService.baseHeight; const targetAspect = targetWidth / targetHeight; const input = context.sceneItem.getObsInput(); let resolution: IResolution; @@ -108,8 +108,8 @@ export class WebcamNode extends Node { // This selects the video device and picks the best resolution. // It should not be performed if context.existing is true performInitialSetup(item: SceneItem) { - const targetWidth = this.data.width * this.videoService.baseWidth; - const targetHeight = this.data.height * this.videoService.baseHeight; + const targetWidth = this.data.width * this.videoSettingsService.baseWidth; + const targetHeight = this.data.height * this.videoSettingsService.baseHeight; const targetAspect = targetWidth / targetHeight; const input = item.getObsInput(); diff --git a/app/services/scene-collections/nodes/root.ts b/app/services/scene-collections/nodes/root.ts index cd88c5610bf4..5432a6d258ae 100644 --- a/app/services/scene-collections/nodes/root.ts +++ b/app/services/scene-collections/nodes/root.ts @@ -9,6 +9,7 @@ import { VideoService } from 'services/video'; import { StreamingService } from 'services/streaming'; import { OS } from 'util/operating-systems'; import { GuestCamNode } from './guest-cam'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { DualOutputService } from 'services/dual-output'; import { SettingsService } from 'services/settings'; import { SceneCollectionsService } from '../scene-collections'; @@ -56,6 +57,7 @@ export class RootNode extends Node { @Inject() videoService: VideoService; @Inject() streamingService: StreamingService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() dualOutputService: DualOutputService; @Inject() settingsService: SettingsService; @Inject() sceneCollectionsService: SceneCollectionsService; @@ -82,8 +84,8 @@ export class RootNode extends Node { hotkeys, guestCam, nodeMap, - baseResolution: this.videoService.baseResolutions?.horizontal, - baseResolutions: this.videoService.baseResolutions, + baseResolution: this.videoSettingsService.baseResolutions?.horizontal, + baseResolutions: this.videoSettingsService.baseResolutions, selectiveRecording: this.streamingService.state.selectiveRecording, dualOutputMode: this.dualOutputService.views.dualOutputMode, operatingSystem: process.platform as OS, @@ -95,24 +97,51 @@ export class RootNode extends Node { * This if/else prevents an error by guaranteeing a video context exists. */ async load(): Promise { - this.videoService.setBaseResolution(this.data.baseResolutions); - this.streamingService.setSelectiveRecording(!!this.data.selectiveRecording); - this.streamingService.setDualOutputMode(this.data.dualOutputMode); + if (!this.videoSettingsService.contexts.horizontal) { + const establishedContext = this.videoSettingsService.establishedContext.subscribe( + async () => { + this.videoService.setBaseResolution(this.data.baseResolutions); + this.streamingService.setSelectiveRecording(!!this.data.selectiveRecording); + this.streamingService.setDualOutputMode(this.data.dualOutputMode); - if (this.data.nodeMap) { - await this.data.nodeMap.load(); - } + await this.data.transitions.load(); + await this.data.sources.load({}); + await this.data.scenes.load({}); - await this.data.transitions.load(); - await this.data.sources.load({}); - await this.data.scenes.load({}); + if (this.data.nodeMap) { + await this.data.nodeMap.load(); + } - if (this.data.hotkeys) { - await this.data.hotkeys.load({}); - } + if (this.data.hotkeys) { + await this.data.hotkeys.load({}); + } + + if (this.data.guestCam) { + await this.data.guestCam.load(); + } + establishedContext.unsubscribe(); + }, + ); + } else { + this.videoService.setBaseResolution(this.data.baseResolutions); + this.streamingService.setSelectiveRecording(!!this.data.selectiveRecording); + this.streamingService.setDualOutputMode(this.data.dualOutputMode); + + if (this.data.nodeMap) { + await this.data.nodeMap.load(); + } + + await this.data.transitions.load(); + await this.data.sources.load({}); + await this.data.scenes.load({}); + + if (this.data.hotkeys) { + await this.data.hotkeys.load({}); + } - if (this.data.guestCam) { - await this.data.guestCam.load(); + if (this.data.guestCam) { + await this.data.guestCam.load(); + } } } @@ -124,11 +153,11 @@ export class RootNode extends Node { // Added baseResolution in version 3 if (version < 3) { - this.data.baseResolution = this.videoService.baseResolution; + this.data.baseResolution = this.videoSettingsService.baseResolution; } // Added multiple displays with individual base resolutions in version 4 if (version < 4) { - this.data.baseResolutions = this.videoService.baseResolutions; + this.data.baseResolutions = this.videoSettingsService.baseResolutions; } } } diff --git a/app/services/scene-collections/nodes/scene-items.ts b/app/services/scene-collections/nodes/scene-items.ts index 137ca763f6d4..57d25b2bf62a 100644 --- a/app/services/scene-collections/nodes/scene-items.ts +++ b/app/services/scene-collections/nodes/scene-items.ts @@ -3,7 +3,7 @@ import { EBlendingMethod, EBlendingMode, EScaleType, Scene, ScenesService } from import { HotkeysNode } from './hotkeys'; import { SourcesService } from '../../sources'; import { Inject } from '../../core/injector'; -import { TDisplayType, VideoService } from 'services/video'; +import { TDisplayType, VideoSettingsService } from 'services/settings-v2'; import { DualOutputService } from 'services/dual-output'; interface ISchema { @@ -59,8 +59,8 @@ export class SceneItemsNode extends Node { @Inject('DualOutputService') dualOutputService: DualOutputService; - @Inject('VideoService') - videoService: VideoService; + @Inject('VideoSettingsService') + videoSettingsService: VideoSettingsService; getItems(context: IContext) { return context.scene.getNodes().slice().reverse(); @@ -143,7 +143,7 @@ export class SceneItemsNode extends Node { // but if the scene item already has a display assigned, skip it if (this.dualOutputService.views.hasNodeMap(context.scene.id)) { // nodes must be assigned to a context, so if it doesn't exist, establish it - this.videoService.validateVideoContext(); + this.videoSettingsService.validateVideoContext(); const nodeMap = this.dualOutputService.views.sceneNodeMaps[context.scene.id]; diff --git a/app/services/scene-collections/scene-collections.ts b/app/services/scene-collections/scene-collections.ts index b2b3ff1bcc38..ae4a4d42e6fd 100644 --- a/app/services/scene-collections/scene-collections.ts +++ b/app/services/scene-collections/scene-collections.ts @@ -43,7 +43,7 @@ import * as remote from '@electron/remote'; import { GuestCamNode } from './nodes/guest-cam'; import { DualOutputService } from 'services/dual-output'; import { NodeMapNode } from './nodes/node-map'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2'; import { WidgetsService, WidgetType } from 'services/widgets'; const uuid = window['require']('uuid/v4'); @@ -93,7 +93,7 @@ export class SceneCollectionsService extends Service implements ISceneCollection @Inject() transitionsService: TransitionsService; @Inject() streamingService: StreamingService; @Inject() dualOutputService: DualOutputService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() private defaultHardwareService: DefaultHardwareService; @Inject() private widgetsService: WidgetsService; @@ -1246,7 +1246,7 @@ export class SceneCollectionsService extends Service implements ISceneCollection */ initNodeMaps(sceneNodeMap?: { [sceneId: string]: Dictionary }) { - this.videoService.validateVideoContext(); + this.videoSettingsService.validateVideoContext(); if (!this.activeCollection) return; diff --git a/app/services/scenes/scene-folder.ts b/app/services/scenes/scene-folder.ts index 05816c9507a8..13d848a5d01a 100644 --- a/app/services/scenes/scene-folder.ts +++ b/app/services/scenes/scene-folder.ts @@ -10,7 +10,7 @@ import { TSceneNodeType } from './scenes'; import { ServiceHelper } from 'services/core'; import compact from 'lodash/compact'; import { assertIsDefined } from '../../util/properties-type-guards'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; @ServiceHelper('ScenesService') export class SceneItemFolder extends SceneItemNode { diff --git a/app/services/scenes/scene-item.ts b/app/services/scenes/scene-item.ts index b0ad3a1f6726..91f85f5b6d95 100644 --- a/app/services/scenes/scene-item.ts +++ b/app/services/scenes/scene-item.ts @@ -2,7 +2,7 @@ import merge from 'lodash/merge'; import { mutation, Inject } from 'services'; import Utils from '../utils'; import { SourcesService, TSourceType, ISource } from 'services/sources'; -import { VideoService, TDisplayType } from 'services/video'; +import { VideoService } from 'services/video'; import { ScalableRectangle, CenteringAxis, @@ -28,6 +28,7 @@ import { Rect } from '../../util/rect'; import { TSceneNodeType } from './scenes'; import { ServiceHelper, ExecuteInWorkerProcess } from 'services/core'; import { assertIsDefined } from '../../util/properties-type-guards'; +import { VideoSettingsService, TDisplayType } from 'services/settings-v2'; /** * A SceneItem is a source that contains @@ -92,6 +93,7 @@ export class SceneItem extends SceneItemNode { @Inject() protected scenesService: ScenesService; @Inject() private sourcesService: SourcesService; @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; constructor(sceneId: string, sceneItemId: string, sourceId: string) { super(); @@ -106,7 +108,9 @@ export class SceneItem extends SceneItemNode { Utils.applyProxy(this, this.state); if (this.type === 'scene') { - const baseResolutions = this.videoService.baseResolutions[this.display ?? 'horizontal']; + const baseResolutions = this.videoSettingsService.baseResolutions[ + this.display ?? 'horizontal' + ]; assertIsDefined(baseResolutions); this.baseWidth = baseResolutions.baseWidth ?? this.width; @@ -295,9 +299,9 @@ export class SceneItem extends SceneItemNode { // guarantee vertical context exists to prevent null errors if (display === 'vertical') { - this.videoService.validateVideoContext('vertical'); + this.videoSettingsService.validateVideoContext('vertical'); } - const context = this.videoService.contexts[display]; + const context = this.videoSettingsService.contexts[display]; const obsSceneItem = this.getObsSceneItem(); obsSceneItem.video = context as obs.IVideo; diff --git a/app/services/scenes/scene-node.ts b/app/services/scenes/scene-node.ts index 390e7808eabf..a31ad6fb36d7 100644 --- a/app/services/scenes/scene-node.ts +++ b/app/services/scenes/scene-node.ts @@ -14,7 +14,7 @@ import { import { SelectionService } from 'services/selection'; import { assertIsDefined } from 'util/properties-type-guards'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export function isFolder(node: SceneItemNode): node is SceneItemFolder { return node.sceneNodeType === 'folder'; diff --git a/app/services/scenes/scene.ts b/app/services/scenes/scene.ts index 1c1543a05f71..e5acf46cedcb 100644 --- a/app/services/scenes/scene.ts +++ b/app/services/scenes/scene.ts @@ -23,7 +23,7 @@ import * as fs from 'fs'; import * as path from 'path'; import uuid from 'uuid/v4'; import { assertIsDefined } from 'util/properties-type-guards'; -import { VideoService, TDisplayType } from 'services/video'; +import { VideoSettingsService, TDisplayType } from 'services/settings-v2'; import { DualOutputService } from 'services/dual-output'; import { SceneCollectionsService } from 'services/scene-collections'; @@ -45,7 +45,7 @@ export class Scene { @Inject() private scenesService: ScenesService; @Inject() private sourcesService: SourcesService; @Inject() private selectionService: SelectionService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private dualOutputService: DualOutputService; @Inject() private sceneCollectionsService: SceneCollectionsService; @@ -230,7 +230,8 @@ export class Scene { const display = options?.display ?? 'horizontal'; // assign context to scene item - const context = this.videoService.contexts[display] ?? this.videoService.contexts.horizontal; + const context = + this.videoSettingsService.contexts[display] ?? this.videoSettingsService.contexts.horizontal; this.ADD_SOURCE_TO_SCENE( sceneItemId, diff --git a/app/services/scenes/scenes.ts b/app/services/scenes/scenes.ts index 5b3573478783..fe8dfdff32c6 100644 --- a/app/services/scenes/scenes.ts +++ b/app/services/scenes/scenes.ts @@ -15,7 +15,7 @@ import namingHelpers from 'util/NamingHelpers'; import uuid from 'uuid/v4'; import { DualOutputService } from 'services/dual-output'; import { SceneCollectionsService } from 'services/scene-collections'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2/video'; import { ExecuteInWorkerProcess, InitAfter, ViewHandler } from 'services/core'; export type TSceneNodeModel = ISceneItem | ISceneItemFolder; diff --git a/app/services/selection/index.ts b/app/services/selection/index.ts index c20111334c88..eecaef634934 100644 --- a/app/services/selection/index.ts +++ b/app/services/selection/index.ts @@ -11,7 +11,7 @@ import { Selection } from './selection'; import { ViewHandler } from 'services/core'; import { GlobalSelection } from './global-selection'; import { DualOutputService } from 'app-services'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export { Selection, GlobalSelection }; diff --git a/app/services/selection/selection.ts b/app/services/selection/selection.ts index bbf21fe2d4aa..d6bbc1de1b5a 100644 --- a/app/services/selection/selection.ts +++ b/app/services/selection/selection.ts @@ -20,7 +20,7 @@ import { Rect } from 'util/rect'; import { AnchorPoint, AnchorPositions, CenteringAxis } from 'util/ScalableRectangle'; import { ISelectionState, TNodesList } from './index'; import { DualOutputService } from 'services/dual-output'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; /** * Helper for working with multiple sceneItems diff --git a/app/services/settings-v2/default-settings-data.ts b/app/services/settings-v2/default-settings-data.ts new file mode 100644 index 000000000000..81c82a56c9f7 --- /dev/null +++ b/app/services/settings-v2/default-settings-data.ts @@ -0,0 +1,15 @@ +import { EVideoFormat, EColorSpace, ERangeType, EScaleType, EFPSType } from 'obs-studio-node'; + +export const verticalDisplayData = { + fpsNum: 30, + fpsDen: 1, + baseWidth: 720, + baseHeight: 1280, + outputWidth: 720, + outputHeight: 1280, + outputFormat: EVideoFormat.I420, + colorspace: EColorSpace.CS709, + range: ERangeType.Full, + scaleType: EScaleType.Bilinear, + fpsType: EFPSType.Integer, +}; diff --git a/app/services/settings-v2/index.ts b/app/services/settings-v2/index.ts new file mode 100644 index 000000000000..0bc8d05f8042 --- /dev/null +++ b/app/services/settings-v2/index.ts @@ -0,0 +1,2 @@ +export * from './default-settings-data'; +export * from './video'; diff --git a/app/services/settings-v2/video.ts b/app/services/settings-v2/video.ts new file mode 100644 index 000000000000..2a31f6d191f5 --- /dev/null +++ b/app/services/settings-v2/video.ts @@ -0,0 +1,551 @@ +import { debounce } from 'lodash-decorators'; +import { Inject } from 'services/core/injector'; +import { mutation, StatefulService } from '../core/stateful-service'; +import { + IVideoInfo, + EScaleType, + EFPSType, + IVideo, + VideoFactory, + Video, + EVideoFormat, + EColorSpace, + ERangeType, +} from '../../../obs-api'; +import { DualOutputService } from 'services/dual-output'; +import { SettingsService } from 'services/settings'; +import { OutputSettingsService } from 'services/settings/output'; +import { Subject } from 'rxjs'; + +/** + * Display Types + * + * Add display type options by adding the display name to the displays array + * and the context name to the context name map. + */ +const displays = ['horizontal', 'vertical'] as const; +export type TDisplayType = typeof displays[number]; + +export interface IVideoSetting { + horizontal: IVideoInfo; + vertical: IVideoInfo; +} + +export type IVideoInfoValue = + | number + | EVideoFormat + | EColorSpace + | ERangeType + | EScaleType + | EFPSType; + +export interface IVideoSettingFormatted { + baseRes: string; + outputRes: string; + scaleType: EScaleType; + fpsType: EFPSType; + fpsCom: string; + fpsNum: number; + fpsDen: number; + fpsInt: number; +} + +export enum ESettingsVideoProperties { + 'baseRes' = 'Base', + 'outputRes' = 'Output', + 'scaleType' = 'ScaleType', + 'fpsType' = 'FPSType', + 'fpsCom' = 'FPSCommon', + 'fpsNum' = 'FPSNum', + 'fpsDen' = 'FPSDen', + 'fpsInt' = 'FPSInt', +} + +const scaleTypeNames = { + 0: 'Disable', + 1: 'Point', + 2: 'Bicubic', + 3: 'Bilinear', + 4: 'Lanczos', + 5: 'Area', +}; + +const fpsTypeNames = { + 0: 'Common', + 1: 'Integer', + 2: 'Fractional', +}; +export function invalidFps(num: number, den: number) { + return num / den > 1000 || num / den < 1; +} + +export class VideoSettingsService extends StatefulService { + @Inject() dualOutputService: DualOutputService; + @Inject() settingsService: SettingsService; + @Inject() outputSettingsService: OutputSettingsService; + + initialState = { + horizontal: null as IVideoInfo, + vertical: null as IVideoInfo, + }; + + establishedContext = new Subject(); + + init() { + this.establishVideoContext(); + + if (this.dualOutputService.views.activeDisplays.vertical) { + this.establishVideoContext('vertical'); + } + + this.establishedContext.next(); + } + + contexts = { + horizontal: null as IVideo, + vertical: null as IVideo, + }; + + get values() { + return { + horizontal: this.formatVideoSettings('horizontal'), + vertical: this.formatVideoSettings('vertical'), + }; + } + + /** + * The below provides a default base resolution + * @remark replaces the legacy base resolution in the video service + */ + get baseResolution() { + return this.baseResolutions.horizontal; + } + + /** + * The below provides a default base width + * @remark replaces the legacy base width in the video service + */ + get baseWidth() { + return this.baseResolutions.horizontal.baseWidth; + } + + /** + * The below provides a default base width + * @remark replaces the legacy base width in the video service + */ + get baseHeight() { + return this.baseResolutions.horizontal.baseHeight; + } + + /** + * The below conditionals are to prevent undefined errors on app startup + */ + get baseResolutions() { + // to prevent any possible undefined errors on load in the event that the root node + // attempts to load before the first video context has finished establishing + // the below are fallback dimensions + + return { + horizontal: { + baseWidth: this.state.horizontal?.baseWidth ?? 1920, + baseHeight: this.state.horizontal?.baseHeight ?? 1080, + }, + vertical: { + baseWidth: this.state.vertical?.baseWidth ?? 720, + baseHeight: this.state.vertical?.baseHeight ?? 1280, + }, + }; + } + + get outputResolutions() { + return { + horizontal: { + outputWidth: this.state.horizontal?.outputWidth, + outputHeight: this.state.horizontal?.outputHeight, + }, + vertical: { + outputWidth: this.state.vertical?.outputWidth, + outputHeight: this.state.vertical?.outputHeight, + }, + }; + } + + /** + * Format video settings for the video settings form + * + * @param display - Optional, the display for the settings + * @returns Settings formatted for the video settings form + */ + formatVideoSettings(display: TDisplayType = 'horizontal', typeStrings?: boolean) { + // use vertical display setting as a failsafe to prevent null errors + const settings = + this.state[display] ?? + this.dualOutputService.views.videoSettings[display] ?? + this.dualOutputService.views.videoSettings.vertical; + + const scaleType = typeStrings ? scaleTypeNames[settings?.scaleType] : settings?.scaleType; + const fpsType = typeStrings ? fpsTypeNames[settings?.fpsType] : settings?.fpsType; + + return { + baseRes: `${settings?.baseWidth}x${settings?.baseHeight}`, + outputRes: `${settings?.outputWidth}x${settings?.outputHeight}`, + scaleType, + fpsType, + fpsCom: `${settings?.fpsNum}-${settings?.fpsDen}`, + fpsNum: settings?.fpsNum, + fpsDen: settings?.fpsDen, + fpsInt: settings?.fpsNum, + }; + } + + /** + * Load legacy video settings from cache. + * + * @remarks + * Ideally, the first time the user opens the app after the settings + * have migrated to being stored on the front end, load the settings from + * the legacy settings. Because the legacy settings are just values from basic.ini + * if the user is starting from a clean cache, there will be no such file. + * In that case, load from the video property. + + * Additionally, because this service is loaded lazily, calling this function elsewhere + * before the service has been initiated will call the function twice. + * To prevent errors, just return if both properties are null because + * the function will be called again as a part of establishing the context. + * @param display - Optional, the context's display name + */ + + loadLegacySettings(display: TDisplayType = 'horizontal') { + const legacySettings = this.contexts[display]?.legacySettings; + const videoSettings = this.contexts[display]?.video; + + if (!legacySettings && !videoSettings) return; + + if (legacySettings?.baseHeight === 0 || legacySettings?.baseWidth === 0) { + // return if null for the same reason as above + if (!videoSettings) return; + + Object.keys(videoSettings).forEach((key: keyof IVideoInfo) => { + this.SET_VIDEO_SETTING(key, videoSettings[key]); + this.dualOutputService.setVideoSetting({ [key]: videoSettings[key] }, display); + }); + } else { + // return if null for the same reason as above + if (!legacySettings) return; + Object.keys(legacySettings).forEach((key: keyof IVideoInfo) => { + this.SET_VIDEO_SETTING(key, legacySettings[key]); + this.dualOutputService.setVideoSetting({ [key]: legacySettings[key] }, display); + }); + this.contexts[display].video = this.contexts[display].legacySettings; + } + + if (invalidFps(this.contexts[display].video.fpsNum, this.contexts[display].video.fpsDen)) { + this.createDefaultFps(display); + } + } + + /** + * Migrate settings from legacy settings or obs + * + * @param display - Optional, the context's display name + */ + migrateSettings(display: TDisplayType = 'horizontal') { + /** + * If this is the first time starting the app set default settings for horizontal context + */ + if (display === 'horizontal' && !this.dualOutputService.views.videoSettings?.horizontal) { + this.loadLegacySettings(); + this.contexts.horizontal.video = this.contexts.horizontal.legacySettings; + } else { + // otherwise, load them from the dual output service + const settings = this.dualOutputService.views.videoSettings[display]; + + Object.keys(settings).forEach((key: keyof IVideoInfo) => { + this.SET_VIDEO_SETTING(key, settings[key], display); + }); + this.contexts[display].video = settings; + + if (invalidFps(this.contexts[display].video.fpsNum, this.contexts[display].video.fpsDen)) { + this.createDefaultFps(display); + } + } + + this.SET_VIDEO_CONTEXT(display, this.contexts[display].video); + } + + /** + * Establish the obs video context + * + * @remarks + * Many startup errors in other services will result from a context not being established before + * the service initiates. + * + * @param display - Optional, the context's display name + * @returns Boolean denoting success + */ + establishVideoContext(display: TDisplayType = 'horizontal') { + if (this.contexts[display]) return; + this.SET_VIDEO_CONTEXT(display); + this.contexts[display] = VideoFactory.create(); + this.migrateSettings(display); + + this.contexts[display].video = this.state[display]; + this.contexts[display].legacySettings = this.state[display]; + Video.video = this.state.horizontal; + Video.legacySettings = this.state.horizontal; + + if (display === 'vertical') { + // ensure vertical context as the same fps settings as the horizontal context + const updated = this.syncFPSSettings(); + if (updated) { + this.settingsService.refreshVideoSettings(); + } + + // ensure that the v1 video resolution settings are the same as the horizontal context + this.settingsService.setSettingValue('Video', 'Base', `${this.baseWidth}x${this.baseHeight}`); + this.settingsService.setSettingValue( + 'Video', + 'Output', + `${this.outputResolutions.horizontal.outputWidth}x${this.outputResolutions.horizontal.outputHeight}`, + ); + } + + return !!this.contexts[display]; + } + + validateVideoContext(display: TDisplayType = 'vertical') { + if (!this.contexts[display]) { + this.establishVideoContext(display); + } + } + + createDefaultFps(display: TDisplayType = 'horizontal') { + this.setVideoSetting('fpsNum', 30, display); + this.setVideoSetting('fpsDen', 1, display); + } + + /** + * Migrate optimized settings to vertical context + */ + migrateAutoConfigSettings() { + // load optimized settings onto horizontal context + this.loadLegacySettings('horizontal'); + + if (this.contexts?.vertical) { + // add optimized settings to vertical context + const newVerticalSettings = { + ...this.contexts.horizontal.video, + baseWidth: this.state.vertical.baseWidth, + baseHeight: this.state.vertical.baseHeight, + outputWidth: this.state.vertical.outputWidth, + outputHeight: this.state.vertical.outputHeight, + }; + this.updateVideoSettings(newVerticalSettings, 'vertical'); + + // update the Video settings property to the horizontal context dimensions + const base = `${this.state.horizontal.baseWidth}x${this.state.horizontal.baseHeight}`; + const output = `${this.state.horizontal.outputWidth}x${this.state.horizontal.outputHeight}`; + this.settingsService.setSettingValue('Video', 'Base', base); + this.settingsService.setSettingValue('Video', 'Output', output); + } else { + // if there is no vertical context, only update persisted settings for vertical context + const horizontalScaleType = this.contexts.horizontal.video.scaleType; + const horizontalFpsType = this.contexts.horizontal.video.fpsType; + const horizontalFpsNum = this.contexts.horizontal.video.fpsNum; + const horizontalFpsDen = this.contexts.horizontal.video.fpsDen; + + this.dualOutputService.setVideoSetting({ scaleType: horizontalScaleType }, 'vertical'); + this.dualOutputService.setVideoSetting({ fpsType: horizontalFpsType }, 'vertical'); + this.dualOutputService.setVideoSetting({ fpsNum: horizontalFpsNum }, 'vertical'); + this.dualOutputService.setVideoSetting({ fpsDen: horizontalFpsDen }, 'vertical'); + } + } + + /** + * Confirm video setting dimensions in settings + * @remarks Primarily used with the optimizer to ensure the horizontal context dimensions + * are the dimensions in the settings + */ + confirmVideoSettingDimensions() { + const [baseWidth, baseHeight] = this.settingsService.views.values.Video.Base.split('x'); + const [outputWidth, outputHeight] = this.settingsService.views.values.Video.Output.split('x'); + + if ( + Number(baseWidth) !== this.state.horizontal.baseWidth || + Number(baseHeight) !== this.state.horizontal.baseHeight + ) { + const base = `${this.state.horizontal.baseWidth}x${this.state.horizontal.baseHeight}`; + this.settingsService.setSettingValue('Video', 'Base', base); + } + + if ( + Number(outputWidth) !== this.state.horizontal.outputWidth || + Number(outputHeight) !== this.state.horizontal.outputHeight + ) { + const output = `${this.state.horizontal.outputWidth}x${this.state.horizontal.outputHeight}`; + this.settingsService.setSettingValue('Video', 'Output', output); + } + } + + @debounce(200) + updateObsSettings(display: TDisplayType = 'horizontal') { + // confirm all vertical fps settings are synced to the horizontal fps settings + // update contexts to values on state + this.contexts[display].video = this.state[display]; + this.contexts[display].legacySettings = this.state[display]; + } + + updateVideoSettings(patch: Partial, display: TDisplayType = 'horizontal') { + const newVideoSettings = { ...this.state[display], ...patch }; + + this.SET_VIDEO_CONTEXT(display, newVideoSettings); + this.updateObsSettings(display); + + // also update the persisted settings + this.dualOutputService.updateVideoSettings(newVideoSettings, display); + } + + /** + * Set Video Settings + * @remark V2 api. This ealso updates the video settings in the V1 api. + * @param key - name of the video setting, must be key of obs video info + * @param value - value of the video setting, must be valid value of obs video info + * @param display - (optional) name of context (aka display) to apply setting to. Default is horizontal. + */ + setVideoSetting( + key: keyof IVideoInfo, + value: IVideoInfoValue, + display: TDisplayType = 'horizontal', + ) { + this.SET_VIDEO_SETTING(key, value, display); + this.updateObsSettings(display); + + // also update the persisted settings + this.dualOutputService.setVideoSetting({ [key]: value }, display); + + // refresh v1 settings + this.settingsService.refreshVideoSettings(); + } + + setSettings(settings: Partial, display: TDisplayType = 'horizontal') { + this.SET_SETTINGS(settings, display); + + this.updateObsSettings(display); + + // also update the persisted settings + this.dualOutputService.setVideoSetting(settings, display); + + // refresh v1 settings + this.settingsService.refreshVideoSettings(); + } + + /** + * Sync FPS settings between contexts + * @remark - If the fps settings are not the same for both contexts, the output settings + * is working with mismatched values, which contributes to an issue with speed and duration + * being out of sync. The other factor in this issue is if the latest obs settings are not + * loaded into the store. When a context is created, the dual output service syncs the vertical + * fps settings with the horizontal one. But any time we make a change to the fps settings, + * we need to apply this change to both contexts to keep them synced. + * @param - Currently, we must confirm fps settings are synced before start streaming + */ + syncFPSSettings(updateContexts?: boolean): boolean { + const fpsSettings = ['scaleType', 'fpsType', 'fpsCom', 'fpsNum', 'fpsDen', 'fpsInt']; + + // update persisted local settings if the vertical context does not exist + const verticalVideoSetting: IVideoInfo = this.contexts.vertical + ? this.state.vertical + : this.dualOutputService.views.videoSettings.vertical; + + let updated = false; + + fpsSettings.forEach((setting: keyof IVideoInfo) => { + const hasSameVideoSetting = + this.contexts.horizontal.video[setting as keyof IVideoInfo] === + verticalVideoSetting[setting as keyof IVideoInfo]; + let shouldUpdate = hasSameVideoSetting; + + // if the vertical context has been established, also compare legacy settings + if (this.contexts.vertical) { + const hasSameLegacySetting = + this.contexts.horizontal.legacySettings[setting] === + this.contexts.vertical.legacySettings[setting]; + shouldUpdate = !hasSameVideoSetting || !hasSameLegacySetting; + } + // sync the horizontal setting to the vertical setting if they are not the same + if (shouldUpdate) { + const value = this.state.horizontal[setting]; + // always update persisted setting + this.dualOutputService.setVideoSetting({ [setting]: value }, 'vertical'); + + // update state if the vertical context exists + if (this.contexts.vertical) { + this.SET_VIDEO_SETTING(setting, value, 'vertical'); + } + + updated = true; + } + }); + + // only update the vertical context if it exists + if ((updateContexts || updated) && this.contexts.vertical) { + this.contexts.vertical.video = this.state.vertical; + this.contexts.vertical.legacySettings = this.state.vertical; + } + return updated; + } + + /** + * Shut down the video settings service + * + * @remarks + * Each context must be destroyed when shutting down the app to prevent errors + */ + shutdown() { + displays.forEach(display => { + if (this.contexts[display]) { + // save settings as legacy settings + this.contexts[display].legacySettings = this.state[display]; + + // destroy context + this.contexts[display].destroy(); + this.contexts[display] = null as IVideo; + this.DESTROY_VIDEO_CONTEXT(display); + } + }); + } + + @mutation() + private DESTROY_VIDEO_CONTEXT(display: TDisplayType = 'horizontal') { + this.state[display] = null as IVideoInfo; + } + + @mutation() + private SET_VIDEO_SETTING( + key: keyof IVideoInfo, + value: IVideoInfoValue, + display: TDisplayType = 'horizontal', + ) { + this.state[display] = { + ...this.state[display], + [key]: value, + }; + } + + @mutation() + private SET_SETTINGS(settings: Partial, display: TDisplayType = 'horizontal') { + this.state[display] = { + ...this.state[display], + ...settings, + }; + } + + @mutation() + private SET_VIDEO_CONTEXT(display: TDisplayType = 'horizontal', settings?: IVideoInfo) { + if (settings) { + this.state[display] = settings; + } else { + this.state[display] = {} as IVideoInfo; + } + } +} diff --git a/app/services/settings/output/output-settings.ts b/app/services/settings/output/output-settings.ts index a788fd9469a5..c6b9f23319ee 100644 --- a/app/services/settings/output/output-settings.ts +++ b/app/services/settings/output/output-settings.ts @@ -1,6 +1,6 @@ import { Service } from 'services/core/service'; import { ISettingsSubCategory, SettingsService } from 'services/settings'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; import { HighlighterService } from 'services/highlighter'; import { Inject } from 'services/core/injector'; import { Dictionary } from 'vuex'; @@ -185,7 +185,7 @@ export function obsEncoderToEncoderFamily( export class OutputSettingsService extends Service { @Inject() private settingsService: SettingsService; @Inject() private audioService: AudioService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private highlighterService: HighlighterService; /** @@ -378,8 +378,8 @@ export class OutputSettingsService extends Service { if (settingsPatch.inputResolution) { const [width, height] = settingsPatch.inputResolution.split('x'); - this.videoService.setVideoSetting('baseWidth', Number(width)); - this.videoService.setVideoSetting('baseHeight', Number(height)); + this.videoSettingsService.setVideoSetting('baseWidth', Number(width)); + this.videoSettingsService.setVideoSetting('baseHeight', Number(height)); } if (settingsPatch.streaming) { diff --git a/app/services/settings/streaming/stream-settings.ts b/app/services/settings/streaming/stream-settings.ts index f2e0afd5531d..07e40ef30f64 100644 --- a/app/services/settings/streaming/stream-settings.ts +++ b/app/services/settings/streaming/stream-settings.ts @@ -9,7 +9,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { TwitchService } from 'services/platforms/twitch'; import { PlatformAppsService } from 'services/platform-apps'; import { IGoLiveSettings, IPlatformFlags } from 'services/streaming'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2/video'; import Vue from 'vue'; import { IVideo } from 'obs-studio-node'; import { DualOutputService } from 'services/dual-output'; diff --git a/app/services/sources/sources-api.ts b/app/services/sources/sources-api.ts index 07e9bfdd6df5..b852cca69878 100644 --- a/app/services/sources/sources-api.ts +++ b/app/services/sources/sources-api.ts @@ -4,7 +4,7 @@ import { WidgetType } from 'services/widgets'; import { Observable } from 'rxjs'; import { IAudioSource } from 'services/audio'; import { EDeinterlaceFieldOrder, EDeinterlaceMode } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export interface ISource { sourceId: string; diff --git a/app/services/sources/sources.ts b/app/services/sources/sources.ts index c3acf127c8cd..16de72378b94 100644 --- a/app/services/sources/sources.ts +++ b/app/services/sources/sources.ts @@ -37,7 +37,7 @@ import { IconLibraryManager } from './properties-managers/icon-library-manager'; import { assertIsDefined } from 'util/properties-type-guards'; import { UsageStatisticsService } from 'services/usage-statistics'; import { SourceFiltersService } from 'services/source-filters'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2'; import { CustomizationService } from '../customization'; import { EAvailableFeatures, IncrementalRolloutService } from '../incremental-rollout'; import { EMonitoringType, EDeinterlaceMode, EDeinterlaceFieldOrder } from '../../../obs-api'; @@ -168,7 +168,7 @@ class SourcesViews extends ViewHandler { } } -@InitAfter('VideoService') +@InitAfter('VideoSettingsService') export class SourcesService extends StatefulService { static initialState = { sources: {}, @@ -190,7 +190,7 @@ export class SourcesService extends StatefulService { @Inject() private defaultHardwareService: DefaultHardwareService; @Inject() private usageStatisticsService: UsageStatisticsService; @Inject() private sourceFiltersService: SourceFiltersService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private customizationService: CustomizationService; @Inject() private incrementalRolloutService: IncrementalRolloutService; @Inject() private guestCamService: GuestCamService; @@ -363,10 +363,10 @@ export class SourcesService extends StatefulService { const type: TSourceType = obsInput.id as TSourceType; const managerType = options.propertiesManager || 'default'; const width = options?.display - ? this.videoService.baseResolutions[options?.display].baseWidth + ? this.videoSettingsService.baseResolutions[options?.display].baseWidth : obsInput.width; const height = options?.display - ? this.videoService.baseResolutions[options?.display].baseHeight + ? this.videoSettingsService.baseResolutions[options?.display].baseHeight : obsInput.height; this.ADD_SOURCE({ diff --git a/app/services/streaming/streaming-api.ts b/app/services/streaming/streaming-api.ts index c31ae591892a..9baec256eb06 100644 --- a/app/services/streaming/streaming-api.ts +++ b/app/services/streaming/streaming-api.ts @@ -11,7 +11,7 @@ import { IKickStartStreamOptions } from 'services/platforms/kick'; import { ITwitterStartStreamOptions } from 'services/platforms/twitter'; import { IInstagramStartStreamOptions } from 'services/platforms/instagram'; import { IVideo } from 'obs-studio-node'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export enum EStreamingState { Offline = 'offline', diff --git a/app/services/streaming/streaming-view.ts b/app/services/streaming/streaming-view.ts index 139866f28b2a..e363ae5459d3 100644 --- a/app/services/streaming/streaming-view.ts +++ b/app/services/streaming/streaming-view.ts @@ -16,7 +16,7 @@ import cloneDeep from 'lodash/cloneDeep'; import difference from 'lodash/difference'; import { Services } from '../../components-react/service-provider'; import { getDefined } from '../../util/properties-type-guards'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; /** * The stream info view is responsible for keeping diff --git a/app/services/streaming/streaming.ts b/app/services/streaming/streaming.ts index 954e6ce73caa..92afa3ddacf0 100644 --- a/app/services/streaming/streaming.ts +++ b/app/services/streaming/streaming.ts @@ -29,7 +29,7 @@ import { NotificationsService, } from 'services/notifications'; import { VideoEncodingOptimizationService } from 'services/video-encoding-optimizations'; -import { VideoService, TDisplayType } from 'services/video'; +import { VideoSettingsService, TDisplayType } from 'services/settings-v2/video'; import { StreamSettingsService } from '../settings/streaming'; import { RestreamService, TOutputOrientation } from 'services/restream'; import Utils from 'services/utils'; @@ -97,7 +97,7 @@ export class StreamingService @Inject() private hostsService: HostsService; @Inject() private growService: GrowService; @Inject() private recordingModeService: RecordingModeService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private markersService: MarkersService; @Inject() private dualOutputService: DualOutputService; @Inject() private youtubeService: YoutubeService; @@ -299,7 +299,7 @@ export class StreamingService // preserve user's dual output display setting but correctly go live to custom destinations in single output mode const display = this.views.isDualOutputMode ? destination.display : 'horizontal'; - destination.video = this.videoService.contexts[display]; + destination.video = this.videoSettingsService.contexts[display]; destination.mode = this.views.getDisplayContextName(display); }); @@ -915,8 +915,8 @@ export class StreamingService if (this.views.isDualOutputMode) { // start dual output - const horizontalContext = this.videoService.contexts.horizontal; - const verticalContext = this.videoService.contexts.vertical; + const horizontalContext = this.videoSettingsService.contexts.horizontal; + const verticalContext = this.videoSettingsService.contexts.vertical; NodeObs.OBS_service_setVideoInfo(horizontalContext, 'horizontal'); NodeObs.OBS_service_setVideoInfo(verticalContext, 'vertical'); @@ -940,7 +940,7 @@ export class StreamingService await new Promise(resolve => setTimeout(resolve, 1000)); } else { // start single output - const horizontalContext = this.videoService.contexts.horizontal; + const horizontalContext = this.videoSettingsService.contexts.horizontal; NodeObs.OBS_service_setVideoInfo(horizontalContext, 'horizontal'); NodeObs.OBS_service_startStreaming(); diff --git a/app/services/transitions.ts b/app/services/transitions.ts index a26104246293..9cacb0e1be1e 100644 --- a/app/services/transitions.ts +++ b/app/services/transitions.ts @@ -5,6 +5,7 @@ import { TObsValue, TObsFormData } from 'components/obs/inputs/ObsInput'; import { IListOption } from 'components/shared/inputs'; import { WindowsService } from 'services/windows'; import { ScenesService } from 'services/scenes'; +import { Scene } from 'services/scenes/scene'; import uuid from 'uuid/v4'; import { SceneCollectionsService } from 'services/scene-collections'; import { $t } from 'services/i18n'; @@ -14,9 +15,9 @@ import { isUrl } from '../util/requests'; import { getOS, OS } from 'util/operating-systems'; import { UsageStatisticsService } from './usage-statistics'; import { SourcesService } from 'services/sources'; +import { VideoSettingsService } from './settings-v2'; import { DualOutputService } from './dual-output'; import { NotificationsService, ENotificationType } from './notifications'; -import { VideoService } from 'services/video'; export const TRANSITION_DURATION_MAX = 2_000_000_000; @@ -117,7 +118,7 @@ export class TransitionsService extends StatefulService { @Inject() sceneCollectionsService: SceneCollectionsService; @Inject() usageStatisticsService: UsageStatisticsService; @Inject() sourcesService: SourcesService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() dualOutputService: DualOutputService; @Inject() notificationsService: NotificationsService; @@ -167,7 +168,11 @@ export class TransitionsService extends StatefulService { this.disableStudioMode(); }); - if (!this.studioModeTransition) this.createStudioModeTransition(); + // a video context must be initialized before loading the scene transition + const establishedContext = this.videoSettingsService.establishedContext.subscribe(() => { + if (!this.studioModeTransition) this.createStudioModeTransition(); + establishedContext.unsubscribe(); + }); } enableStudioMode() { diff --git a/app/services/ts-importer.ts b/app/services/ts-importer.ts index c8be69803409..36cd1ec9b0b4 100644 --- a/app/services/ts-importer.ts +++ b/app/services/ts-importer.ts @@ -13,6 +13,7 @@ import { SceneItem } from 'services/scenes'; import { VideoService } from './video'; import Utils from './utils'; import { WidgetType } from './widgets'; +import { VideoSettingsService } from './settings-v2'; interface ITSConfig { graph: { @@ -173,6 +174,7 @@ export class TwitchStudioImporterService extends StatefulService<{ @Inject() sourcesService: SourcesService; @Inject() defaultHardwareService: DefaultHardwareService; @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; static initialState: { isTwitchStudioInstalled: boolean } = { isTwitchStudioInstalled: false, @@ -218,8 +220,8 @@ export class TwitchStudioImporterService extends StatefulService<{ } setupVideo(config: ITSConfig) { - this.videoService.setVideoSetting('baseWidth', config.graphics.canvasWidth); - this.videoService.setVideoSetting('baseHeight', config.graphics.canvasHeight); + this.videoSettingsService.setVideoSetting('baseWidth', config.graphics.canvasWidth); + this.videoSettingsService.setVideoSetting('baseHeight', config.graphics.canvasHeight); } async importScenes(config: ITSConfig) { diff --git a/app/services/video.ts b/app/services/video.ts index a8cc62673735..2a3804b8574f 100644 --- a/app/services/video.ts +++ b/app/services/video.ts @@ -1,13 +1,12 @@ import { Service } from './core/service'; -import { RealmObject } from './realm'; -import { ObjectSchema } from 'realm'; -import { InitAfter } from 'services/core'; +import { StatefulService, InitAfter, mutation } from 'services/core'; import { ISettingsSubCategory, SettingsService } from './settings'; import * as obs from '../../obs-api'; import { Inject } from './core/injector'; import Utils from './utils'; import { WindowsService } from './windows'; import { ScalableRectangle } from '../util/ScalableRectangle'; +import { Subscription } from 'rxjs'; import { DualOutputService } from './dual-output'; import { byOS, OS, getOS } from 'util/operating-systems'; import * as remote from '@electron/remote'; @@ -15,536 +14,7 @@ import { onUnload } from 'util/unload'; import { ISelectionState, SelectionService } from 'services/selection'; import { SourcesService } from 'services/sources'; import { ScenesService } from 'services/scenes'; -import { debounce } from 'lodash-decorators'; -import { Subscription } from 'rxjs'; -import path from 'path'; -import fs from 'fs'; - -/** - * Display Types - * - * Add display type options by adding the display name to the displays array - * and the context name to the context name map. - */ -const displays = ['horizontal', 'vertical'] as const; -export type TDisplayType = typeof displays[number]; - -export interface IVideoSetting { - horizontal: obs.IVideoInfo; - vertical: obs.IVideoInfo; -} - -export type IVideoInfoValue = - | number - | obs.EVideoFormat - | obs.EColorSpace - | obs.ERangeType - | obs.EScaleType - | obs.EFPSType; - -export interface IVideoSettingFormatted { - baseRes: string; - outputRes: string; - scaleType: obs.EScaleType; - fpsType: obs.EFPSType; - fpsCom: string; - fpsNum: number; - fpsDen: number; - fpsInt: number; -} - -// to migrate from the V1 to V2 API, we need to map the old enum to the new API enum -enum EFPSType { - 'Common FPS Value' = 0, - 'Integer FPS Value' = 1, - 'Fractional FPS Value' = 2, -} - -// to migrate from the V1 to V2 API, we need to map the old enum to the new API enum -enum EScaleType { - // Disable, - // Point, - 'bicubic' = 2, - 'bilinear' = 3, - 'lanczos' = 4, - // Area, -} - -export enum ESettingsVideoProperties { - 'baseRes' = 'Base', - 'outputRes' = 'Output', - 'scaleType' = 'ScaleType', - 'fpsType' = 'FPSType', - 'fpsCom' = 'FPSCommon', - 'fpsNum' = 'FPSNum', - 'fpsDen' = 'FPSDen', - 'fpsInt' = 'FPSInt', -} - -const scaleTypeNames = { - 0: 'Disable', - 1: 'Point', - 2: 'Bicubic', - 3: 'Bilinear', - 4: 'Lanczos', - 5: 'Area', -}; - -const fpsTypeNames = { - 0: 'Common', - 1: 'Integer', - 2: 'Fractional', -}; - -export function invalidFps(num: number, den: number) { - return num / den > 1000 || num / den < 1; -} - -type TIVideoInfo = (key: keyof obs.IVideoInfo) => T; -type TScaleTypeNames = keyof typeof scaleTypeNames; -type TFpsTypeNames = keyof typeof fpsTypeNames; - -interface IVideoContextSetting { - video: obs.IVideoInfo; - isActive: boolean; -} -export interface IVideoSettingsState { - horizontal: IVideoContextSetting; - vertical: IVideoContextSetting; -} - -class VideoInfo extends RealmObject implements obs.IVideoInfo { - fpsNum: number; - fpsDen: number; - baseWidth: number; - baseHeight: number; - outputWidth: number; - outputHeight: number; - outputFormat: number; - colorspace: number; - range: number; - scaleType: number; - fpsType: number; - - static schema: ObjectSchema = { - name: 'VideoInfo', - embedded: true, - properties: { - fpsNum: { type: 'int', default: 30 }, - fpsDen: { type: 'int', default: 1 }, - baseWidth: { type: 'int', default: 1920 }, - baseHeight: { type: 'int', default: 1080 }, - outputWidth: { type: 'int', default: 1920 }, - outputHeight: { type: 'int', default: 1080 }, - outputFormat: { type: 'int', default: obs.EVideoFormat.I420 }, - colorspace: { type: 'int', default: obs.EColorSpace.CS709 }, - range: { type: 'int', default: obs.ERangeType.Full }, - scaleType: { type: 'int', default: obs.EScaleType.Bilinear }, - fpsType: { type: 'int', default: obs.EFPSType.Integer }, - }, - }; - - get videoInfo() { - return { - fpsNum: this.fpsNum, - fpsDen: this.fpsDen, - baseWidth: this.baseWidth, - baseHeight: this.baseHeight, - outputWidth: this.outputWidth, - outputHeight: this.outputHeight, - outputFormat: this.outputFormat, - colorspace: this.colorspace, - range: this.range, - scaleType: this.scaleType, - fpsType: this.fpsType, - }; - } - - get baseRes() { - const base = `${this.baseWidth}x${this.baseHeight}`; - return { label: base, value: base }; - } - - get outputRes() { - const output = `${this.outputWidth}x${this.outputHeight}`; - return { label: output, value: output }; - } - - get values() { - const scaleType = { - label: scaleTypeNames[this.scaleType as TScaleTypeNames], - value: this.scaleType, - }; - - const fpsType = { - label: fpsTypeNames[this.fpsType as TFpsTypeNames], - value: this.fpsType, - }; - - return { - baseRes: this.baseRes, - outputRes: this.outputRes, - scaleType, - fpsType, - }; - } -} - -VideoInfo.register({ persist: true }); -class VideoContextSetting extends RealmObject { - video: VideoInfo; - isActive: boolean; - - static schema: ObjectSchema = { - name: 'VideoContextSetting', - embedded: true, - properties: { - video: { type: 'object', objectType: 'VideoInfo', default: {} }, - isActive: { type: 'bool', default: true }, - }, - }; - - get videoInfo(): obs.IVideoInfo { - return this.video.videoInfo; - } - - get baseWidth() { - return this.video.baseWidth; - } - - get baseHeight() { - return this.video.baseHeight; - } - - get baseRes() { - return `${this.baseWidth}x${this.baseHeight}`; - } - - get outputWidth() { - return this.video.outputWidth; - } - - get outputHeight() { - return this.video.outputHeight; - } - - get outputRes() { - return `${this.outputWidth}x${this.outputHeight}`; - } - - get values(): IVideoSettingFormatted { - const settings = this.video; - - return { - baseRes: `${settings.baseWidth}x${settings.baseHeight}`, - outputRes: `${settings.outputWidth}x${settings.outputHeight}`, - scaleType: settings.scaleType, - fpsType: settings.fpsType, - fpsCom: `${settings.fpsNum}-${settings.fpsDen}`, - fpsNum: settings.fpsNum, - fpsDen: settings.fpsDen, - fpsInt: settings.fpsNum, - }; - } -} - -VideoContextSetting.register({ persist: true }); - -export class VideoSettingsState extends RealmObject { - horizontal: VideoContextSetting; - vertical: VideoContextSetting; - - static schema: ObjectSchema = { - name: 'VideoSettingsState', - properties: { - horizontal: { - type: 'object', - objectType: 'VideoContextSetting', - default: {}, - }, - vertical: { - type: 'object', - objectType: 'VideoContextSetting', - default: {}, - }, - }, - }; - - /** - * Fetch Video settings and format for the new API - * @remark Primarily used to migrate legacy settings when creating the realm - * @returns Legacy video settings - */ - fetchLegacySettings() { - // default video settings - let videoSettings = { - fpsNum: 30, - fpsDen: 1, - baseWidth: 1920, - baseHeight: 1080, - outputWidth: 1920, - outputHeight: 1080, - outputFormat: obs.EVideoFormat.I420, - colorspace: obs.EColorSpace.CS709, - range: obs.ERangeType.Full, - scaleType: obs.EScaleType.Bilinear, - fpsType: obs.EFPSType.Integer, - }; - - // try to fetch video settings from video factory - try { - const temporaryVideoContext = obs.VideoFactory.create(); - const videoFactoryVideoSettings = { ...temporaryVideoContext.video }; - const videoFactoryLegacySettings = { ...temporaryVideoContext.legacySettings }; - temporaryVideoContext.destroy(); - - if (videoFactoryVideoSettings) { - videoSettings = videoFactoryVideoSettings; - } else if (videoFactoryLegacySettings) { - videoSettings = videoFactoryLegacySettings; - } - - return videoSettings; - } catch (e: unknown) { - console.warn('Error fetching video settings from video factory', e); - } - - // as a fallback, try to fetch video settings from the old API - try { - const oldAPISettings = obs.NodeObs.OBS_settings_getSettings('Video')?.data[0]?.parameters; - - if (oldAPISettings) { - oldAPISettings.forEach((setting: any) => { - if (!setting.currentValue) return; - switch (setting.name) { - case 'Base': { - const [baseWidth, baseHeight] = setting.currentValue.split('x'); - if (baseWidth === '0' || baseHeight === '0') break; - videoSettings.baseWidth = Number(baseWidth); - videoSettings.baseHeight = Number(baseHeight); - break; - } - case 'Output': { - const [outputWidth, outputHeight] = setting.currentValue.split('x'); - if (outputWidth === '0' || outputHeight === '0') break; - videoSettings.outputWidth = Number(outputWidth); - videoSettings.outputHeight = Number(outputHeight); - break; - } - case 'ScaleType': - videoSettings.scaleType = (EScaleType[ - setting.currentValue - ] as unknown) as obs.EScaleType; - break; - case 'FPSType': - videoSettings.fpsType = (EFPSType[setting.currentValue] as unknown) as obs.EFPSType; - break; - case 'FPSNum': - videoSettings.fpsNum = setting.currentValue; - break; - case 'FPSDen': - videoSettings.fpsDen = setting.currentValue; - break; - default: - break; - } - }); - } - - return videoSettings; - } catch (e: unknown) { - console.warn('Error fetching video settings from video factory', e); - } - - // as a last resort, fetch settings from the local settings file - try { - const filePath = path.join(remote.app.getPath('userData'), 'basic.ini'); - const truePath = path.resolve(filePath); - - const data = fs.readFileSync(truePath).toString(); - - const propertiesToValidate = ['BaseCX', 'BaseCY', 'OutputCX', 'OutputCY']; - - propertiesToValidate.forEach(property => { - const regex = new RegExp(`${property}=(.*?)(\r?\n|$)`); - const match = data.match(regex); - - if (match && match[1].trim() !== '0') { - const value = Number(match[1].trim()); - if (isNaN(value)) return; - - switch (property) { - case 'BaseCX': - videoSettings.baseWidth = Number(match[1].trim()); - break; - case 'BaseCY': - videoSettings.baseHeight = Number(match[1].trim()); - break; - case 'OutputCX': - videoSettings.outputWidth = Number(match[1].trim()); - break; - case 'OutputCY': - videoSettings.outputHeight = Number(match[1].trim()); - break; - default: - break; - } - } - }); - } catch (e: unknown) { - console.warn('Error reading basic.ini', e); - } - - // if everything fails, return the default video settings - return videoSettings; - } - - protected onCreated(): void { - // fetch horizontal video settings (also is the legacy settings) - const horizontalSettings = this.fetchLegacySettings(); - - // migrate horizontal settings to realm - this.db.write(() => { - this.horizontal.video.deepPatch(horizontalSettings); - }); - - // migrate vertical video settings - const verticalSettings = { - ...horizontalSettings, - baseWidth: 720, - baseHeight: 1280, - outputWidth: 720, - outputHeight: 1280, - }; - - // migrate vertical settings to realm - this.db.write(() => { - this.vertical.video.deepPatch(verticalSettings); - }); - - // load persisted horizontal settings from service - const data = localStorage.getItem('PersistentStatefulService-DualOutputService'); - - if (data) { - const parsed = JSON.parse(data); - - // update active display settings - if (parsed.videoSettings?.activeDisplays) { - this.db.write(() => { - this.horizontal.isActive = parsed.videoSettings.activeDisplays?.horizontal ?? true; - this.vertical.isActive = parsed.videoSettings.activeDisplays.vertical; - }); - } - } - } - - /** - * The below provides a default base resolution - * @remark replaces the legacy base resolution in the video service - */ - get baseResolution() { - return this.baseResolutions.horizontal; - } - - /** - * The below provides a default base width - * @remark replaces the legacy base width in the video service - */ - get baseWidth() { - return this.baseResolutions.horizontal.baseWidth; - } - - /** - * The below provides a default base width - * @remark replaces the legacy base width in the video service - */ - get baseHeight() { - return this.baseResolutions.horizontal.baseHeight; - } - - /** - * Get base resolutions for the displays - * @remark Default values exist to prevent undefined errors on app startup - */ - get baseResolutions() { - // to prevent any possible undefined errors on load in the event that the root node - // attempts to load before the first video context has finished establishing - // the below are fallback dimensions - if (!this.horizontal || !this.vertical) { - console.error('Error loading video settings state. Default base resolution used.'); - return { - horizontal: { - baseWidth: 1920, - baseHeight: 1080, - }, - vertical: { - baseWidth: 720, - baseHeight: 1080, - }, - }; - } - - return { - horizontal: { - baseWidth: this.horizontal.videoInfo.baseWidth, - baseHeight: this.horizontal.videoInfo.baseHeight, - }, - vertical: { - baseWidth: this.vertical.videoInfo.baseWidth, - baseHeight: this.vertical.videoInfo.baseHeight, - }, - }; - } - get videoInfo(): Dictionary { - return { - horizontal: this.horizontal.videoInfo, - vertical: this.vertical.videoInfo, - }; - } - - /** - * Get base resolutions for the displays - * @remark Default values exist to prevent undefined errors on app startup - */ - get outputResolutions() { - // to prevent any possible undefined errors on load in the event that the root node - // attempts to load before the first video context has finished establishing - // the below are fallback dimensions - if (!this.horizontal || !this.vertical) { - console.error('Error loading video settings state. Default base resolution used.'); - return { - horizontal: { - outputWidth: 1920, - outputHeight: 1080, - }, - vertical: { - outputWidth: 720, - outputHeight: 1080, - }, - }; - } - - return { - horizontal: { - outputWidth: this.horizontal.videoInfo.outputWidth, - outputHeight: this.horizontal.videoInfo.outputHeight, - }, - vertical: { - outputWidth: this.vertical.videoInfo.outputWidth, - outputHeight: this.vertical.videoInfo.outputHeight, - }, - }; - } - - get values(): Dictionary { - return { - horizontal: this.horizontal.values, - vertical: this.vertical.values, - }; - } -} - -VideoSettingsState.register({ persist: true }); +import { TDisplayType, VideoSettingsService } from './settings-v2'; // TODO: There are no typings for nwr let nwr: any; @@ -832,29 +302,18 @@ export class Display { } } @InitAfter('UserService') +@InitAfter('VideoSettingsService') export class VideoService extends Service { @Inject() settingsService: SettingsService; @Inject() scenesService: ScenesService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() dualOutputService: DualOutputService; @Inject() sourcesService: SourcesService; - state = VideoSettingsState.inject(); - init() { this.settingsService.loadSettingsIntoStore(); - - this.establishVideoContext(); - - if (this.state.vertical?.isActive && !this.contexts.vertical) { - this.establishVideoContext('vertical'); - } } - contexts = { - horizontal: null as obs.IVideo, - vertical: null as obs.IVideo, - }; - getScreenRectangle(display: TDisplayType = 'horizontal') { return new ScalableRectangle({ x: 0, @@ -864,70 +323,19 @@ export class VideoService extends Service { }); } - get values(): Dictionary { - return this.state.values; - } - - get videoInfo(): Dictionary { - return this.state.videoInfo; - } - - /** - * The below conditionals are to prevent undefined errors on app startup - */ get baseResolutions() { - // to prevent any possible undefined errors on load in the event that the root node - // attempts to load before the first video context has finished establishing - // the below are fallback dimensions - if (!this.state) { - console.error('Error loading video settings state. Default base resolution used.'); - return { - horizontal: { - baseWidth: 1920, - baseHeight: 1080, - }, - vertical: { - baseWidth: 720, - baseHeight: 1080, - }, - }; - } + const baseResolutions = this.videoSettingsService.baseResolutions; - return this.state.baseResolutions; - } - - get baseResolution() { - return this.baseResolutions.horizontal; - } - - /** - * The below provides a default base width - * @remark replaces the legacy base width in the video service - */ - get baseWidth() { - return this.baseResolutions.horizontal.baseWidth; - } - - /** - * The below provides a default base width - * @remark replaces the legacy base width in the video service - */ - get baseHeight() { - return this.baseResolutions.horizontal.baseHeight; - } - - get outputResolutions() { - return this.state.outputResolutions; - } - - validateVideoContext(display: TDisplayType = 'vertical', condition: boolean = false) { - if (!this.contexts[display] && !condition) { - this.establishVideoContext(display); - } - } - - getContext(display?: TDisplayType) { - return this.contexts[display] as obs.IVideo; + return { + horizontal: { + baseWidth: baseResolutions.horizontal.baseWidth, + baseHeight: baseResolutions.horizontal.baseHeight, + }, + vertical: { + baseWidth: baseResolutions.vertical.baseWidth, + baseHeight: baseResolutions.vertical.baseHeight, + }, + }; } setBaseResolution(resolutions: { @@ -943,456 +351,14 @@ export class VideoService extends Service { // if the context has not been established when the migration for the root node has run, // there will be no base resolution data in the node so access it directly from the service if that is the case const baseWidth = - resolutions?.horizontal.baseWidth ?? this.baseResolutions.horizontal.baseWidth; + resolutions?.horizontal.baseWidth ?? + this.videoSettingsService.baseResolutions.horizontal.baseWidth; const baseHeight = - resolutions?.horizontal.baseHeight ?? this.baseResolutions.horizontal.baseHeight; + resolutions?.horizontal.baseHeight ?? + this.videoSettingsService.baseResolutions.horizontal.baseHeight; this.settingsService.setSettingValue('Video', 'Base', `${baseWidth}x${baseHeight}`); } - /** - * Format video settings for the video settings form - * - * @param display - Optional, the display for the settings - * @returns Settings formatted for the video settings form - */ - formatVideoSettings(display: TDisplayType = 'horizontal', typeStrings?: boolean) { - // use vertical display setting as a failsafe to prevent null errors - const settings = this.contexts[display].video ?? this.state[display].video; - - const scaleType = typeStrings - ? scaleTypeNames[settings?.scaleType as obs.EScaleType] - : settings?.scaleType; - const fpsType = typeStrings - ? fpsTypeNames[settings?.fpsType as obs.EFPSType] - : settings?.fpsType; - - return { - baseRes: `${settings?.baseWidth}x${settings?.baseHeight}`, - outputRes: `${settings?.outputWidth}x${settings?.outputHeight}`, - scaleType, - fpsType, - fpsCom: `${settings?.fpsNum}-${settings?.fpsDen}`, - fpsNum: settings?.fpsNum, - fpsDen: settings?.fpsDen, - fpsInt: settings?.fpsNum, - }; - } - - /** - * Format video settings for the video settings form - * - * @param display - Optional, the display for the settings - * @returns Settings formatted for the video settings form - */ - formatVideoDiagValues(display: TDisplayType = 'horizontal') { - const settings = this.videoInfo[display]; - - const scaleType = scaleTypeNames[settings?.scaleType as TScaleTypeNames]; - const fpsType = fpsTypeNames[settings?.fpsType as TFpsTypeNames]; - - return { - baseRes: `${settings?.baseWidth}x${settings?.baseHeight}`, - outputRes: `${settings?.outputWidth}x${settings?.outputHeight}`, - scaleType, - fpsType, - fpsCom: `${settings?.fpsNum}-${settings?.fpsDen}`, - fpsNum: settings?.fpsNum, - fpsDen: settings?.fpsDen, - fpsInt: settings?.fpsNum, - }; - } - - /** - * Establish the obs video context - * - * @remarks - * Many startup errors in other services will result from a context not being established before - * the service initiates. - * - * @param display - Optional, the context's display name - */ - establishVideoContext(display: TDisplayType = 'horizontal') { - if (this.contexts[display]) return; - this.contexts[display] = obs.VideoFactory.create(); - - this.contexts[display].video = this.videoInfo[display]; - this.contexts[display].legacySettings = this.videoInfo[display]; - - // this is necessary to guarantee that the default video context is using the horizontal video settings - obs.Video.video = this.videoInfo.horizontal; - obs.Video.legacySettings = this.videoInfo.horizontal; - - return !!this.contexts[display]; - } - - @debounce(200) - updateObsSettings(display: TDisplayType) { - if (!this.contexts[display]) return; - - this.contexts[display].video = this.videoInfo[display]; - this.contexts[display].legacySettings = this.videoInfo[display]; - } - - /** - * Migrate optimized settings to vertical context - */ - migrateAutoConfigSettings() { - // load optimized settings onto horizontal context - // const settings = - // this.contexts.horizontal?.legacySettings ?? - // this.contexts.horizontal?.video ?? - // this.state.horizontal.video; - // const updatedSettings = { - // ...settings, - // baseWidth: this.state.vertical.video.baseWidth, - // baseHeight: this.state.vertical.video.baseHeight, - // outputWidth: this.state.vertical.video.outputWidth, - // outputHeight: this.state.vertical.video.outputHeight, - // }; - // // this.updateVideoSettings(updatedSettings, 'vertical'); - // if (this.contexts?.vertical) { - // // update the Video settings property to the horizontal context dimensions - // const base = `${settings.baseWidth}x${settings.baseHeight}`; - // const output = `${settings.outputWidth}x${settings.outputHeight}`; - // } - } - - /** - * Confirm video setting dimensions in settings - * @remarks Primarily used with the optimizer to ensure the horizontal context dimensions - * are the dimensions in the settings - */ - // confirmVideoSettingDimensions() { - // const [baseWidth, baseHeight] = this.settingsService.views.values.Video.Base.split('x'); - // const [outputWidth, outputHeight] = this.settingsService.views.values.Video.Output.split('x'); - - // if ( - // Number(baseWidth) !== this.state.horizontal.video.baseWidth || - // Number(baseHeight) !== this.state.horizontal.video.baseHeight - // ) { - // const base = `${this.state.horizontal.video.baseWidth}x${this.state.horizontal.video.baseHeight}`; - // this.settingsService.setSettingValue('Video', 'Base', base); - // } - - // if ( - // Number(outputWidth) !== this.state.horizontal.video.outputWidth || - // Number(outputHeight) !== this.state.horizontal.video.outputHeight - // ) { - // const output = `${this.state.horizontal.video.outputWidth}x${this.state.horizontal.video.outputHeight}`; - // this.settingsService.setSettingValue('Video', 'Output', output); - // } - // } - - get settingsFormData() { - return [ - this.formatSettingsForInputField('baseRes', 'horizontal', [ - { label: '1920x1080', value: '1920x1080' }, - { label: '1280x720', value: '1280x720' }, - ]), - ]; - } - - formatSettingsForInputField( - category: keyof typeof ESettingsVideoProperties, - display: TDisplayType, - options?: { label: string; value: unknown }[], - ): ISettingsSubCategory { - const formData = {} as any; - - formData.name = ESettingsVideoProperties[category]; - - if (formData.name === ESettingsVideoProperties.baseRes) { - formData.type = 'OBS_INPUT_RESOLUTION_LIST'; - formData.description = 'Base (Canvas) Resolution'; - formData.subType = 'OBS_COMBO_FORMAT_STRING'; - formData.currentValue = this.state.values[display].baseRes; - formData.values = options; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].baseRes; - formData.options = options; - } - - if (formData.name === ESettingsVideoProperties.outputRes) { - formData.type = 'OBS_INPUT_RESOLUTION_LIST'; - formData.description = 'Output (Scaled) Resolution'; - formData.subType = 'OBS_COMBO_FORMAT_STRING'; - formData.currentValue = this.state.values[display].outputRes; - formData.values = [ - { - '1920x1080': '1920x1080', - }, - { - '1536x864': '1536x864', - }, - { - '1440x810': '1440x810', - }, - { - '1280x720': '1280x720', - }, - { - '1152x648': '1152x648', - }, - { - '1096x616': '1096x616', - }, - { - '960x540': '960x540', - }, - { - '852x480': '852x480', - }, - { - '768x432': '768x432', - }, - { - '698x392': '698x392', - }, - { - '640x360': '640x360', - }, - ]; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].outputRes; - formData.options = [ - { - value: '1920x1080', - description: '1920x1080', - }, - { - value: '1536x864', - description: '1536x864', - }, - { - value: '1440x810', - description: '1440x810', - }, - { - value: '1280x720', - description: '1280x720', - }, - { - value: '1152x648', - description: '1152x648', - }, - { - value: '1096x616', - description: '1096x616', - }, - { - value: '960x540', - description: '960x540', - }, - { - value: '852x480', - description: '852x480', - }, - { - value: '768x432', - description: '768x432', - }, - { - value: '698x392', - description: '698x392', - }, - { - value: '640x360', - description: '640x360', - }, - ]; - } - - if (formData.name === ESettingsVideoProperties.scaleType) { - formData.type = 'OBS_PROPERTY_LIST'; - formData.description = 'Downscale Filter'; - formData.subType = 'OBS_COMBO_FORMAT_STRING'; - formData.currentValue = this.state.values[display].scaleType; - formData.values = [ - { - 'Bilinear (Fastest, but blurry if scaling)': 'bilinear', - }, - { - 'Bicubic (Sharpened scaling, 16 samples)': 'bicubic', - }, - { - 'Lanczos (Sharpened scaling, 32 samples)': 'lanczos', - }, - ]; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].scaleType; - formData.options = [ - { - value: 'bilinear', - description: 'Bilinear (Fastest, but blurry if scaling)', - }, - { - value: 'bicubic', - description: 'Bicubic (Sharpened scaling, 16 samples)', - }, - { - value: 'lanczos', - description: 'Lanczos (Sharpened scaling, 32 samples)', - }, - ]; - } - - if (formData.name === ESettingsVideoProperties.fpsType) { - formData.type = 'OBS_PROPERTY_LIST'; - formData.description = 'FPS Type'; - formData.subType = 'OBS_COMBO_FORMAT_STRING'; - formData.currentValue = this.state.values[display].fpsType; - formData.values = [ - { - 'Common FPS Values': 'Common FPS Values', - }, - { - 'Integer FPS Value': 'Integer FPS Value', - }, - { - 'Fractional FPS Value': 'Fractional FPS Value', - }, - ]; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].fpsType; - formData.options = [ - { - value: 'Common FPS Values', - description: 'Common FPS Values', - }, - { - value: 'Integer FPS Value', - description: 'Integer FPS Value', - }, - { - value: 'Fractional FPS Value', - description: 'Fractional FPS Value', - }, - ]; - } - - if (formData.name === ESettingsVideoProperties.fpsCom) { - formData.type = 'OBS_PROPERTY_STRING'; - formData.description = 'FPS Common'; - formData.subType = ''; - formData.currentValue = this.state.values[display].fpsCom; - formData.values = []; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].fpsCom; - } - - if (formData.name === ESettingsVideoProperties.fpsNum) { - formData.type = 'OBS_PROPERTY_INT'; - formData.description = 'FPS Numerator'; - formData.subType = ''; - formData.currentValue = this.state.values[display].fpsNum; - formData.values = []; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].fpsNum; - } - - if (formData.name === ESettingsVideoProperties.fpsDen) { - formData.type = 'OBS_PROPERTY_INT'; - formData.description = 'FPS Denominator'; - formData.subType = ''; - formData.currentValue = this.state.values[display].fpsDen; - formData.values = []; - formData.visible = true; - formData.enabled = true; - formData.masked = false; - formData.value = this.state.values[display].fpsDen; - } - - return formData; - } - - /** - * Update a multiple video settings - * @remark Use to reduce calls to obs, which contributes to app bloat - * @param patch - key/values for video info - * @param display - context to apply setting - */ - updateVideoSettings(patch: Partial, display: TDisplayType = 'horizontal') { - this.setSettings({ video: { ...patch } }, display); - } - - /** - * Update a single video setting - * @remark Primarily used for the video settings form - * @param key - property name of setting - * @param value - new value for setting - * @param display - context to apply setting - */ - setVideoSetting( - key: keyof obs.IVideoInfo, - value: IVideoInfoValue, - display: TDisplayType = 'horizontal', - ) { - const setting = { [key]: value }; - this.setSettings({ video: { ...setting } }, display); - } - - /** - * Set if the context is active - * @remark Primarily used to - * - show and hide the displays in the studio editor - * - dictate which displays are streamed, recorded, or have replay buffered. - * @param isActive - boolean for if the context should be available for the user - * @param display - display to update - */ - setIsActive(isActive: boolean, display: TDisplayType) { - this.setSettings({ isActive }, display); - } - - setSettings(settingsPatch: DeepPartial, display: TDisplayType) { - this.state.db.write(() => { - this.state.deepPatch({ [display]: settingsPatch }); - }); - - // update video contexts - this.updateObsSettings(display); - } - - toggleActive(status: boolean, display: TDisplayType) { - this.state.db.write(() => { - this.state[display].isActive = status; - }); - } - - /** - * Shut down the video settings service - * - * @remarks - * Each context must be destroyed when shutting down the app to prevent errors - */ - shutdown() { - Object.keys(this.contexts).forEach((display: TDisplayType) => { - if (this.contexts[display]) { - // save settings as legacy settings - const videoInfo = this.videoInfo[display]; - - Object.keys(videoInfo).forEach((key: keyof TIVideoInfo) => { - this.contexts[display].video[key] = videoInfo[key]; - this.contexts[display].legacySettings[key] = videoInfo[key]; - }); - - // destroy context - this.contexts[display].destroy(); - } - }); - } - /** * @warning DO NOT USE THIS METHOD. Use the Display class instead */ @@ -1407,7 +373,8 @@ export class VideoService extends Service { // the display must have a context, otherwise the sources will not identify // which display they belong to - const context = this.contexts[type] ?? this.contexts.horizontal; + const context = + this.videoSettingsService.contexts[type] ?? this.videoSettingsService.contexts.horizontal; if (sourceId) { obs.NodeObs.OBS_content_createSourcePreviewDisplay( diff --git a/app/services/widgets/widgets.ts b/app/services/widgets/widgets.ts index d171c74dbf8a..9d515df09ea5 100644 --- a/app/services/widgets/widgets.ts +++ b/app/services/widgets/widgets.ts @@ -25,7 +25,7 @@ import { getAlertsConfig, TAlertType } from './alerts-config'; import { getWidgetsConfig } from './widgets-config'; import { WidgetDisplayData } from '.'; import { DualOutputService } from 'services/dual-output'; -import { TDisplayType, VideoService } from 'services/video'; +import { TDisplayType, VideoSettingsService } from 'services/settings-v2'; import { IncrementalRolloutService } from 'app-services'; import { EAvailableFeatures } from 'services/incremental-rollout'; @@ -84,7 +84,7 @@ export class WidgetsService @Inject() hostsService: HostsService; @Inject() editorCommandsService: EditorCommandsService; @Inject() dualOutputService: DualOutputService; - @Inject() videoService: VideoService; + @Inject() videoSettingsService: VideoSettingsService; @Inject() incrementalRolloutService: IncrementalRolloutService; widgetDisplayData = WidgetDisplayData(); // cache widget display data @@ -151,8 +151,8 @@ export class WidgetsService }); rect.withAnchor(widgetTransform.anchor, () => { - rect.x = widgetTransform.x * this.videoService.baseResolutions.horizontal.baseWidth; - rect.y = widgetTransform.y * this.videoService.baseResolutions.horizontal.baseHeight; + rect.x = widgetTransform.x * this.videoSettingsService.baseResolutions.horizontal.baseWidth; + rect.y = widgetTransform.y * this.videoSettingsService.baseResolutions.horizontal.baseHeight; }); const item = this.editorCommandsService.executeCommand( @@ -345,11 +345,18 @@ export class WidgetsService settings, name: source.name, type: source.getPropertiesManagerSettings().widgetType, - x: widgetItem.transform.position.x / this.videoService.baseResolutions.horizontal.baseWidth, - y: widgetItem.transform.position.y / this.videoService.baseResolutions.horizontal.baseHeight, - scaleX: widgetItem.transform.scale.x / this.videoService.baseResolutions.horizontal.baseWidth, + x: + widgetItem.transform.position.x / + this.videoSettingsService.baseResolutions.horizontal.baseWidth, + y: + widgetItem.transform.position.y / + this.videoSettingsService.baseResolutions.horizontal.baseHeight, + scaleX: + widgetItem.transform.scale.x / + this.videoSettingsService.baseResolutions.horizontal.baseWidth, scaleY: - widgetItem.transform.scale.y / this.videoService.baseResolutions.horizontal.baseHeight, + widgetItem.transform.scale.y / + this.videoSettingsService.baseResolutions.horizontal.baseHeight, }; } @@ -400,8 +407,8 @@ export class WidgetsService this.createWidgetFromJSON( widget, widgetItem, - this.videoService.baseResolutions.horizontal.baseWidth, - this.videoService.baseResolutions.horizontal.baseHeight, + this.videoSettingsService.baseResolutions.horizontal.baseWidth, + this.videoSettingsService.baseResolutions.horizontal.baseHeight, 'horizontal', ); @@ -418,8 +425,8 @@ export class WidgetsService this.createWidgetFromJSON( widget, verticalSceneItem, - this.videoService.baseResolutions.horizontal.baseWidth, - this.videoService.baseResolutions.horizontal.baseHeight, + this.videoSettingsService.baseResolutions.horizontal.baseWidth, + this.videoSettingsService.baseResolutions.horizontal.baseHeight, 'vertical', ); }); diff --git a/app/util/DragHandler.ts b/app/util/DragHandler.ts index 9a4b498ba000..0dd087521219 100644 --- a/app/util/DragHandler.ts +++ b/app/util/DragHandler.ts @@ -1,7 +1,7 @@ import { SettingsService } from 'services/settings'; import { Inject } from 'services/core/injector'; import { SceneItem } from 'services/scenes'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2'; import { WindowsService } from 'services/windows'; import { DualOutputService } from 'services/dual-output'; import { ScalableRectangle } from 'util/ScalableRectangle'; @@ -58,7 +58,7 @@ interface IDragHandlerOptions { // Encapsulates logic for dragging sources in the overlay editor export class DragHandler { @Inject() private settingsService: SettingsService; - @Inject() private videoService: VideoService; + @Inject() private videoSettingsService: VideoSettingsService; @Inject() private windowsService: WindowsService; @Inject() private selectionService: SelectionService; @Inject() private editorCommandsService: EditorCommandsService; @@ -102,7 +102,7 @@ export class DragHandler { this.centerSnapping = this.settingsService.views.values.General.CenterSnapping; // Load some attributes about the video canvas - const baseRes = this.videoService.baseResolutions[startEvent.display]; + const baseRes = this.videoSettingsService.baseResolutions[startEvent.display]; this.baseWidth = baseRes.baseWidth; this.baseHeight = baseRes.baseHeight; @@ -281,8 +281,8 @@ export class DragHandler { } private pageSpaceToCanvasSpace(vec: IVec2, display = 'horizontal') { - const baseWidth = this.videoService.baseResolutions[display].baseWidth; - const baseHeight = this.videoService.baseResolutions[display].baseHeight; + const baseWidth = this.videoSettingsService.baseResolutions[display].baseWidth; + const baseHeight = this.videoSettingsService.baseResolutions[display].baseHeight; return { x: (vec.x * this.scaleFactor * baseWidth) / this.displaySize.x, y: (vec.y * this.scaleFactor * baseHeight) / this.displaySize.y, diff --git a/app/util/menus/EditMenu.ts b/app/util/menus/EditMenu.ts index 354f4868a10a..40d8135485f2 100644 --- a/app/util/menus/EditMenu.ts +++ b/app/util/menus/EditMenu.ts @@ -13,7 +13,7 @@ import { ProjectorService } from 'services/projector'; import { $t } from 'services/i18n'; import { EditorCommandsService } from 'services/editor-commands'; import { StreamingService } from 'services/streaming'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; import * as remote from '@electron/remote'; import { ProjectorMenu } from './ProjectorMenu'; import { FiltersMenu } from './FiltersMenu'; diff --git a/app/util/menus/SourceTransformMenu.ts b/app/util/menus/SourceTransformMenu.ts index d5e046a67260..f3dcbf6f8450 100644 --- a/app/util/menus/SourceTransformMenu.ts +++ b/app/util/menus/SourceTransformMenu.ts @@ -5,7 +5,7 @@ import { $t } from 'services/i18n'; import { EditorCommandsService } from 'services/editor-commands'; import { ECenteringType } from 'services/editor-commands/commands/center-items'; import { EFlipAxis } from 'services/editor-commands/commands/flip-items'; -import { TDisplayType } from 'services/video'; +import { TDisplayType } from 'services/settings-v2'; export class SourceTransformMenu extends Menu { @Inject() private selectionService: SelectionService; diff --git a/test/regular/api/dual-output.ts b/test/regular/api/dual-output.ts index 936b405cb7f0..7b439778f8df 100644 --- a/test/regular/api/dual-output.ts +++ b/test/regular/api/dual-output.ts @@ -2,7 +2,7 @@ import { DualOutputService } from 'services/dual-output'; import { getApiClient } from '../../helpers/api-client'; import { test, useWebdriver, TExecutionContext } from '../../helpers/webdriver'; import { ScenesService, Scene, SceneItem } from 'services/scenes'; -import { VideoService } from 'services/video'; +import { VideoSettingsService } from 'services/settings-v2/video'; // not a react hook // eslint-disable-next-line react-hooks/rules-of-hooks @@ -53,7 +53,7 @@ function confirmVerticalSceneItem( test('Convert single output collection to dual output', async (t: TExecutionContext) => { const client = await getApiClient(); const scenesService = client.getResource('ScenesService'); - const videoService = client.getResource('VideoService'); + const videoSettingsService = client.getResource('VideoSettingsService'); const dualOutputService = client.getResource('DualOutputService'); const scene = scenesService.createScene('Scene1'); scene.createAndAddSource('Item1', 'color_source'); @@ -61,7 +61,7 @@ test('Convert single output collection to dual output', async (t: TExecutionCont scene.createAndAddSource('Item3', 'color_source'); // single output - const horizontalContext = videoService.contexts.horizontal; + const horizontalContext = videoSettingsService.contexts.horizontal; scene.getItems().forEach(sceneItem => { t.is(sceneItem?.display, 'horizontal'); t.deepEqual(sceneItem?.output, horizontalContext); @@ -77,7 +77,7 @@ test('Convert single output collection to dual output', async (t: TExecutionCont t.not(sceneNodeMaps, null, 'Dual output scene collection has node maps.'); const nodeMap = sceneNodeMaps[scene.id]; - const verticalContext = videoService.contexts.vertical; + const verticalContext = videoSettingsService.contexts.vertical; const sceneItems = scene.getItems(); // confirm dual output collection length is double the single output collection length diff --git a/test/regular/api/scenes.ts b/test/regular/api/scenes.ts index 9cdd3a54ebcc..67c7f309859b 100644 --- a/test/regular/api/scenes.ts +++ b/test/regular/api/scenes.ts @@ -2,7 +2,7 @@ import { useWebdriver, test } from '../../helpers/webdriver'; import { getApiClient } from '../../helpers/api-client'; import { SceneBuilder } from '../../helpers/scene-builder'; import { ScenesService } from '../../../app/services/api/external-api/scenes'; -import { VideoService, DualOutputService } from 'app-services'; +import { VideoSettingsService, DualOutputService } from 'app-services'; const path = require('path');