diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..19e9a94f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "start", + "type": "npm", + "script": "start", + "problemMatcher": [ + "$tsc" + ], + "isBackground": true, + "promptOnClose": false + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index d7874e77..ff1569e9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "build": "yarn tsc && webpack --config webpack.build.config.js --mode production", "build-now": "webpack --config webpack.build.config.js --mode production", "package": "yarn run build", - "postpackage": "electron-packager ./ --out=./builds --overwrite", + "postpackage": "electron-packager --overwrite ./ --out=./builds --overwrite", "package-linux": "electron-packager . sisyfos-audio-controller --overwrite --asar=true --platform=linux --arch=x64 --prune=true --out=builds", "package-win": "electron-packager . sisyfos-audio-controller --overwrite --asar=true --platform=win32 --arch=x64 --prune=true --out=builds", "package-release": "yarn package && yarn package-win && yarn package-linux" diff --git a/src/assets/css/ChannelSettings.css b/src/assets/css/ChannelSettings.css index ac62b507..2a7be67e 100644 --- a/src/assets/css/ChannelSettings.css +++ b/src/assets/css/ChannelSettings.css @@ -59,4 +59,8 @@ width: 90%; font-size: 30px; line-height: 50px; +} + +.channel-settings-group > button.active { + border-color: rgb(17, 0, 255); } \ No newline at end of file diff --git a/src/components/ChannelSettings.tsx b/src/components/ChannelSettings.tsx index d269ff07..a5e83c48 100644 --- a/src/components/ChannelSettings.tsx +++ b/src/components/ChannelSettings.tsx @@ -5,10 +5,12 @@ import { MixerProtocolPresets } from '../constants/MixerProtocolPresets'; import { IMixerProtocolGeneric, ICasparCGMixerGeometry } from '../constants/MixerProtocolInterface'; import { Store } from 'redux'; import { connect } from 'react-redux'; +import { IStore } from '../reducers/indexReducer'; interface IChannelSettingsInjectProps { label: string, mixerProtocol: string, + sourceOption: string } interface IChannelProps { @@ -55,8 +57,14 @@ class ChannelSettings extends React.PureComponent {Object.getOwnPropertyNames(this.mixerProtocol!.sourceOptions!.options[prop]).map(option => { - console.log(prop, option) - return + return }) || null} ) @@ -67,10 +75,10 @@ class ChannelSettings extends React.PureComponent { - console.log(state.channels[0].channel, props.channelIndex); return { label: state.channels[0].channel[props.channelIndex].label, - mixerProtocol: state.settings[0].mixerProtocol + mixerProtocol: state.settings[0].mixerProtocol, + sourceOption: (state.channels[0].channel[props.channelIndex].private || {})['channel_layout'] } } diff --git a/src/components/Channels.tsx b/src/components/Channels.tsx index 3451521d..c7866886 100644 --- a/src/components/Channels.tsx +++ b/src/components/Channels.tsx @@ -13,7 +13,7 @@ class Channels extends React.Component { } public shouldComponentUpdate(nextProps: IAppProps) { - return false; + return this.props.store.settings[0].showOptions !== nextProps.store.settings[0].showOptions; } diff --git a/src/constants/MixerProtocolInterface.ts b/src/constants/MixerProtocolInterface.ts index 7c0a4b02..c19416df 100644 --- a/src/constants/MixerProtocolInterface.ts +++ b/src/constants/MixerProtocolInterface.ts @@ -82,10 +82,10 @@ export interface ICasparCGMixerGeometryFile { MONITOR_CHANNEL_FADER_LEVEL: Array } sourceOptions?: { - sources: (ICasparCGChannelLayerPair & { + sources: Array<(ICasparCGChannelLayerPair & { producer: string, file: string - }) + })> options: { [key: string]: { // producer property invocation [key: string]: string // label: property @@ -109,10 +109,10 @@ export interface ICasparCGMixerGeometry extends IMixerProtocolGeneric { } channelLabels?: string[], sourceOptions?: { - sources: (ICasparCGChannelLayerPair & { + sources: Array<(ICasparCGChannelLayerPair & { producer: string, file: string - }) + })> options: { [key: string]: { // producer property invocation [key: string]: string // label: property diff --git a/src/constants/MixerProtocolPresets.ts b/src/constants/MixerProtocolPresets.ts index e4d1fd1c..65423441 100644 --- a/src/constants/MixerProtocolPresets.ts +++ b/src/constants/MixerProtocolPresets.ts @@ -6,6 +6,12 @@ import { MidasMaster } from './mixerProtocols/midasMaster'; import { GenericMidi } from './mixerProtocols/genericMidi'; import { LawoClient } from './mixerProtocols/EmberLawo'; import { CasparCGMaster } from './mixerProtocols/casparCGMaster'; + +interface IMessageProtocol { + mixerMessage: string, + value: any, + type: string +} import { StuderVista1Master } from './mixerProtocols/StuderVista1Ember'; import { StuderVista5Master } from './mixerProtocols/StuderVista5Ember'; import { StuderVista9Master } from './mixerProtocols/StuderVista9Ember'; diff --git a/src/reducers/channelsReducer.ts b/src/reducers/channelsReducer.ts index 4d48e199..aefcd444 100644 --- a/src/reducers/channelsReducer.ts +++ b/src/reducers/channelsReducer.ts @@ -16,7 +16,10 @@ export interface IChannel { pstOn: boolean, pflOn: boolean, showChannel: boolean, - snapOn: Array + snapOn: Array, + private?: { + [key: string]: string + } } interface IVuMeters { @@ -134,6 +137,16 @@ export const channels = ((state = defaultChannelsReducerState([1]), action: any) nextState[0].channel[index].pstOn = !!state[0].channel[index].snapOn[action.snapIndex]; }); return nextState; + case 'SET_PRIVATE': + if (!nextState[0].channel[action.channel].private) { + nextState[0].channel[action.channel].private = {}; + } + nextState[0].channel[action.channel].private![action.tag] = action.value; + return nextState; + case 'SET_OPTION': + console.log(action); + window.mixerGenericConnection.updateChannelSettings(action.channel, action.prop, action.option); + return nextState; default: return nextState; } diff --git a/src/utils/CasparCGConnection.ts b/src/utils/CasparCGConnection.ts index 97db410d..e3b4c10e 100644 --- a/src/utils/CasparCGConnection.ts +++ b/src/utils/CasparCGConnection.ts @@ -13,6 +13,15 @@ interface CommandChannelMap { [key: string]: number } +interface ICasparCGChannel extends IChannel { + producer?: string + source?: string +} + +const OSC_PATH_PRODUCER = /\/channel\/(\d+)\/stage\/layer\/(\d+)\/producer\/type/ +const OSC_PATH_PRODUCER_FILE_NAME = /\/channel\/(\d+)\/stage\/layer\/(\d+)\/file\/path/ +const OSC_PATH_PRODUCER_CHANNEL_LAYOUT = /\/channel\/(\d+)\/stage\/layer\/(\d+)\/producer\/channel_layout/ + export class CasparCGConnection { store: IStore; mixerProtocol: ICasparCGMixerGeometry; @@ -73,6 +82,40 @@ export class CasparCGConnection { // We therefore want to premultiply this to show useful information about audio levels level: Math.min(1, message.args[0] * this.store.channels[0].channel[index].faderLevel) }); + } else if (this.mixerProtocol.sourceOptions) { + const m = message.address.split('/'); + + if (m[1] === 'channel' && m[6] === 'producer' && m[7] === 'type') { + const index = this.mixerProtocol.sourceOptions.sources.findIndex(i => i.channel === parseInt(m[2], 10) && i.layer === parseInt(m[5])) + if (index >= 0) { + window.storeRedux.dispatch({ + type: 'SET_PRIVATE', + channel: index, + tag: 'producer', + value: message.args[0] + }) + } + } else if (m[1] === 'channel' && m[6] === 'producer' && m[7] === 'channel_layout') { + const index = this.mixerProtocol.sourceOptions.sources.findIndex(i => i.channel === parseInt(m[2], 10) && i.layer === parseInt(m[5])) + if (index >= 0) { + window.storeRedux.dispatch({ + type: 'SET_PRIVATE', + channel: index, + tag: 'channel_layout', + value: message.args[0] + }) + } + } else if (m[1] === 'channel' && m[6] === 'file' && m[7] === 'path') { + const index = this.mixerProtocol.sourceOptions.sources.findIndex(i => i.channel === parseInt(m[2], 10) && i.layer === parseInt(m[5])) + if (index >= 0) { + window.storeRedux.dispatch({ + type: 'SET_PRIVATE', + channel: index, + tag: 'file_path', + value: message.args[0] + }) + } + } } }) .on('error', (error: any) => { @@ -110,6 +153,15 @@ export class CasparCGConnection { return undefined } + findChannelIndex(channel: number, layer: number, channelLayerPairs: Array, matchFirst?: boolean): number { + return channelLayerPairs.findIndex((i) => { + if (matchFirst) { + return (i[0].channel === channel && i[0].layer === layer) + } + return !!i.find(j => j.channel === channel && j.layer === layer) + }) + } + pingMixerCommand = () => { //Ping OSC mixer if mixerProtocol needs it. /* this.mixerProtocol.pingCommand.map((command) => { @@ -127,12 +179,64 @@ export class CasparCGConnection { }) } + controlChannelSetting = (channel: number, layer: number, producer: string, file: string, setting: string, value: string) => { + this.connection.stop(channel, layer) + .then(() => { + if (setting === 'CHANNEL_LAYOUT') { + switch (producer) { + case 'ffmpeg': + return this.connection.play( + channel, + layer, + file, + true, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + value); + case 'decklink': + return this.connection.playDecklink( + channel, + layer, + parseInt(file, 10), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + value); + } + } + return Promise.reject('Unknown operation'); + }).then(() => { + + }).catch((e) => { + console.error('Failed to change channel setting', e); + }) + } + setAllLayers = (pairs: ICasparCGChannelLayerPair[], value: number) => { pairs.forEach((i) => { this.controlVolume(i.channel, i.layer, value); }) } + updateChannelSetting(channelIndex: number, setting: string, value: string) { + if (this.mixerProtocol.sourceOptions && this.store.channels[0].channel[channelIndex].private) { + const pair = this.mixerProtocol.sourceOptions.sources[channelIndex]; + const producer = this.store.channels[0].channel[channelIndex].private!['producer']; + let filePath = this.store.channels[0].channel[channelIndex].private!['file_path']; + filePath = filePath.replace(/\.[\w\d]+$/, '') + this.controlChannelSetting(pair.channel, pair.layer, producer, filePath, setting, value); + } + } + updateOutLevel(channelIndex: number) { if (channelIndex > this.mixerProtocol.toMixer.PGM_CHANNEL_FADER_LEVEL.length - 1) { return diff --git a/src/utils/MixerConnection.ts b/src/utils/MixerConnection.ts index af86cd5e..7ca985b9 100644 --- a/src/utils/MixerConnection.ts +++ b/src/utils/MixerConnection.ts @@ -67,6 +67,12 @@ export class MixerGenericConnection { this.mixerConnection.updateChannelName(channelIndex); } + updateChannelSettings(channelIndex: number, setting: string, value: string) { + if (this.mixerProtocol.protocol === 'CasparCG') { + this.mixerConnection.updateChannelSetting(channelIndex, setting, value) + } + } + fadeInOut (channelIndex: number, fadeTime: number){ //Clear Old timer or set Fade to active: if (this.store.channels[0].channel[channelIndex].fadeActive) {