IMAG0601

HTML5 (CEP) Panels for After Effects: two engines for the price of one

As I mentioned in my previous post, one of the keys to CEP panels is that they contain two distinct Javascript engines: the more modern one that runs and interacts with the panel UI (in what’s basically an embedded Chrome window), and the ExtendScript (JSX) engine, which is based on an older version of Javascript and interacts with the host application.

They’re asynchronous, which means they’re perfectly happy to ignore each other and run on their own thread at their own pace. But a UI that couldn’t talk to the host application wouldn’t be very useful, so this post is all about getting the two engines to work together.

Sending data from one to the other can sometimes feel a bit like this:

There are a few tricks to make it easier, though, and to make sure the data’s being sent at the right time.

Calling ExtendScript functions from Javascript

You’ll probably need to do this a lot. But I have good news: it’s easy!

There’s a special function, evalScript(), included with CEP’s CSInterface.js library that enables you to call both embedded ExtendScript and functions saved in a separate file. It’s good practice to separate them (and means you won’t have to do all your AE scripting within quotes!).

  1. Include your .jsx file in CSXS/manifest.xml, like so:
     ...
     <Resources>
          <MainPath>./index.html</MainPath>
          <ScriptPath>./jsx/hostscript.jsx</ScriptPath>
     </Resources>
    ...
  2. You need to reference the CSInterface library in order to use its functions, so make sure you have
     var csInterface = new CSInterface();

    at the top of your .js file.

  3. Now you can use csInterface.evalScript() to call:
    • Embedded ExtendScript:
      csInterface.evalScript('app.project.items.addComp("Sample", 1920, 1080, 1, 1, 23.976)');
    • An external ExtendScript function:
      Include the function in your JSX file, like so:

      function addSampleComp() {
           app.project.items.addComp("Sample", 1920, 1080, 1, 1, 23.976);
      }

      Then call it from the JS side:

      csInterface.evalScript('addSampleComp()');

Sending data from Javascript to ExtendScript

Let’s get fancier.

Note the quote marks surrounding  the ExtendScript code above. The Javascript engine sees each of those code snippets as one big string. If you need to pass the value of a Javascript variable to ExtendScript, you’ll need to concatenate strings around the variable:

var compName = "Sample";
csInterface.evalScript('app.project.items.addComp("' + compName + '", 1920, 1080, 1, 1, 23.976)');

Note that the inner (double) quotes are still there! ExtendScript strings need to be in quotes too, so you end up with quotes within quotes. This is one of the first things I check when debugging — it’s easy to forget to include the inner quote marks.

What about functions with multiple arguments?

csInterface.evalScript('setFilepath("' + pathType + '","' + result + '")');

Hmm. But I need to send a lot of data.
Pass an object of any size using JSON.stringify().

csInterface.evalScript('initRecording(' + JSON.stringify(recData) + ')');

You don’t need to parse the object on the JSX side before accessing it.

Calling Javascript functions from ExtendScript

I cheated a bit with the last example. In my actual script, that line looks a little different:

csInterface.evalScript('initRecording(' + tracktype + ',' + JSON.stringify(recData) + ')', setRecVars);

There’s a second, optional part to .evalScript(). In this example, setRecVars is a callback function. That means the Javascript engine waits for ExtendScript to complete whatever it needs to do, then lets the Javascript engine know it’s done, and that it can go ahead and execute that function.

You can call a function you’ve declared elsewhere in your script, like I’ve done in this example, or you can add a one-off anonymous function.

function callbackFunction(){
     // Do some stuff
}
csInterface.evalScript('someFunction()', callbackFunction);

vs.

csInterface.evalScript('someFunction()', function(){
     // Do some stuff
 });

I’m not going to go too in-depth on callbacks here, but they are vital to CEP panel development. Not only can you do things like change your UI based on the comp you just created, you can chain a bunch of these functions together and pingpong your way across the JS-JSX divide.

Sending data from Extendscript to Javascript

The simplest method is to return a single value, and handle it with a callback function, as above.

When you need to transmit more information, things get trickier. The official documentation suggests an elaborate process involving the use of the plugPlugExternalObject library to dispatch events with payloads. You can ignore those recommendations, and simply send send data from JSX to JS as stringified JSON.

Well, I say “simply” but there are two big caveats:

  • JSON isn’t built into ExtendScript, so you’ll need to download the library and include it at the top of your JSX file:
#include "../js/libs/json2.js"
  • Much more confusingly, JSON.stringify() doesn’t like packing up “circular values” — values that refer to other values. For example, if you’ve shortened app.project.activeItem to a variable called theComp, sending  the JS engine a reference to a layer as theComp.layer(index) won’t work. Fortunately, Michael Delaney figured out a solution:
// Michael Delaney's fix to strip out circular functions
        function (data, out) {
            var e, k, results;
            if (out === null) {
                out = {};
            }
            if (!data) {
                return void 0;
            }
            if (typeof data === 'object') {
                results = [];
                for (k in data) {
                    if (k === "theComp" || k === "parentFolder" || k === "source") {
                        if (data[k]) {
                            out[k] = data[k].id;
                        }
                    }
                }
                return results;
            } else {
                return data;
            }
        }

This function replaces circular references with their specific ID values. Run your outgoing object through it before returning your data object as JSON, and it should work.

return JSON.stringify(data);

On the JS side, handle the JSX data by using JSON.parse() to parse the result in the callback function. Here’s a simple example that notifies the user if an error occurred and changes a variable:

function endRecording(result) {
    returnedData = JSON.parse(result);
    if (returnedData.warning !== undefined) {
        alert(returnedData.warning);
        return;
    }
    ready = returnedData.ready;
}

 

Referring to another file from ExtendScript

I’ll add one last thing as a bonus, because it took me way too long to figure this one out.

Say you have a preset saved as an .ffx file in the same directory as your JSX file. You’d think you could just use .applyPreset(File(“presetfile.ffx”)), right?

NOPE.

There’s a weird quirk of CEP panels where, when you call its functions via JS, your JSX file doesn’t know its own directory. This shouldn’t be this hard, but here’s my super awesome workaround (note sarcasm):

On the JSX side:

var presetFile;
function setJSXpath(pathdata) {
      presetFile = File(pathdata + "presetfile.ffx");
 }
targetLayer.applyPreset(presetFile);

On the JS side (which does know its directory):

var JSXpath = (window.location.href.substr(0, location.href.lastIndexOf("/") + 1) + "jsx/").toString();

csInterface.evalScript('setJSXpath("' + JSXpath + '")');

Not elegant, but it works.¯\_(?)_/¯

2 thoughts on “HTML5 (CEP) Panels for After Effects: two engines for the price of one”

  1. The bonus thing made my day, had been trying to figure out for way too long how to get the path in JSX. Many thanks for that!

Leave a Reply