RSS feed video generator

Discover how we used the Moovly API in conjunction with a given RSS feed to generate a video on the go

Watch demo

For this demo we will create a PHP scrip that generates a video containing the three latest items on a given RSS-feed with some extra checks on the validity of the RSS feeds. This demo covers RSS 2.0 specification and will be using the "breaking news" NASA RSS feed. Yet every RSS feed complying to RSS 2.0 spec can be used as a drop-in replacement.

2.1. Create project

First things first. As always, we start by creating a new project (from scratch in this case) in the Moovly dashboard. For this video we will pick a 16:9 ratio and use a coloured background.

2.2. Create video

This video contains some simple objects: an image, some text fields, and the Moovly logo in an outro. Some of those objects will be marked as template fields so they can be changed with our Automator API. But before all that, we make sure that the video looks good with all our placeholder information.

As labels in the timeline objects we use names that are meaningful. Though this has no impact on the video itself, we recommend to use meaningful naming since the API returns those labels as well to make things easier on you.

2.3. Setup template variables

Once we're satisfied with the looks of our video, we can start creating template fields from our placeholder objects.

We make the title and description field fixed. Fixed means that we can specify a maximum width and height for a field. Which makes sure that once we replace our placeholder text, the new text won't exceed the dimensions of our current placeholder text.

The image in our video is used as a background so it makes sense to mark the object fit as cover. That way we'll have the optimal placement for that image: it will always fill its space without being stretched.

2.4. Create template from project

At this point our video (with placeholder data) is finished and we will create a template from our project in the Moovly dashboard. Since you can always update a template later (by updating the original project), there's no need to worry about having a 100% perfect video at this moment.

Now that we created the template, it can be used in a multitude of ways. Like our Quick Edit menu in the dashboard, a Wordpress plugin, or by using our Automator API.

We will continue this tutorial by using the API.

3.1. Generate Moovly API token

To make use of the Moovly API, you need to generate a personal access token. This can be done in our Developer Portal. The API tokens you generate will be valid for one year. So at least once a year, you'll need to generate a new token.

Now that we've created a template and have obtained API keys for Moovly, we can start to create a PHP script that fetches items from the given RSS feed and uses that data to replace our placeholder template fields in our Moovly template.

4.1 Get template variables

First and foremost we'll prepare an array with all template variables mapped on a more readable key. We can retrieve all template variables by getting the details of the template we want to use:

## Get template
curl "https://api.moovly.com/generator/v1/templates/40f2b98a-1d2d-4ade-a227-d9d6c2e7f54d" \
     -H 'Content-Type: application/json' \
     -H 'Authorization: Bearer $MOOVLY_API_TOKEN'

This cURL request returns a JSON with a variables field containing all variables

"variables": [
    {
      "id": "04dd6c70-0183-45d3-a6d2-59e931fe9453",
      "name": "$IMAGE_1",
      "weight": 1,
      "type": "image",
      "requirements": {
        "width": 719,
        "height": 480
      },
      "default": "https:\/\/assets.moovly.com\/converted\/images\/image-35715fa6b0e9e35006066f8beb3bb940.jpg",
      "clip_id": "8eb6e27944f201c0b217"
    },
    {
      "id": "3b577eb8-bfea-4f77-ad0c-994b0457645a",
      "name": "$TITLE_3",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 300,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003ETITLE NEWS ITEM NUMBER THREE\u003C\/p\u003E",
      "clip_id": "746e62966d316be96b07"
    },
    {
      "id": "3bf1ad52-a565-4b43-b306-f26222b93628",
      "name": "$IMAGE_3",
      "weight": 1,
      "type": "image",
      "requirements": {
        "width": 723,
        "height": 480
      },
      "default": "https:\/\/assets.moovly.com\/converted\/images\/image-7818625e207844e6778e38b23c911ad4.jpg",
      "clip_id": "746e62966d316be96b07"
    },
    {
      "id": "650d872c-d0c3-43c7-a5a5-cc26dafa373f",
      "name": "$DATE",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 100,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003EDAY\/MONTH\/YEAR\u003C\/p\u003E",
      "clip_id": "a93468f19ae7fddbc316"
    },
    {
      "id": "6d43dc25-fc07-44b0-ac5e-0cf113a0d281",
      "name": "$TITLE_2",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 200,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003ETITLE NEWS ITEM NUMBER TWO\u003C\/p\u003E",
      "clip_id": "fbe61fef5de26bdfb975"
    },
    {
      "id": "951fb6dd-3d90-493d-96e7-42681527afc6",
      "name": "$TITLE_1",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 300,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003ETITLE NEWS ITEM NUMBER ONE\u003C\/p\u003E",
      "clip_id": "8eb6e27944f201c0b217"
    },
    {
      "id": "9853eeed-3968-4bea-a759-f71b69ee6d97",
      "name": "$RSS_FEED_NAME",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 200,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003ECOMPANY NAME\u003C\/p\u003E",
      "clip_id": "a93468f19ae7fddbc316"
    },
    {
      "id": "a3ea2228-2af9-47a6-875a-ac737fa77e6d",
      "name": "$DESCRIPTION_2",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 1000,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003EAt vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt moll",
      "clip_id": "fbe61fef5de26bdfb975"
    },
    {
      "id": "b17cb347-ccc9-42f3-a944-6392ce0aebfc",
      "name": "$IMAGE_2",
      "weight": 1,
      "type": "image",
      "requirements": {
        "width": 720,
        "height": 480
      },
      "default": "https:\/\/assets.moovly.com\/converted\/images\/image-d2940ddada890cd36c391456c63b0822.jpg",
      "clip_id": "fbe61fef5de26bdfb975"
    },
    {
      "id": "dc442952-082d-4616-827b-1356ff310ca4",
      "name": "$DESCRIPTION_3",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 1000,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003EAt vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt moll",
      "clip_id": "746e62966d316be96b07"
    },
    {
      "id": "f6f9c789-2506-434b-aa5a-04670c0bcf58",
      "name": "$DESCRIPTION_1",
      "weight": 1,
      "type": "text",
      "requirements": {
        "multiline": false,
        "maximum_length": 1000,
        "minimum_length": 1
      },
      "default": "\u003Cp\u003EAt vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt moll",
      "clip_id": "8eb6e27944f201c0b217"
    }
  ],

