import Device from './Device.js'
import { BASE_URL, DELETE, checkResponse } from './Common.js'
/**
* @classdesc CaptureBridge utilizes a plugin system for interacting with
* hardware devices and vendor SDKs. Clients will interact with plugins via the
* REST API using their unique plugin ID. This class provides basic methods
* for working with these plugins such as obtaining a list of compatible devices
* and managing the plugin's configuration.
*
* @see {@link CaptureBridge#plugins} Get all installed plugin.
* @see {@link CaptureBridge#plugin} Get a single plugin by ID.
*/
class Plugin {
/**
* @property {string} id - ID of the plugin, used when building REST endpoint paths.
* @property {string} name - Human friendly name for the plugin.
* @property {string} description - Human friendly description of plugin.
* @property {string} version - {@link https://semver.org/|Semantic version} version of the plugin.
* @property {string[]} methods - A plugin's list of supported RPC methods.
* @property {boolean} ready - True if plugin has been loaded into memory and is ready to receive requests.
* @property {object} defaultDevice - Default device to use for the plugin.
* @property {string[]} configMethods - Methods required to implement configuration.
*/
baseUrl
id
name
description
version
methods = []
ready = false
defaultDevice = null
configMethods = ['config_schema', 'get_config', 'set_config']
/**
* Instantiate a plugin.
* @constructor
* @param {object|string} plugin - plugin object as received from the API or a plugin ID string.
* @param {string} plugin.id - ID of the plugin, used when building endpoint paths to call methods on the plugin.
* @param {string} plugin.name - Human friendly name of plugin.
* @param {string} plugin.description - Human friendly description of plugin.
* @param {string} plugin.version - {@link https://semver.org/|Semantic version} version of the plugin.
* @param {string[]} plugin.methods - plugin's capabilities.
* @param {boolean} plugin.ready - True if plugin has been loaded into memory and is ready.
* @param {string} [baseURL=BASE_URL] - Override the default protocol, domain, and port for the service.
* @throws Will throw an Error the plugin argument is not a plugin ID (String) or an object.
*/
constructor (plugin, baseUrl = BASE_URL) {
this.baseUrl = baseUrl
if (typeof plugin === 'string') {
this.id = plugin
} else if (typeof plugin === 'object') {
Object.assign(this, {}, plugin)
} else {
throw new Error('Invalid properties supplied to Plugin constructor')
}
}
/**
* Get all devices for this plugin
*
* @method
* @async
* @see {@link https://local.capturebridge.net:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
* @returns {object[]} Array of {@link Device} objects.
* @example
* const plugin = await captureBridge.plugin('capture_camera_canon')
* const devices = await plugin.devices()
*/
async devices () {
const response = await fetch(`${this.baseUrl}/plugin/${this.id}/device`)
await checkResponse(response)
const devices = await response.json()
return devices.map(d => new Device(d, this, this.baseUrl))
}
/**
* Get a device by ID for this plugin
*
* @method
* @async
* @see {@link https://local.capturebridge.net:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
* @param {string} id - Device ID
* @returns {object?} Requested {@link Device} or null
* @example
* const plugin = await captureBridge.plugin('capture_camera_canon')
* const device = await plugin.device('AWOOO56709')
*/
async device (id) {
const response = await fetch(`${this.baseUrl}/plugin/${this.id}/device`)
const devices = await response.json()
const device = devices.find(d => d.id === id)
return device ? new Device(device, this, this.baseUrl) : null
}
async configSchema () {
if (!await this.supportsConfig()) {
throw new Error('Plugin does not support config')
}
const response = await fetch(`${this.baseUrl}/plugin/${this.id}/config/schema`)
return await response.json()
}
async config () {
if (!await this.supportsConfig()) {
throw new Error('Plugin does not support config')
}
const response = await fetch(`${this.baseUrl}/plugin/${this.id}/config`)
return await response.json()
}
async saveConfig (config) {
if (!await this.supportsConfig()) {
throw new Error('Plugin does not support config')
}
const options = {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
}
const url = `${this.baseUrl}/plugin/${this.id}/config`
const response = await fetch(url, options)
return await response.json()
}
async supportsConfig () {
await this.update()
return this.configMethods.every(m => this.methods.includes(m))
}
/**
* Shutdown a plugin
* @method
* @async
* @see {@link https://local.capturebridge.net:9001/doc/#api-Plugin-ShutdownPlugin|API Endpoint (/plugin/:pluginId)}
* @param {number} [timeout=10] - Timeout in seconds. Forcefully terminate
* the plugin if it doesn't shutdown cleanly within the specified timeout. A
* value of `0` will result in immediately terminating the plugin and should
* be avoided unless the plugin is unresponsive.
* @example
* await plugin.shutdown()
*/
async shutdown (timeout = 10) {
const url = `${this.baseUrl}/plugin/${this.id}?timeout=${timeout}`
const response = await fetch(url, DELETE)
return await response.json()
}
async startup () {
// See: ValidRD/wa_capture_bridge/issues/48
// We need an explicit endpoint for this instead
const devices = await this.devices()
if (devices) {
await this.update(true)
return { status: 'starting' }
}
return { status: 'failed' }
}
/**
* This class can be instantiated from either a plugin ID or a full plugin
* object from an API request.
* Because constructors cannot be async we use this method to hydrate the
* plugin properties if needed.
*
* @private
* @method
*/
async update (force = false) {
if (force || this.methods.length === 0) {
const response = await fetch(`${this.baseUrl}/plugin`)
const plugins = await response.json()
const plugin = plugins.find(p => p.id === this.id)
Object.assign(this, plugin)
}
}
/**
* Check if a plugin has an RPC method
*
* @param {string} method - An RPC method
* @returns {boolean} Return true if plugin has the provided method.
* @example
* const canCapture = await plugin.hasMethod('capture')
* @method
*/
async hasMethod (method) {
await this.update()
return this.methods.includes(method)
}
async rpc (request) {
const options = {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
}
const url = `${this.baseUrl}/plugin/${this.id}/rpc`
const response = await fetch(url, options)
return await response.json()
}
async ping () {
const response = await fetch(`${this.baseUrl}/plugin/${this.id}/ping`)
return await response.json()
}
/**
* Classes that extend this one will add their own methods to perform
* operations on a device. They will call this method first to get a device
* object to use for making calls to the appropriate endpoint.
*
* This method really should be "protected" in classical OOP terms but
* Javascript does not currently have support for such scopes, and it's not
* marked using the ES6 private modifier so that it can be inherited by it's
* children.
* @private
* @method
*/
async setupDevice (deviceOpt) {
await this.update()
if (deviceOpt instanceof Device) {
this.defaultDevice = deviceOpt
return this.defaultDevice
}
if (typeof deviceOpt === 'string') {
this.defaultDevice = this.device(deviceOpt)
return this.defaultDevice
}
if (!this.defaultDevice) {
const devices = await this.devices()
if (!devices.length) {
throw new Error('No devices found for the plugin')
}
this.defaultDevice = devices[0]
return this.defaultDevice
}
return this.defaultDevice
}
}
export default Plugin