diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbd2290..bc3b4b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Default names to types - Functionality for making windows stay on top - Support for Caspar CGs HTML producer +- Support for SCALE_MODE for Caspar media for Caspar 2.5.x +- Support for Caspar CGs image scroller ### Changed - Some features have moved to the footer of the app window - Context menus now follow the color theme diff --git a/media/screenshot.png b/media/screenshot.png index bfb24e6a..74e1c9a3 100644 Binary files a/media/screenshot.png and b/media/screenshot.png differ diff --git a/plugins/caspar/lib/AMCP.js b/plugins/caspar/lib/AMCP.js index d404a749..267ad260 100644 --- a/plugins/caspar/lib/AMCP.js +++ b/plugins/caspar/lib/AMCP.js @@ -16,7 +16,7 @@ const types = require('./types') * Construct a channel-layer string * for use in commands * @param { AMCPOptions | undefined } opts - * @returns { String } + * @returns { string } */ function layerString (opts = {}) { if (opts.channel == null) { @@ -32,12 +32,22 @@ function layerString (opts = {}) { * Construct a transition-string * for use in commands * @param { AMCPOptions | undefined } opts - * @returns { String } + * @returns { string } */ function transitionString (opts = {}) { return ` ${types.TRANSITION_NAME_ENUM[opts.transitionName] || ''} ${opts.transitionDuration || '0'} ${(opts.transitionEasing || 'LINEAR')} ${(types.TRANSITION_DIRECTION_ENUM[opts.transitionDirection] || 'LEFT')}`.toUpperCase() } +/** + * Construct a scale mode string + * for use in commands + * @param { AMCPOptions | undefined } opts + * @returns { string } + */ +function scaleModeString (opts = {}) { + return ` SCALE_MODE ${(types.SCALE_MODE_ENUM[opts.scaleMode] || 'STRETCH').toUpperCase()}` +} + /** * List media files in the media directory * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#cls @@ -86,56 +96,64 @@ exports.clear = opts => `CLEAR ${layerString(opts)}` /** * Load a media item in the background * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#loadbg - * @param { String } file The file to play - * @param { Boolean } loop - * @param { Number } seek - * @param { Number } length - * @param { String } filter - * @param { Boolean } auto + * @param { string } file The file to play + * @param { boolean } loop + * @param { number } seek + * @param { number } length + * @param { string } filter + * @param { boolean } auto * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ -exports.loadbg = (file, loop, seek, length, filter, auto, opts) => `LOADBG ${layerString(opts)}${file ? ` "${file}"` : ''}${loop ? ' LOOP' : ''}${seek ? ` SEEK ${seek}` : ''}${length ? ` LENGTH ${length}` : ''}${filter ? ` FILTER ${filter}` : ''}${transitionString(opts)} ${auto ? 'AUTO' : ''}` +exports.loadbg = (file, loop, seek, length, filter, auto, opts) => `LOADBG ${layerString(opts)}${file ? ` "${file}"` : ''}${loop ? ' LOOP' : ''}${seek ? ` SEEK ${seek}` : ''}${length ? ` LENGTH ${length}` : ''}${filter ? ` FILTER ${filter}` : ''}${transitionString(opts)}${scaleModeString(opts)} ${auto ? 'AUTO' : ''}` /** * Play a media item in the foreground * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#play - * @param { String } file The file to play - * @param { Boolean } loop - * @param { Number } seek - * @param { Number } length - * @param { String } filter - * @param { Boolean } auto + * @param { string } file The file to play + * @param { boolean } loop + * @param { number } seek + * @param { number } length + * @param { string } filter + * @param { boolean } auto * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ -exports.play = (file, loop, seek, length, filter, auto, opts) => `PLAY ${layerString(opts)}${file ? ` "${file}"` : ''}${loop ? ' LOOP' : ''}${seek ? ` SEEK ${seek}` : ''}${length ? ` LENGTH ${length}` : ''}${filter ? ` FILTER ${filter}` : ''}${transitionString(opts)} ${auto ? ' AUTO' : ''}` +exports.play = (file, loop, seek, length, filter, auto, opts) => `PLAY ${layerString(opts)}${file ? ` "${file}"` : ''}${loop ? ' LOOP' : ''}${seek ? ` SEEK ${seek}` : ''}${length ? ` LENGTH ${length}` : ''}${filter ? ` FILTER ${filter}` : ''}${transitionString(opts)}${scaleModeString(opts)} ${auto ? ' AUTO' : ''}` /** * Play a media item in the foreground that * has already been loaded in the background * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#play * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.playLoaded = opts => `PLAY ${layerString(opts)}` +/** + * Play an image scroller + * @param { string } file The file to play + * @param { AMCPOptions } opts + * @returns { string } + */ +exports.playImageScroller = (file, opts) => `PLAY ${layerString(opts)}${file ? ` "${file}"` : ''} BLUR ${opts?.blur || 0} SPEED ${opts?.speed || 7}${opts?.progressive ? ' PROGRESSIVE' : ''}` + /** * Stop an item running in the foreground * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#stop * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.stop = opts => `STOP ${layerString(opts)}` /** * Add a template * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#cg-add - * @param { String } template + * @param { string } template * @param { Any } data - * @param { Boolean } playOnLoad + * @param { boolean } playOnLoad * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.cgAdd = (template, data, playOnLoad = true, opts) => `CG ${layerString(opts)} ADD ${opts.cgLayer ?? 1} "${template}" ${playOnLoad ? 1 : 0} ${JSON.stringify(data || '')}` @@ -143,49 +161,49 @@ exports.cgAdd = (template, data, playOnLoad = true, opts) => `CG ${layerString(o * Stop a template * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#cg-stop * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.cgStop = opts => `CG ${layerString(opts)} STOP ${opts.cgLayer ?? 1}` /** * Update a template * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#cg-update - * @param { String } data + * @param { string } data * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.cgUpdate = (data, opts) => `CG ${layerString(opts)} UPDATE ${opts.cgLayer ?? 1} ${JSON.stringify(data || '')}` /** * Change the opacity of a layer * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#mixer-opacity - * @param { String } opacity + * @param { string } opacity * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.mixerOpacity = (opacity, opts) => `MIXER ${layerString(opts)} OPACITY ${opacity}${transitionString(opts)}` /** * Change the volume of a layer * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#mixer-volume - * @param { String } volume + * @param { string } volume * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.mixerVolume = (volume, opts) => `MIXER ${layerString(opts)} VOLUME ${volume}${transitionString(opts)}` /** * Get the thumbnail for a file * @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#thumbnail-retrieve - * @param { String } fileName - * @returns { String } + * @param { string } fileName + * @returns { string } */ exports.thumbnailRetrieve = fileName => `THUMBNAIL RETRIEVE "${fileName}"` /** * Start the HTML producer - * @param { String } url + * @param { string } url * @param { AMCPOptions } opts - * @returns { String } + * @returns { string } */ exports.html = (url, opts) => `PLAY ${layerString(opts)} [HTML] "${url}"${transitionString(opts)}` diff --git a/plugins/caspar/lib/handlers.js b/plugins/caspar/lib/handlers.js index d946a697..afcd282f 100644 --- a/plugins/caspar/lib/handlers.js +++ b/plugins/caspar/lib/handlers.js @@ -24,6 +24,9 @@ const PLAY_HANDLERS = { 'bridge.caspar.media': async (serverId, item) => { return commands.sendCommand(serverId, 'play', item?.data?.caspar?.target, item?.data?.caspar?.loop, 0, undefined, undefined, undefined, item?.data?.caspar) }, + 'bridge.caspar.image-scroller': async (serverId, item) => { + return commands.sendCommand(serverId, 'playImageScroller', item?.data?.caspar?.target, item?.data?.caspar) + }, 'bridge.caspar.load': async (serverId, item) => { return commands.sendCommand(serverId, 'loadbg', item?.data?.caspar?.target, item?.data?.caspar?.loop, 0, undefined, undefined, item?.data?.caspar?.auto, item?.data?.caspar) }, @@ -48,6 +51,9 @@ const STOP_HANDLERS = { 'bridge.caspar.media': (serverId, item) => { return commands.sendCommand(serverId, 'stop', item?.data?.caspar) }, + 'bridge.caspar.image-scroller': (serverId, item) => { + return commands.sendCommand(serverId, 'stop', item?.data?.caspar) + }, 'bridge.caspar.load': async (serverId, item) => { }, diff --git a/plugins/caspar/lib/types.js b/plugins/caspar/lib/types.js index fc0c43b1..a6c5f2bb 100644 --- a/plugins/caspar/lib/types.js +++ b/plugins/caspar/lib/types.js @@ -10,6 +10,9 @@ exports.TRANSITION_NAME_ENUM = TRANSITION_NAME_ENUM const TRANSITION_DIRECTION_ENUM = ['Left', 'Right'] exports.TRANSITION_DIRECTION_ENUM = TRANSITION_DIRECTION_ENUM +const SCALE_MODE_ENUM = ['Stretch', 'Fit', 'Fill', 'Original', 'HFILL', 'VFILL'] +exports.SCALE_MODE_ENUM = SCALE_MODE_ENUM + const DEFAULT_SERVER_ID = 'group:0' function init (htmlPath) { @@ -147,6 +150,13 @@ function init (htmlPath) { allowsVariables: true, 'ui.group': 'Caspar' }, + 'caspar.scaleMode': { + name: 'Scale mode', + type: 'enum', + enum: SCALE_MODE_ENUM, + default: '0', + 'ui.group': 'Caspar' + }, 'caspar.loop': { name: 'Loop', type: 'boolean', @@ -201,6 +211,41 @@ function init (htmlPath) { } }) + bridge.types.registerType({ + id: 'bridge.caspar.image-scroller', + name: 'Image scroller', + category: 'Caspar', + inherits: 'bridge.caspar.playable', + properties: { + 'caspar.target': { + name: 'Target', + type: 'string', + allowsVariables: true, + 'ui.group': 'Caspar' + }, + 'caspar.speed': { + name: 'Speed', + type: 'string', + default: '7', + allowsVariables: true, + 'ui.group': 'Image scroller' + }, + 'caspar.blur': { + name: 'Blur', + type: 'string', + default: '0', + allowsVariables: true, + 'ui.group': 'Image scroller' + }, + 'caspar.progressive': { + name: 'Progressive', + type: 'boolean', + default: false, + 'ui.group': 'Image scroller' + } + } + }) + bridge.types.registerType({ id: 'bridge.caspar.template', name: 'Template',