Using the variables data, we'll create a more comprehensive mapping for the template variable ids

static $templateId = '40f2b98a-1d2d-4ade-a227-d9d6c2e7f54d';

static $templateVariableIds = [
    'date' => '650d872c-d0c3-43c7-a5a5-cc26dafa373f',
    'title' => '9853eeed-3968-4bea-a759-f71b69ee6d97',
    'items' => [
        [
            'title' => '951fb6dd-3d90-493d-96e7-42681527afc6',
            'body' => 'f6f9c789-2506-434b-aa5a-04670c0bcf58',
            'image' => '04dd6c70-0183-45d3-a6d2-59e931fe9453'
        ],
        [
            'title' => '6d43dc25-fc07-44b0-ac5e-0cf113a0d281',
            'body' => 'a3ea2228-2af9-47a6-875a-ac737fa77e6d',
            'image' => 'b17cb347-ccc9-42f3-a944-6392ce0aebfc'
        ],
        [
            'title' => '3b577eb8-bfea-4f77-ad0c-994b0457645a',
            'body' => 'dc442952-082d-4616-827b-1356ff310ca4',
            'image' => '3bf1ad52-a565-4b43-b306-f26222b93628'
        ]
    ]
];

4.2 Check RSS feed validity

First we'll check if the given feed is an RSS feed, not an atom feed. Atom and RSS have different fields. Atom falls outside the scope of this example. Then we'll check if the RSS feed has equal or more items then the $templateVariableIds

$rssFeedData = new SimpleXMLElement(file_get_contents($rssFeed));

if ($rssFeedData->getName() !== 'rss') {
	throw new \Exception('Given URL is no RSS feed');
}

if ($rssFeedData->channel->item->count() < count(RssFeedService::$templateVariableIds['items'])) {
	throw new \Exception(
		sprintf(
			'Not enough items on feed to generate a video: %d needed, %d found',
			count(RssFeedService::$templateVariableIds['items']),
			$rssFeedData->channel->item->count()
		)
	);
}

4.3 Map RSS feed data to template variable ids

Now we'll map the item data from the $rssFeedData. title and description are required fields in the RSS spec. Yet the template allows for a custom image and images are not required in RSS. When one is set it will be referred to in the enclosure of an item. If none is provided or it's not an image (audio and video files are supported inside the RSS spec) we'll pass a null value for that template variable. In this case, the automater will use the placeholder image provided when creating the video.

$templateVariables = [
	RssFeedService::$templateVariableIds['date'] => date('d-m-Y'),
	RssFeedService::$templateVariableIds['title'] => (string) $rssFeedData->channel->title
];

foreach (RssFeedService::$templateVariableIds['items'] as $i => $item) {
	$templateVariables[$item['title']] = (string) $rssFeedData->channel->item[$i]->title ?: null;
	$templateVariables[$item['body']] = strip_tags((string) $rssFeedData->channel->item[$i]->description) ?: null;

	$imageUrl = null;
	if (property_exists($rssFeedData->channel->item[$i], 'enclosure') && preg_match('/^image\/.+/', $rssFeedData->channel->item[$i]->enclosure->attributes()->type)
) {
		$imageUrl = (string) $rssFeedData->channel->item[$i]->enclosure->attributes()->url;
	}

	$templateVariables[$item['image']] = $imageUrl;
}

