Source Properties in XJS 3.0

This is a technical draft related to the usage of source properties in XJS 3.0 pre-pre-pre alpha. The current implementation is just the proposed usage, which is documented in this post.


Specifying the source property's URL

import Xjs from '@dcefram/xjs';

const xjs = new Xjs();

xjs.setConfigurationWindow(`${window.location.href}#source-props`);

You should execute this method in your source plugin. This approach removes the use of meta tags, which was the approach used by the previous XJS version.

Rationale

The reasoning to why we opted to do this rather than meta tags is because I've been using React for source plugins, and calling the internals for setting the source property window's URL caused some issue with the packaged version (PLG) IF both the source plugin and source props HTML has the meta tag... which they would have by default, since they both use the same public/index.html file

I had to create a tool that would remove/add the meta tags on the build step. Not fancy at all.

Oh, if you're doing some React stuff, I suggest that you create a utils file that would just initialize xjs, and then import that whenever you need to use the top-level xjs instance. Set the config URL in a useEffect or componentDidMount. Codes explains better than a bunch of words, so:

// src/utils/xjs.ts
import Xjs from '@dcefram/xjs';
const xjs = new Xjs();

export default xjs;

// src/plugin/index.tsx
import React, { useState, useEffect } from 'react';
import xjs from 'utils/xjs';

export default function() {
  const [isPropsAttached, setPropsAttached] = useState(false);

  useEffect(() => {
    xjs
      .setConfigWindow(`${window.location.href}#source-props`)
      .then(() => setPropsAttached(true));
  }, []);

  useEffect(() => {
    if (!isPropsAttached) return;

    // Send someOtherProps to property window here
  }, [isPropsAttached, someOtherProps]);

  // Rest of code
}

Oh, and for sending data from source plugin to source props, we can make use of localStorage as long as we make sure both the plugin and source props have the same origin in the final plg file :) PM me if you got questions with that.


Sending data between source plugin and source props

Tbh, we can make use of web native functions for communication between the 2 windows, but there's an XSplit specific method to do so, at least for sending data from source props to source plugin.

Sending data from source plugin to source props

Use localStorage. Just make sure that you have some sort of a unique key, as an identifier for the source props to know that the message came from the right source plugin... just incase other plugins use the same approach for sending data back to the source props.

Codes speak better than sentences:

// Source plugin
import ItemProps from '@dcefram/xjs/props/item-props';
import getCurrentItem from '@dcefram/xjs/core/item/get-current-item';

const fn = async () => {
  try {
    const item = await getCurrentItem();
    const id = await item.getProperty(ItemProps.srcid);
    const lsKey = `some-string-${id}`;

    // I'd like to point out that this isn't safe...
    // Other plugins could possibly access the same
    // localStorage if they're packaged as they would
    // eventually share the same origin
    localStorage.setItem(lsKey, JSON.stringify({
      apiKey: 'somekey',
    });
  } catch (error: any) {
    console.error(error);
  }
};

fn();

// Source props
import ItemProps from '@dcefram/xjs/props/item-props';
import getCurrentItem from '@dcefram/xjs/core/item/get-current-item';

try {
  const item = await getCurrentItem();
  const id = await item.getProperty(ItemProps.srcid);
  const lsKey = `some-string-${id}`;

  window.addEventListener('storage', ({ key, newValue }: StorageEvent) => {
    if (lsKey !== key) return;

    const config = JSON.parse(newValue as string);

    // Do something with config
  });
} catch (error: any) {
  console.error(error);
}

Sending data from source props to source plugin

We can use localStorage here too, but there's a much more XSplit "native" way, which is built-in the framework... It's a little more secure as this ensures that only the connected plugin receives it :)

// Source props
import send from '@dcefram/xjs/core/source-property-window/send';

send({ data: 'data' });

// Source plugin
import subscribe from '@dcefram/xjs/core/source-property-window/subscribe';

subscribe((data: any) => console.log(data));

Now, I'm still unconvinced that this is the right names to use. I honestly prefer these to be simple functions rather than have a class with some methods in it, but the downside is that the function names seem to be too... vague.

If this was a class, we would have something like how Electron does its message passing:

// This is Electron thingy, not our framework!
// Renderer
const { ipcRenderer } = require('electron');

ipcRenderer.send('some-key', 'some-string');

// The main process
const { ipcMain } = require('electron');

ipcMain.on('some-key', fn);

The only downside with classes is that the resulting bundled code might include all other unused methods of the class... Since one of the objective of this framework re-write is to decrease the bundle size, specially the resulting bundle size of the project that would use our framework, I opted for a lodash/date-fns approach.

So now, I'd like to know what you think. The alternative API would've look like this:

// Warning! This isn't how this is implemented right now
// Source props
import sourceProps from '@dcefram/xjs/core/source-property-window';

sourceProps.emit('some-key', 'message');

// Source plugin
import sourceProps from '@dcefram/xjs/core/source-property-window';

sourceProps.on('some-key', fn);

But the resulting code would have both the emit and on bundled even if you just wanted to use emit... and all other source property related methods like resizing, setting of custom tabs, etc.

Configuring source props (modes, custom tabs, etc.)

The source property, similar to Xjs2.x, has two modes: Fullscreen and Tabbed mode. You can also create custom tabs and sort the tabs if in Tabbed mode.

// In source props
import initialize from '@dcefram/xjs/core/source-property-window/configure';
import { MODES } from '@dcefram/xjs/core/source-property-window/set-mode';

initialize({
  mode: MODES.TABBED, // Or MODES.FULL
  customTabs: ['Auth', 'Integrations'],
  tabOrder: ['Auth', 'Integrations', 'Color'],
});

Now, the underlying behavior of custom tabs is somewhat funky, in that the reality is there's only one possible HTML that can be used as a custom property window for each plugin.

So in order for our plugin's source props know which custom tab is selected, we would be receiving a simple message event in the window namespace.

// Source props
window.addEventListnener('message', (event: MessageEvent) => {
  const data = JSON.parse(event.data);

  if (data.event !== 'set-selected-tab') return; // Ignore other messages

  const selectedCustomTab = data.value;

  // Do something with `selectedCustomTab`
});

I am aware that this is abstracted in XJS 2.x, but I'm having doubts if creating an abstraction for this is really necessary. Thoughts anyone?

Resizing source props

// In source props
import resize from '@dcefram/xjs/core/source-property-window/resize';

resize(354, 490);

Again, this whole source props related feature is still open for discussion. I am already using this for the new stream-chat plugin, but I very much need feedback if this is the right path, if there are areas for improvement, or other differing ideas that might make more sense than the approach that I opted to use here.

I would really appreciate comments if this is the right direction to pursue for the framework. Please tweet me!