Skip to content

Commit

Permalink
Add thumbnail support to the caspar plugin
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Boberg <[email protected]>
  • Loading branch information
axelboberg committed Jan 14, 2024
1 parent dd07f2b commit 175b0aa
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 43 deletions.
10 changes: 7 additions & 3 deletions lib/template/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
"title": "Rundown",
"component": "bridge.internals.grid",
"layout": {
"timing": { "x": 0, "y": 0, "w": 6, "h": 2 },
"liveSwitch": { "x": 0, "y": 0, "w": 6, "h": 2 },
"library": { "x": 0, "y": 2, "w": 6, "h": 10 },
"rundown": { "x": 6, "y": 0, "w": 12, "h": 12 },
"inspector": { "x": 18, "y": 0, "w": 6, "h": 12 }
"thumbnail": { "x": 18, "y": 0, "w": 6, "h": 3 },
"inspector": { "x": 18, "y": 3, "w": 6, "h": 9 }
},
"children": {
"timing": {
"liveSwitch": {
"component": "bridge.plugins.caspar.liveSwitch"
},
"library": {
Expand All @@ -22,6 +23,9 @@
"rundown": {
"component": "bridge.plugins.rundown"
},
"thumbnail": {
"component": "bridge.plugins.caspar.thumbnail"
},
"inspector": {
"component": "bridge.plugins.inspector"
}
Expand Down
3 changes: 3 additions & 0 deletions plugins/caspar/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InspectorTemplate } from './views/InspectorTemplate'
import { InspectorTransition } from './views/InspectorTransition'

import { LiveSwitch } from './views/LiveSwitch'
import { Thumbnail } from './views/Thumbnail'
import { Library } from './views/Library'
import { Status } from './views/Status'

Expand All @@ -34,6 +35,8 @@ export default function App () {
return <Settings.Servers />
case 'liveSwitch':
return <LiveSwitch />
case 'thumbnail':
return <Thumbnail />
case 'status':
return <Status />
case 'library':
Expand Down
14 changes: 14 additions & 0 deletions plugins/caspar/app/components/ThumbnailImage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import './style.css'

export const ThumbnailImage = ({ src, alt = 'Thumbnail image' }) => {
return (
<div className='ThumbnailImage'>
{
src
? <img className='ThumbnailImage-img' alt={alt} src={src} />
: <div className='Thumbnail-text'>Thumbnail not available</div>
}
</div>
)
}
17 changes: 17 additions & 0 deletions plugins/caspar/app/components/ThumbnailImage/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.ThumbnailImage {
display: flex;
position: relative;

width: 100%;
height: 100%;

align-items: center;
justify-content: center;
}

.ThumbnailImage-img {
width: 100%;
height: 100%;

object-fit: contain;
}
55 changes: 55 additions & 0 deletions plugins/caspar/app/views/Thumbnail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import bridge from 'bridge'

import { SharedContext } from '../sharedContext'
import { ThumbnailImage } from '../components/ThumbnailImage'

export const Thumbnail = () => {
const [state] = React.useContext(SharedContext)
const [item, setItem] = React.useState({})
const [image, setImage] = React.useState()
const [selection, setSelection] = React.useState([])

React.useEffect(() => {
const selection = state?._connections?.[bridge.client.getIdentity()]?.selection
setSelection(selection)
}, [state])

React.useEffect(() => {
async function getItem () {
if (!selection || selection.length < 1) {
setImage(undefined)
setItem(undefined)
return
}
const item = await bridge.items.getItem(selection[0])

if (item?.type !== 'bridge.caspar.media') {
setImage(undefined)
setItem(undefined)
return
}

if (!item?.data?.caspar?.server || !item?.data?.caspar?.target) {
setImage(undefined)
setItem(undefined)
return
}

try {
const res = await bridge.commands.executeCommand('caspar.sendCommand', item?.data?.caspar?.server, 'thumbnailRetrieve', item?.data?.caspar?.target)
const src = (res?.data || []).join('')
setImage(`data:image/png;base64,${src}`)
setItem(item)
} catch (_) {
setImage(undefined)
setItem(undefined)
}
}
getItem()
}, [selection])

return (
<ThumbnailImage src={image} alt={item?.name} />
)
}
7 changes: 7 additions & 0 deletions plugins/caspar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,11 @@ exports.activate = async () => {
uri: `${htmlPath}?path=liveSwitch`,
description: 'Control the live status of Caspar CG'
})

bridge.widgets.registerWidget({
id: 'bridge.plugins.caspar.thumbnail',
name: 'Thumbnail',
uri: `${htmlPath}?path=thumbnail`,
description: 'Display a thumbnail of the selected media item'
})
}
11 changes: 10 additions & 1 deletion plugins/caspar/lib/AMCP.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ exports.play = (file, opts) => `PLAY ${layerString(opts)} ${file} ${transitionSt
* @param { AMCPOptions } opts
* @returns { String }
*/
exports.playLoaded = (file, opts) => `PLAY ${layerString(opts)}`
exports.playLoaded = opts => `PLAY ${layerString(opts)}`

/**
* Stop an item running in the foreground
Expand Down Expand Up @@ -157,3 +157,12 @@ exports.cgUpdate = (data, opts) => `CG ${layerString(opts)} UPDATE ${opts.cgLaye
* @returns { String }
*/
exports.mixerOpacity = (opacity, opts) => `MIXER ${layerString(opts)} OPACITY ${opacity} ${transitionString(opts)}`

/**
* Get the thumbnail for a file
* @see https://github.com/CasparCG/help/wiki/AMCP-Protocol#thumbnail-retrieve
* @param { String } fileName
* @param { AMCPOptions } opts
* @returns { String }
*/
exports.thumbnailRetrieve = fileName => `THUMBNAIL RETRIEVE ${fileName}`
83 changes: 45 additions & 38 deletions plugins/caspar/lib/Caspar.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,59 +226,66 @@ class Caspar extends EventEmitter {

/**
* @private
* This function processes data received
* as a response from Caspar by bundling chunks
* together and producing a response object,
* which is the resolved through the
* matching transaction
*
* The parsing step is inspired by
* the caspar connector written by
* SuperflyTV
*
* @see https://github.com/SuperFlyTV/casparcg-connection/blob/master/src/connection.ts
*/
_processData (chunk) {
const newLines = chunk.toString('utf8').split('\r\n')
const lastLine = newLines.pop()

if (lastLine !== '') {
this._unfinishedLine = lastLine
} else {
this._unfinishedLine = ''
if (!this._unprocessedData) {
this._unprocessedData = ''
}

this._unprocessedLines.push(this._unfinishedLine + newLines.shift(), ...newLines)
this._unprocessedData += chunk.toString('utf8')
const newLines = this._unprocessedData.split('\r\n')

if (this._isProcessingData) {
return
}
this._isProcessingData = true
this._unprocessedData = newLines.pop() ?? ''
this._unprocessedLines.push(...newLines)

while (this._unprocessedLines.length > 0) {
const line = this._unprocessedLines.shift()

/*
Finish the response object
if the line is empty and we're
waiting for more lines
*/
if (line === '' && this._currentResponseObject) {
this._resolveResponseObject(this._currentResponseObject)
this._currentResponseObject = undefined
continue
}
const line = this._unprocessedLines[0]
const res = RES_HEADER_REX.exec(line)

if (res?.groups?.code) {
let processedLines = 1

/*
Initialize a new response object
if there isn't one already
*/
if (!this._currentResponseObject) {
const header = RES_HEADER_REX.exec(line)?.groups || {}
this._currentResponseObject = {
...header,
const resObject = {
...(res?.groups || {}),
data: []
}

if (this._unfinishedLine === '') {
this._resolveResponseObject(this._currentResponseObject)
this._currentResponseObject = undefined
if (resObject.code === '200') {
const indexOfTermination = this._unprocessedLines.indexOf('')

resObject.data = this._unprocessedLines.slice(1, indexOfTermination)
processedLines += resObject.data.length + 1
} else if (resObject.code === '201' || resObject.code === '400') {
if (this._unprocessedLines.length < 2) {
break
}
resObject.data = [this._unprocessedLines[1]]
processedLines++
}

this._unprocessedLines.splice(0, processedLines)

this._resolveResponseObject(resObject)
} else {
this._currentResponseObject.data.push(line)
/*
Unknown error,
skip this line
and move on
*/
this._unprocessedLines.splice(0, 1)
}
}

this._isProcessingData = false
}

/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/caspar/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const PLAY_HANDLERS = {
},
'bridge.caspar.media': async (serverId, item) => {
await commands.sendCommand(serverId, 'loadbg', item?.data?.caspar?.target, item?.data?.caspar?.loop, 0, undefined, undefined, undefined, item?.data?.caspar)
return commands.sendCommand(serverId, 'playLoaded', '', item?.data?.caspar)
return commands.sendCommand(serverId, 'playLoaded', item?.data?.caspar)
},
'bridge.caspar.template': (serverId, item) => {
return commands.sendCommand(serverId, 'cgAdd', item?.data?.caspar?.target, item?.data?.caspar?.templateData, true, item?.data?.caspar)
Expand Down

0 comments on commit 175b0aa

Please sign in to comment.