Are you looking to a make video from images? Do you want to create a video from images using an API? And, to bring your video to life do you also want to add music, motion effects and transitions?
In this post, I'll show you how you can effortlessly transform your images into stunning videos, using motion effects like the Ken Burns effect, smooth fade transitions and add a soundtrack.
While you can create videos from images using traditional desktop video editing software, this article will focus on converting images to video using an API. Using a video editing API lets you automate the video editing process and create videos at scale.
Some common use cases include:
Shotstack is an API first, video automation platform that enables you to create, edit, build, and share dynamic videos at scale. For this post, we will be leveraging Shotstack's Edit API to seamlessly turn images into immersive videos with music and motion effects.
Before you begin, sign up to the Shotstack website to get a free to use API key. You'll need this key to make authorized requests to the Edit API. You should also be familiar with running commands from the command line of your operating system as we will be running commands using cURL as well as a Node.js script.
The simplest way to create a video from images using the Shotstack Edit API us to use cURL and a JSON file.
First, create a new file named edit.json, copy and paste the following JSON and save the file:
{
"timeline": {
"soundtrack": {
"src": "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/music/freepd/advertising.mp3",
"effect": "fadeInFadeOut"
},
"tracks": [
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/189349/pexels-photo-189349.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 0,
"length": 5,
"effect": "zoomIn",
"transition": {
"in": "fade",
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/1680140/pexels-photo-1680140.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 4,
"length": 5,
"effect": "slideUp",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/847393/pexels-photo-847393.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 8,
"length": 5,
"effect": "slideLeft",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/1295138/pexels-photo-1295138.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 12,
"length": 5,
"effect": "zoomOut",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/1452701/pexels-photo-1452701.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 16,
"length": 5,
"effect": "slideDown",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/756856/pexels-photo-756856.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 20,
"length": 5,
"effect": "slideRight",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "https://images.pexels.com/photos/1533720/pexels-photo-1533720.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
"start": 24,
"length": 5,
"effect": "zoomIn",
"transition": {
"out": "fade"
}
}
]
}
]
},
"output": {
"format": "mp4",
"resolution": "sd"
}
}
The JSON schema in this file adds the following properties to the video:
Next, from your shell or command line, run the following command, make sure you use your own stage/sandbox API key as the value for the x-api-key
header parameter, instead of YOUR_API_KEY
:
curl -X POST \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d @edit.json \
https://api.shotstack.io/edit/stage/render
This command sends a POST request to the Shotstack Edit API with the JSON payload in the edit.json file. You should receive a response like this:
{
"success": true,
"message": "Created",
"response": {
"message": "Render Successfully Queued",
"id": "8c9686be-62c1-486b-bd70-13f1f20d936f"
}
}
Copy the id from the response, we'll be using this to check the render status in the next step.
The video will take a few seconds to render so wait for a few moments and then run the following command, make sure to replace the id in the URL with the one received in the previous API call response:
curl -X GET \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
https://api.shotstack.io/edit/stage/render/8c9686be-62c1-486b-bd70-13f1f20d936f
Check the response for a status parameter to see if the video has finished rendering; if it has, it should say done
. If it's still in progress, you might see statuses like queued
, rendering
, saving
, or fetching
. In that case, simply wait for a few seconds and then send the same request again.
Once you receive the done
status in the response, visit the link at the url
parameter in your web browser. It should look like this:
Another way to create a video is using the the Studio Editor within the Shotstack dashboard. The Studio Editor is an online video editor that lets you design a video using a visual timeline based video editor with a preview and drag and drop interface.
Within the template you can add merge fields and placeholders that can be replaced with your own images when you render the video using the API.
You can design the template from scratch but it is also possible to import JSON in to the editor to instantly create a template.
Follow the steps below to create the template:
{
"timeline": {
"soundtrack": {
"src": "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/music/freepd/advertising.mp3",
"effect": "fadeInFadeOut"
},
"tracks": [
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_1 }}"
},
"start": 0,
"length": 5,
"effect": "zoomIn",
"transition": {
"in": "fade",
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_2 }}"
},
"start": 4,
"length": 5,
"effect": "slideUp",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_3 }}"
},
"start": 8,
"length": 5,
"effect": "slideLeft",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_4 }}"
},
"start": 12,
"length": 5,
"effect": "zoomOut",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_5 }}"
},
"start": 16,
"length": 5,
"effect": "slideDown",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_6 }}"
},
"start": 20,
"length": 5,
"effect": "slideRight",
"transition": {
"out": "fade"
}
}
]
},
{
"clips": [
{
"asset": {
"type": "image",
"src": "{{ IMAGE_7 }}"
},
"start": 24,
"length": 5,
"effect": "zoomIn",
"transition": {
"out": "fade"
},
"scale": 1,
"offset": {
"x": 0,
"y": 0
},
"position": "center"
}
]
}
]
},
"output": {
"format": "mp4",
"size": {
"width": 1024,
"height": 576
}
},
"merge": [
{
"find": "IMAGE_1",
"replace": "https://images.pexels.com/photos/189349/pexels-photo-189349.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_2",
"replace": "https://images.pexels.com/photos/1680140/pexels-photo-1680140.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_3",
"replace": "https://images.pexels.com/photos/847393/pexels-photo-847393.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_4",
"replace": "https://images.pexels.com/photos/1295138/pexels-photo-1295138.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_5",
"replace": "https://images.pexels.com/photos/1452701/pexels-photo-1452701.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_6",
"replace": "https://images.pexels.com/photos/756856/pexels-photo-756856.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_7",
"replace": "https://images.pexels.com/photos/1533720/pexels-photo-1533720.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
}
]
}
The command you copied from the previous step will look similar to below, paste the command to your terminal and run it to send the request to the Edit API templates/render endpoint:
curl --request POST 'https://api.shotstack.io/edit/stage/templates/render' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data-raw '{
"id": "3788ba7b-bea2-451b-aa70-b751b13882bf",
"merge": [
{
"find": "IMAGE_1",
"replace": "https://images.pexels.com/photos/189349/pexels-photo-189349.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_2",
"replace": "https://images.pexels.com/photos/1680140/pexels-photo-1680140.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_3",
"replace": "https://images.pexels.com/photos/847393/pexels-photo-847393.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_4",
"replace": "https://images.pexels.com/photos/1295138/pexels-photo-1295138.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_5",
"replace": "https://images.pexels.com/photos/1452701/pexels-photo-1452701.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_6",
"replace": "https://images.pexels.com/photos/756856/pexels-photo-756856.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_7",
"replace": "https://images.pexels.com/photos/1533720/pexels-photo-1533720.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
}
]
}'
The JSON we pasted in to the video editor is very similar to the original JSON, except instead of adding the URL's to the clips, we use placeholders that look like this: {{ IMAGE_1 }}
. The merge fields array contains a list of objects that contain the placeholder to find and the URL to the image that will replace it.
The cURL command uses the template ID created when the template was first saved, and the merge fields array to find the placeholder values and replace them with the images specified.
When you run the command you will receive a response just like in the previous example:
{
"success": true,
"message": "Created",
"response": {
"message": "Render Successfully Queued",
"id": "a8bf787a-f6f0-4d6c-9b79-88b931942876"
}
}
As before, copy the ID and check the status of the video render using the following command, make sure to replace the id in the URL with the correct render id:
curl -X GET \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
https://api.shotstack.io/edit/stage/render/a8bf787a-f6f0-4d6c-9b79-88b931942876
When the status is done
you can visit the URL in the url
parameter to view the video. It will of course look the same as before, but this time we used a template to create the video.
You can modify the template to use different images by changing the URL's in the merge fields array. To demonstrate, make the same API call but use different image URLs in the merge fields array:
curl --request POST 'https://api.shotstack.io/edit/stage/templates/render' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data-raw '{
"id": "3788ba7b-bea2-451b-aa70-b751b13882bf",
"merge": [
{
"find": "IMAGE_1",
"replace": "https://images.pexels.com/photos/618833/pexels-photo-618833.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_2",
"replace": "https://images.pexels.com/photos/808465/pexels-photo-808465.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_3",
"replace": "https://images.pexels.com/photos/789380/pexels-photo-789380.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_4",
"replace": "https://images.pexels.com/photos/691034/pexels-photo-691034.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_5",
"replace": "https://images.pexels.com/photos/2335126/pexels-photo-2335126.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_6",
"replace": "https://images.pexels.com/photos/1062057/pexels-photo-1062057.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
},
{
"find": "IMAGE_7",
"replace": "https://images.pexels.com/photos/1004682/pexels-photo-1004682.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=720&w=1280"
}
]
}'
We have now rendered a completely different video using the same template, just by changing the images:
If you want to have more programmatic control over video creation, you can use one of our SDKs to interact with the Edit API. We have SDKs for Node.js, PHP, Python, and Ruby. For the purpose of this guide, we'll use the Node.js SDK.
First create a folder where you will add the script and install the Shotstack Node SDK. Then, inside the folder run the following command to install the SDK:
npm install shotstack-sdk
Next, create a new file named edit.js and open it in your favourite code editor and copy the following code to it:
const Shotstack = require('shotstack-sdk');
const defaultClient = Shotstack.ApiClient.instance;
const DeveloperKey = defaultClient.authentications['DeveloperKey'];
const api = new Shotstack.EditApi();
defaultClient.basePath = 'https://api.shotstack.io/edit/stage';
DeveloperKey.apiKey = 'YOUR_API_KEY';
const images = [
'https://images.pexels.com/photos/326259/pexels-photo-326259.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/248747/pexels-photo-248747.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/3311574/pexels-photo-3311574.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/593172/pexels-photo-593172.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/315938/pexels-photo-315938.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/1592384/pexels-photo-1592384.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/3399938/pexels-photo-3399938.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/5214413/pexels-photo-5214413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
];
const clips = [];
const length = 1.5;
let start = 0;
const soundtrack = new Shotstack.Soundtrack;
soundtrack
.setSrc('https://templates.shotstack.io/basic/asset/audio/music/unminus/runs.mp3')
.setEffect('fadeOut');
images.forEach((image) => {
const imageAsset = new Shotstack.ImageAsset;
imageAsset
.setSrc(image);
const clip = new Shotstack.Clip;
clip
.setAsset(imageAsset)
.setStart(start)
.setLength(length)
.setEffect('zoomIn');
start = start + length;
clips.push(clip);
});
const track = new Shotstack.Track;
track
.setClips(clips);
const timeline = new Shotstack.Timeline;
timeline
.setSoundtrack(soundtrack)
.setTracks([track]);
const output = new Shotstack.Output;
output
.setFormat('mp4')
.setResolution('sd');
const edit = new Shotstack.Edit;
edit
.setTimeline(timeline)
.setOutput(output);
api.postRender(edit).then((data) => {
const { id, message } = data.response;
console.log(message);
console.log('>> The render id: ' + id);
}, (error) => {
console.error('Request failed: ', error);
});
The code above uses the Shotstack SDK to create a video montage from an array of images. The array of images is prepared at the top of the script and we then loop through it and use the SDK to create the clips, start time and length, and add a motion effect. The rest of the script uses the SDK to prepare the edit and send it to the Shotstack API for rendering.
Make sure you use your own API key for the sandbox/staging environment as the value for the DeveloperKey.apiKey variable instead of YOUR_API_KEY
.
Finally, run the script using the following command:
node edit.js
Expect an output like this:
Render Successfully Queued
>> The render id: 67609896-e1a2-4999-894d-af238f167c11
Copy the id, as we'll be using it to fetch the video in the next step.
Just like we checked the status of the video render using cURL, we can also do this using the SDK.
In the same folder create a file called status.js and copy the following code to it:
const Shotstack = require('shotstack-sdk');
const defaultClient = Shotstack.ApiClient.instance;
const DeveloperKey = defaultClient.authentications['DeveloperKey'];
const api = new Shotstack.EditApi();
defaultClient.basePath = 'https://api.shotstack.io/edit/stage';
DeveloperKey.apiKey = 'YOUR_API_KEY';
const id = process.argv[2];
api.getRender(id).then((data) => {
const { status, url } = data.response;
console.log('Status: ' + status);
if (status == 'done') {
console.log('>> Asset URL: ' + url);
}
}, (error) => {
console.error('Request failed or not found: ', error);
});
Remember to replace the YOUR_API_KEY
placeholder with your own API key once again.
Next run the script using the following command, make sure to replace the id in the command with the one you copied from the previous step:
node status.js 67609896-e1a2-4999-894d-af238f167c11
Once the video has finished rendering and the status is done
you should see an output like below:
Status: done
>> Asset URL: https://shotstack-api-stage-output.s3-ap-southeast-2.amazonaws.com/hkwxdnk0f1/67609896-e1a2-4999-894d-af238f167c11.mp4
Open the Asset URL in your browser to play the video. Here is how the video looks, this time we used a different soundtrack and automotive themed photos:
Because the video is generated programmatically from an array of images you can now start having fun creating new videos. You could populate the array from a database or 3rd party API, like the Pexels API using their search endpoint.
Here is an example using some very simple array functions to randomise and splice the array of images, just add these lines after the image array is declared:
// Shuffle the order of the array
images.sort(() => Math.random() - 0.5);
// Remove up to 4 images from the array
images.splice(0, Math.floor(Math.random() * 4) + 1);
Now the video will be different every time you run the script. Give it a try and see what other ways you can come up with the generate slideshow style videos from images.
curl --request POST 'https://api.shotstack.io/v1/render' \
--header 'x-api-key: YOUR_API_KEY' \
--data-raw '{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {
"type": "video",
"src": "https://shotstack-assets.s3.amazonaws.com/footage/beach-overhead.mp4"
},
"start": 0,
"length": "auto"
}
]
}
]
},
"output": {
"format": "mp4",
"size": {
"width": 1280,
"height": 720
}
}
}'