How to post to Wordpress when a broadcast starts, by using a Google Cloud Function

Receiving a Webhook event

Start by following our guide on how to receive a webhook on GCF.

Create a new http triggered function and remember to register it as a receiver on Bambuser dashboard's developer page.

At this point, our main function in index.js will receive the webhook event into a variable, let's call it message this time around:

exports.postBroadcastToWordpress = async (req, res) => {
  // https://bambuser.com/docs/api/webhooks/#example-webhook-notification
  const message = req.body;

To keep the rest of the code clearer, let's validate some basic assumptions on the message structure

if (!message.action || !message.payload || !message.payload.resourceUri) {
  return res.status(400).send('Incoming message has an unexpected form');
}

...and complain formally with a 4xx client error if the sender gave us a funky-looking message

The broadcast webhook will fire multiple times for a typical broadcast. Most of the time you get an add event, followed by several update events each time something has changed significantly, whether an edit to the broadcast title or a significant geolocation change. Another common update is when a broadcast goes from being live to an archived replayable mode: the type field indicates this state.

For simple scenarios where we just want to react once per broadcast, we need to ignore all update events, otherwise our target system will be spammed with duplicates!

if (message.action !== 'add') {
  console.log('ignoring non-add event');
  return res.status(200).send('Ignoring non-add event');
}

Next, let's destructure some useful items from the payload, to keep our code short and readable:

let { id, resourceUri, title, author } = message.payload;

As post title, let's use the broadcast title. However, sometimes the title field is empty. The user might leave it blank. Some RTMP ingest tools do not even expose a title field, nor does the WebRTC broadcaster on the content page. We'll keep things simple and fall back to a static string in that case:

if (!title) title = 'New broadcast';

Sometimes we have other interesting metadata available. If your content originates from the Bambuser app for Android or iOS, the author field will contain the name of the user in your team who produced the broadcast.

Just as an example, let's append the author value to the title when present:

if (author) title += ' by ' + author;

Sending metadata from the broadcaster to the REST API and Webhook messages

The title and author field as well as a custom data field are available in the Android and iOS SDK:s and can be set to strings of your choice, should you like to use a similar approach in your own broadcasting app.

When using third party RTMP based broadcasters on the other hand, the title field is probably the only way to pass along a custom string (and not all broadcast tools expose it). Feel free to use it for other means than human-readable text, if you feel the need to.

Installing a WordPress client

We could construct a bare HTTP request that creates a blog post via the WordPress API, but using a WordPress-aware npm module is easier. We'll try wpapi since it is fairly popular and has Promise & async / await support out-of-the-box.

Switch to the package.json tab and add wpapi to the list of dependencies:

"dependencies": {
  "wpapi": "^1.1.2"
}

The Google Cloud Function environment will automatically install wpapi for us when we make a change to package.json, which will allow us to require('wpapi') in our function code in index.js.

Next, create an instance of wpapi and provide it with your blog's base url and credentials with posting privileges

const blog = (await WPAPI.discover('https://example.com')).auth({
  username: 'replaceme',
  password: 'replaceme',
});

Using the embedded player

In most one-blog-post-per-broadcast scenarios, showing an embedded player is desireable. Let's use the basic iframe embed syntax in the Content field. When constructing the player url, we must urlencode the resourceUri, since it is essentially an url transported within an url. We end up with something along the lines of:

const playerUrl = `https://dist.bambuser.net/player/?resourceUri=${encodeURIComponent(resourceUri)}`
const embedHTML = `<iframe src="${playerUrl}" style="border: none" allowfullscreen></iframe>`;

Our embedHTML variable will be part of the content section of our blog post.

Submitting a blog post

Finally, send the assembled blog post to the WordPress API using our wpapi instance:

await blog.posts().create({
  title,
  content: embedHTML,
  status: 'publish',
});

When that request succeds, all we have left to do is to inform Bambuser's webhook sender that we've processed this webhook message successfully, by returning 200 on the incoming request.

res.status(200).send('post created');

If we do not send a 200 response, the webhook service will try to resend the message later which if our code was otherwise correct, would lead to duplicate blog posts.

Putting it all together

package.json:

{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "wpapi": "^1.4.1"
  }
}

index.js:

const WPAPI = require('wpapi');

exports.postBroadcastToWordpress = async (req, res) => {
  // https://bambuser.com/docs/api/webhooks/#example-webhook-notification
  const message = req.body;

  // Abort if input doesn't look ok
  if (!message || !message.action || !message.payload || !message.payload.resourceUri) {
    return res.status(400).send('Incoming message has an unexpected form');
  }

  // Avoid duplicates by ignoring update events
  if (message.action !== 'add') {
    console.log('ignoring non-add event');
    return res.status(200).send('Ignoring non-add event');
  }

  // Pick relevant values from payload and set default values
  let { id, resourceUri, title, author } = message.payload;
  if (!title) title = 'New broadcast';
  if (author) title += ' by ' + author;

  // Set up a WordPress client and authenticate with your blog
  const blog = (await WPAPI.discover('https://example.com')).auth({
    username: 'replaceme',
    password: 'replaceme',
  });

  // Construct an HTML embed code
  // https://bambuser.com/docs/playback/web-player/#basic-iframe-embed
  // https://bambuser.com/docs/key-concepts/resource-uri/
  const playerUrl = `https://dist.bambuser.net/player/?resourceUri=${encodeURIComponent(resourceUri)}`
  const embedHTML = `<iframe src="${playerUrl}" style="border: none" allowfullscreen></iframe>`;

  // Create blog post
  await blog.posts().create({
    title,
    content: embedHTML,
    status: 'publish',
  });

  console.log(`created a blog post for broadcast ${id}`);
  res.status(200).send('post created');
};

Whitelisting iframes in WordPress

By default, WordPress filters HTML tags quite aggressively. The iframe tag needed to embed a Bambuser player is not allowed unless you whitelist it.

This can be done by adding the following snippet to theme/functions.php or to a custom plugin.

Security

You should probably not enable this if you allow untrusted people to edit the blog posts on your site.

add_filter('wp_kses_allowed_html', 'allow_iframe_func', 10, 2);
function allow_iframe_func($allowedposttags, $context){
    $allowedposttags['iframe'] = array(
        'id'=> 1,
        'class'=> 1,
        'style' => 1,
        'src' => 1
    );
    return $allowedposttags;
}

In other words, this requires a WordPress host that allows you to edit your php files directly. On wordpress.com for example, a business plan is required.

Alternatives

If you prefer not to write any code, have a look at how Zapier or Integromat can be used to accomplish the same thing.

If you are an AWS user, using Lambda is very similar.

You can use the same idea if you're running a server on your own as well, as long as you have a publicly available url that the webhook server can reach.