Using JavaScript to extend operations [VC 21.3 GEN]
JavaScript can be used to add integration code in a number of places, such as job tasks, transcode presets, naming scripts, etc. This article describes functions and utilities that are common to all JavaScript invocations.
If a script is not working as expected, then it is also possible to debug the script using Eclipse.
JavaScript engines
There are two JavaScript engines that can be used for evaluating JavaScript.
- GraalVM JavaScript
GraalVM JavaScript is an ECMAScript 2019 compliant JavaScript implementation built on GraalVM. This is the default engine.
- Rhino
Rhino is the legacy engine that has been used since the Vidispine project was started. Rhino supports ECMAScript for XML (E4X).
New in version 5.0: GraalJS was added as a script engine.
Selecting a JavaScript engine
The default JavaScript engine can be configured using the javascriptInterpreter
configuration property, but the JavaScript engine to use can also be specified using a comment in the script itself.
The format is:
/* interpreter=graalvm|rhino */
For example, this script would be evaluated using GraalJS:
/* interpreter=graalvm */
let xs = [1, 2, 3];
xs = xs.map(x => x + 1);
xs
While this would use Rhino:
/* interpreter=rhino */
var xs = [1, 2, 3];
for (var i = 0; i < xs.length; i++) {
xs[i]++
}
xs
Migrating to GraalVM JavaScript from Rhino
JavaScript scripts that have been written against Rhino may need to be updated to run properly on GraalVM JavaScript.
E4X
E4X is not supported by GraalJS. Scripts that use E4X should be updated to explicitly use Rhino, or be changed to not use E4X to build or parse XML.
Java interoperability
Both engines can interface with Java objects and classes seamlessly. However, while Rhino will allow lossy conversion, for example, converting a double value (e.g. 14.2
) to an integer value (14
), for GraalJS a TypeError
will be thrown.
Scripts may thus need to be updated to perform integer rounding using Math.round
. For example, scripts in transcode presets that modify the resolution may need to be updated from:
var r = new com.vidispine.generated.ResolutionType();
r.setWidth(512 / 1.5);
To explicitly round using Math.round
:
var r = new com.vidispine.generated.ResolutionType();
r.setWidth(Math.round(512 / 1.5));
Java return values
Rhino will not automatically convert value objects to the native JavaScript counterpart. GraalJS will on the other hand. For example, this will work on Rhino:
// the return type of getWidth is a java.lang.Long
var width = preset.getThumbnailResolution().getWidth().intValue();
But with GraalJS the getWidth
method will return a JavaScript number, not a java.lang.Long
.
Common JavaScript functions
A number of global variables are defined for the script to use. It is also possible to add custom global JavaScript objects and functions, as described in Add generic JavaScript code.
The api
object
The api
object can be used to perform a synchronous HTTP request to the Vidispine API. By default the request will be performed as the user that created the job that is running, unless overridden by the script using the api.user()
function.
These functions all return a new api
object with the parameters of the function added to it, and should thus be chained as shown in the example below.
-
api.
path
(path) Adds the given path to the API URI.
Arguments: path (string) – The path to add.
-
api.
queryParam
(key, value) Adds a query parameter to the API URI.
Arguments: key (string) – The name of the query parameter to set.
value (object) – The value to set. Primitive types will be converted to a string.
-
api.
header
(key, value) Adds a header parameter to the API URI.
Arguments: key (string) – The name of the header to add.
value (string) – The header value to add.
-
api.
dataType
(type) The type of data that should be returned from the server.
Arguments: type (string) – Supported types are
text
,json
andxml
, or a media type such as application/json. The default isjson, xml
.
-
api.
input
(input[, type]) The data to be sent. The content type is optional if the input is a JavaScript or XML object, but mandatory if input is a string (such as a JSON or a XML string).
Arguments: input (string) – The data to be sent.
type (string) – Supported types are
text
,json
andxml
, or a media type such as application/json.
-
api.
user
(username[, password]) The user to authenticate as. If no password is specified then the request will be authenticated using token authentication.
Arguments: username (string) – The username to set.
password (string) – The password to set.
-
api.
timeout
(timeout) Sets the timeout of the request.
Arguments: timeout (long) – The timeout in milliseconds.
Once the request parameters have been specified the request can be performed using one of these four functions:
-
api.
get
() Performs a GET request.
-
api.
put
() Performs a PUT request.
-
api.
post
() Performs a POST request.
-
api.
delete
() Performs a DELETE request.
For example, to retrieve the metadata and shapes for a specific item:
item = api.path("item").path(itemId)
.queryParam("content", "metadata,shape")
.get();
Rich output
By adding rich()
on the api
chain, more information about the HTTP response is given. Without rich
, the operation functions (api.get()
et al.) only return the value returned by the API, and throws an exception if the API returns an error.
-
api.
rich
() With
rich
, the functions returns a JavaScript object, with the following properties:output
- The response, parsed as an object.response
- The response as a string.status
- The HTTP status code.httpheader-*
- The various HTTP headers, with the HTTP header name in lower case, e.g.httpheader-content-length
.
API call information
To aid in troubleshooting API calls, this function can be used to get information about the call that is about to be made.
-
api.
getInfo
() Returns: A JavaScript object with properties:
uri
- The URI of the request.queryParams
- Ajavax.ws.rs.core.MultivaluedMap
containing all of the query parameters.headerCount
- Number of headers set.inputIsXML
- True if the input is an XML object.inputIsJSON
- True if the input is a JSON object.returnTypes
- The media types that have been set usingapi.dataType()
.user
- The name of the user performing the request.passwordIsSet
- True if the password has been set.
The http
object
The http
object is similar to the api
object, but can be used to invoke other HTTP resources. The http
object needs to be used with the http.uri()
function, which takes one parameter, the URI to be used.
-
http.
uri
(uri) Arguments: uri (string) – The URI of the resource.
-
http.
followRedirects
(followRedirects) Arguments: followRedirects (boolean) – If true, follows HTTP redirects. Default false.
-
http.
proxy
(uri) Arguments: uri (string) – Use supplied URI as HTTP proxy. See also Proxying HTTP connection via a VSA.
Example:
var uri = api.path('version').getInfo().uri;
http.uri(uri).user('admin','admin').dataType('JSON').get().licenseInfo.licenceType
Proxying HTTP connection via a VSA
Since: | 21.3. |
It is possible to use a VSA to proxy the HTTP request. This is very useful if the endpoint is not reachable from VidiCore, but from the VSA.
In order to proxy the connection, use proxy()
. Multiple proxy calls can be chained, then VidiCore will select _one_ of the VSAs that is online at the moment to use as proxy.
-
http.
proxy
(uri) Arguments: uri (string) – The
vxa
URI of the resource.
On the VSA, the following setting is needed:
forwardProxy={regular expression}
Regular expression has to match the URI of the endpoint. In case of a HTTPS
request, the path is not available, so the regular expression has to match an empty path.
Example
In VSA’s agent.conf:
forwardProxy=https?://.*
JavaScript code:
http.uri("http://localservice:7777/integration")
.proxy("vxa://742c17e4-8f6f-489a-8caf-aae4c39d272f/")
.proxy("vxa://e2c4c766-4a3e-4662-975c-7f4251385d8c/")
.get()
The shell
object
The shell
object is used to invoke shell commands.
-
shell.
exec
(command[, arg, ...][, options]) Executes the command with the given arguments.
Arguments: command (string) – The name of the command to execute.
arg (string) – Any arguments to pass to the command.
options –
A JavaScript object with fields:
timeout
- Timeout in milliseconds.input
- Input to send to standard input.output
-java.io.OutputStream
to contain the output from the command. If this field is specified thenoutput
will not be included in the response.err
-java.io.OutputStream
to contain the error output from the command. If this field is specified thenoutput
will not be included in the response.
Returns: An object with fields:
exitcode
- The return code (an integer) from the command.output
- Standard output as a string.err
- Standard error as a string.
A step that checks a file for viruses might for example look something like:
var file = ...
var result = shell.exec("clamscan", file);
if (result.exitcode == 1) {
job.failFatal("Virus(es) found");
}
The logger
object
The logger
object outputs information to the log file of the application server. If the JavaScript object is concatenated to a string, the full representation may not be shown.
logger.log('information is '+info);
This can be fixed by using the logger.json()
function:
logger.log('information is '+logger.json(info));
-
logger.
log
(message) Logs the given message to the application server log file.
Arguments: message (object) – The message to log. If this is a JavaScript object then it will automatically be transformed into JSON format.
-
logger.
json
(object) Converts the given JavaScript object into JSON.
The metadatahelper
object
The metadatahelper
object contains some convenient functions for generating a new metadata object.
-
metadatahelper.
createMetadata
() Returns a new MetadataType.
-
metadatahelper.
createMetadataTimespan
(start, end) Returns a new MetadataType.Timespan.
Arguments: start (string) – The start timecode.
end (string) – The end timecode.
-
metadatahelper.
createMetadataGroup
(name) Returns a new MetadataGroupValueType.
Arguments: name (string) – The name of the group.
-
metadatahelper.
generateMetadataField
(name, value) Returns a new MetadataFieldValueType.
Arguments: name (string) – The name of the field.
value (string) – The field value.
-
metadatahelper.
metadataToStr
(metadata) Translate a metadata object to a string.
Arguments: metadata – MetadataType
-
metadatahelper.
log
(obj) Write the value of
obj.toString()
to the server log.
The following example script
var metadata = metadatahelper.createMetadata();
var timespan = metadatahelper.createMetadataTimespan("0", "100");
var group = metadatahelper.createMetadataGroup("mrk_marker");
var field1 = metadatahelper.createMetadataField("mrk_color", "red");
group.getField().add(field1);
timespan.getGroup().add(group);
metadata.getTimespan().add(timespan);
will generate a metadata object like this:
<MetadataDocument xmlns="http://xml.vidispine.com/schema/vidispine">
<timespan start="0" end="100">
<group>
<name>mrk_marker</name>
<field>
<name>mrk_color</name>
<value>red</value>
</field>
</group>
</timespan>
</MetadataDocument>
The notification
object
New in version 4.17.
The notification
can be used to trigger notifications. It is available to JavaScript job steps and from the JavaScript test resource.
-
notification.
send
(notificationId, data) Trigger the notification with the given id with the specified data. Returns the notifications id.
The
data
parameter must be a JavaScript object with string keys and values that are either string or a list of strings.Arguments: notificationId (string) – The id of the notification to be used.
data (object) – A JavaScript object with the key-value data to send.
Examples
For example, to trigger a notification with id VX-45
:
notification.send("VX-45", {
"hello": "world"
});
The notification id can also be an external id, and multiple values can also be provided:
notification.send("external-system", {
"hello": "world",
"items": ["VX-1", "VX-2"]
});
The data shape and delivery method and destination is decided by the notifications action. For example, if the notification VX-45
was a HTTP notification with a content-type set to application/xml
, that notification endpoint would receive:
<SimpleMetadataDocument xmlns="http://xml.vidispine.com/schema/vidispine">
<field>
<key>hello</key>
<value>world</value>
</field>
</SimpleMetadataDocument>
Debugging JavaScript
The JavaScript code can be debugged. When using Rhino, debugging can be done using Eclipse. GraalVM JavaScript supports debugging via the Chrome DevTools Protocol, using debuggers such as Chrome Developer Tools.
The configuration property debugJavaScript
controls if debugging is enabled or not. With this setting set to true
, all JavaScript code will wait for a remote debugger to attach before continuing.
Debugging can also be enabled on a per-script basis by setting the debug
flag in the script header. For example:
/* interpreter=graalvm, debug=true */
Debugging GraalVM JavaScript
Enable JavaScript debugging. Set the configuration property
debugJavaScript
totrue
.Execute a script. Use
POST /javascript/test
to execute some JavaScript code:NONEPOST API/javascript/test Content-Type: application/javascript /* interpreter=graalvm */ var a=3; var b=4; a+b;
If
debugJavaScript
istrue
, then the call will not return immediately.Connect the debugger. From
GET /javascript/session
, find the Chrome Devtools debugging URL:NONEGET API/javascript/session
NONEHTTP/1.1 200 OK Content-Type: text/plain 0 unknown STARTED/RUNNING chrome-devtools://devtools/bundled/js_app.html?ws=localhost:59000/56e2efcf-1551-48f6-ab43-f591033b5b72 null
Start Chrome and paste the
chrome-devtools://
in the URL bar and press Enter.
Debugging Rhino
Enable JavaScript debugging. Set the configuration property
debugJavaScript
totrue
.
Set up Eclipse. To set up Eclipse for debugging, select Run ‣ Debug Configurations… and create a new Remote JavaScript configuration. Use Mozilla Rhino as Connection, and port 59000. The port can be changed using the configuration property
debugJavaScriptPort
.For Source Lookup Path, select a File System Directory, and point it to any existing directory. The directory does not have to contain the source files; it will be sent via the Mozilla Rhino connector.
Execute a script. Now, Eclipse is ready to connect. Use
POST /javascript/test
to execute some JavaScript code:NONEPOST API/javascript/test Content-Type: application/javascript var a=3; var b=4; a+b;
If
debugJavaScript
istrue
, then the call will not return immediately.Connect the debugger. In Eclipse, choose Run ‣ Debug Configurations…, select the created configuration and choose Debug.
Eclipse should show a file named
testscript-xxxx.js
or similar in the source window. The first line includes the textdebugger;
. This is intentional and can be ignored; it is only added so that the debugger will actually start in suspended mode.Also, when the script starts, a random line – typically the second or third – is selected. Single-step once and the first line should be selected and the actual debugging can start. After the script has completed, the API call returns.
Changed in version 5.0: Support for enabling debugging via the script header was added.
Interfacing with the JavaScript engine manually
In order to test functionality, the JavaScript engine can be called manually. For more information, see JavaScript.
Add generic JavaScript code
In order to avoid redundant code, it is possible to register JavaScript code in a “global library”. This is done using configuration properties of the form javascript-{extension}
, where extension
is any suffix.
When doing this, all code that is in the javascript-
properties will be executed before the specific code. Multiple properties can be added, and will be parsed in lexical order. It is advised that only definitions (function
) are made, and not direct statements, in order to avoid confusion.
Example
$ curl –uadmin:admin –Hcontent-type:text/plain \
localhost:8080/API/configuration/properties/javascript-1234 –X PUT --data-binary \
'function add(a,b) {
return a+b;
}'
$ curl –uadmin:admin –Hcontent-type:application/javascript \
localhost:8080/API/javascript/test –X POST --data-binary \
'var a=3;
var b=4;
add(a,b);'