Using JavaScript to extend operations

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 and xml, or a media type such as application/json. The default is json, 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 and xml, 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 - A javax.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 using api.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:

Example:

var uri = api.path('version').getInfo().uri;
http.uri(uri).user('admin','admin').dataType('JSON').get().licenseInfo.licenceType

Doing HTTPS connection to self-signed hosts

Since:22.2.

When doing https calls, the server host’s certificate is matched against the installed root certificates. For a self-signed host, the certificate can be added in the configuration property x509Certificates.

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 then output will not be included in the response.
    • err - java.io.OutputStream to contain the error output from the command. If this field is specified then output 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:
  • metadataMetadataType
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

  1. Enable JavaScript debugging. Set the configuration property debugJavaScript to true.

  2. Execute a script. Use POST /javascript/test to execute some JavaScript code:

    POST API/javascript/test
    Content-Type: application/javascript
    
    /* interpreter=graalvm */
    var a=3;
    var b=4;
    a+b;
    

    If debugJavaScript is true, then the call will not return immediately.

  3. Connect the debugger. From GET /javascript/session, find the Chrome Devtools debugging URL:

    GET API/javascript/session
    
    HTTP/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

  1. Enable JavaScript debugging. Set the configuration property debugJavaScript to true.
  1. 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.

  1. Execute a script. Now, Eclipse is ready to connect. Use POST /javascript/test to execute some JavaScript code:

    POST API/javascript/test
    Content-Type: application/javascript
    
    var a=3;
    var b=4;
    a+b;
    

    If debugJavaScript is true, then the call will not return immediately.

  2. 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 text debugger;. 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);'