4.4 Post job to Moovly

Now all variables are set we can create a generator job using the Moovly API. Note the values field inside the body contains an array of arrays. For each array given here a new video will be generated, knowing this we could create multiple video's in 1 API call. We also would like to get a notification email to $email when the video has been created. We support other notifications than email as well, a list can be found in the docs.

$this->moovlyAPIClient->post(
	'/generator/v1/jobs',
	[
		'body' => json_encode([
          'template_id' => RssFeedService::$templateId,
          'options' => [
              "quality" => "1080p",
              "create_render" => true,
              "create_project" => false
          ],
          'values' => [
              [
                  'external_id' => 'nasa-news-' . date('Y-m-d'),
                  'template_variables' => $templateVariables,
              ]
          ],
          'notifications' => [
              [
                  'type' => 'email',
                  'payload' => [
                      'email' => $email
                  ]
              ]
          ]
		])
	]
);
<?php

use GuzzleHttp\Client;
use SimpleXMLElement;

class RssFeedService
{

    protected $moovlyAPIClient;

    static $templateId = '40f2b98a-1d2d-4ade-a227-d9d6c2e7f54d';

    static $templateVariableIds = [
        'date' => '650d872c-d0c3-43c7-a5a5-cc26dafa373f',
        'title' => '9853eeed-3968-4bea-a759-f71b69ee6d97',
        'items' => [
            [
                'title' => '951fb6dd-3d90-493d-96e7-42681527afc6',
                'body' => 'f6f9c789-2506-434b-aa5a-04670c0bcf58',
                'image' => '04dd6c70-0183-45d3-a6d2-59e931fe9453'
            ],
            [
                'title' => '6d43dc25-fc07-44b0-ac5e-0cf113a0d281',
                'body' => 'a3ea2228-2af9-47a6-875a-ac737fa77e6d',
                'image' => 'b17cb347-ccc9-42f3-a944-6392ce0aebfc'
            ],
            [
                'title' => '3b577eb8-bfea-4f77-ad0c-994b0457645a',
                'body' => 'dc442952-082d-4616-827b-1356ff310ca4',
                'image' => '3bf1ad52-a565-4b43-b306-f26222b93628'
            ]
        ]
    ];

    public function __construct()
    {

        $this->moovlyAPIClient = new Client(
            [
                'base_uri' => 'https://api.moovly.com',
                'headers' => [
                    'Authorization' => getenv('API_TOKEN'),
                    'Content-Type' => 'application/json'
                ]
            ]
        );
    }
    public function create($rssFeed, $email)
    {
        $rssFeedData = new SimpleXMLElement(file_get_contents($rssFeed));

        if ($rssFeedData->getName() !== 'rss') {
            throw new \Exception('Given URL is no RSS feed');
        }

        if ($rssFeedData->channel->item->count() < count(RssFeedService::$templateVariableIds['items'])) {
            throw new \Exception(
                sprintf(
                    'Not enough items on feed to generate a video: %d needed, %d found',
                    count(RssFeedService::$templateVariableIds['items']),
                    $rssFeedData->channel->item->count()
                )
            );
        }

        $templateVariables = [
            RssFeedService::$templateVariableIds['date'] => date('d-m-Y'),
            RssFeedService::$templateVariableIds['title'] => (string) $rssFeedData->channel->title
        ];

        foreach (RssFeedService::$templateVariableIds['items'] as $i => $item) {
            $templateVariables[$item['title']] = (string) $rssFeedData->channel->item[$i]->title ?: null;
            $templateVariables[$item['body']] = strip_tags((string) $rssFeedData->channel->item[$i]->description) ?: null;

            $imageUrl = null;
            if (property_exists($rssFeedData->channel->item[$i], 'enclosure')
                && preg_match('/^image\/.+/', $rssFeedData->channel->item[$i]->enclosure->attributes()->type)
            ) {
                $imageUrl = (string) $rssFeedData->channel->item[$i]->enclosure->attributes()->url;
            }

            $templateVariables[$item['image']] = $imageUrl;
        }

        $this->moovlyAPIClient->post(
            '/generator/v1/jobs',
            [
                'body' => json_encode([
                    'template_id' => RssFeedService::$templateId,
                    'options' => [
                        "quality" => "1080p",
                        "create_render" => true,
                        "create_project" => false
                    ],
                    'values' => [
                        [
                            'external_id' => 'nasa-news-' . date('Y-m-d'),
                            'template_variables' => $templateVariables,
                        ]
                    ],
                    'notifications' => [
                        [
                            'type' => 'email',
                            'payload' => [
                                'email' => $email
                            ]
                        ]
                    ]
                ])
            ]
        );
    }
}