Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Clone this repo, or head into the folder for the example you want. Each example

## Examples

- [first-render](examples/first-render) the very basics: submit an Edit, poll the render status, and print the output URL, in Node.js and Python. Start here if you are new to the API. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/).
- [instagram-ai-video](examples/instagram-ai-video) generates a script, voiceover and background image with AI, renders a 1080x1920 video, and publishes it as an Instagram Reel. Companion code for [How to automate Instagram posts with AI video](https://shotstack.io/learn/automate-instagram-posts-with-ai-video/).
- [rapidreels](examples/rapidreels) creates faceless short-form videos using generative AI. [View demo](https://shotstack.io/demos/social-media-video-maker/).
- [reelestate](examples/reelestate) turns static real estate images into fully edited video slideshows. [View demo](https://shotstack.io/demos/real-estate-video-listing-maker/).
Expand Down
1 change: 1 addition & 0 deletions examples/first-render/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHOTSTACK_API_KEY=your_sandbox_api_key
2 changes: 2 additions & 0 deletions examples/first-render/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
51 changes: 51 additions & 0 deletions examples/first-render/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# First render

The very basics of the Shotstack API, and the loop every other example in this cookbook builds on:
submit an Edit, poll the render status, and get the output URL. The same flow is implemented twice,
in Node.js and in Python. Start here if you have never rendered a video with Shotstack before.

Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/).

## Requirements

- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key
(dashboard menu under your account name, top right, under **API Keys**)
- Node.js 20 or later, or Python 3 with [requests](https://pypi.org/project/requests/)

Sandbox renders are watermarked and don't consume credits, but your account needs at least one
credit to use the environment.

## Setup

```bash
git clone https://github.com/shotstack/shotstack-cookbook.git
cd shotstack-cookbook/examples/first-render
```

Set your sandbox key:

```bash
export SHOTSTACK_API_KEY="your_sandbox_api_key"
```

## Run

Node.js:

```bash
node render.mjs
```

Python:

```bash
python3 -m pip install requests
python3 render.py
```

## What happens

Both scripts read `edit.json` (a five-second "Hello World" rich-text video), submit it to the
sandbox render endpoint, poll every five seconds until the render reaches `done` or `failed`, and
print the temporary output URL. A sandbox render finishes in under a minute. The URL expires after
24 hours; see the guide for retrieving the CDN-hosted copy through the Serve API.
31 changes: 31 additions & 0 deletions examples/first-render/edit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"timeline": {
"background": "#101827",
"tracks": [
{
"clips": [
{
"asset": {
"type": "rich-text",
"text": "Hello World",
"font": {
"size": 64,
"color": "#ffffff"
},
"align": {
"horizontal": "center",
"vertical": "middle"
}
},
"start": 0,
"length": 5
}
]
}
]
},
"output": {
"format": "mp4",
"resolution": "preview"
}
}
108 changes: 108 additions & 0 deletions examples/first-render/render.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { readFile } from 'node:fs/promises';
import { setTimeout as delay } from 'node:timers/promises';

const API_BASE_URL = 'https://api.shotstack.io/edit/stage';
const POLL_INTERVAL_MS = 5_000;
const MAX_WAIT_MS = 10 * 60 * 1_000;
const apiKey = process.env.SHOTSTACK_API_KEY;

if (!apiKey) {
console.error('Set the SHOTSTACK_API_KEY environment variable first.');
process.exit(1);
}

async function shotstackRequest(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
signal: AbortSignal.timeout(30_000),
headers: {
Accept: 'application/json',
'x-api-key': apiKey,
...options.headers
}
});

const responseText = await response.text();
let body = null;

try {
body = JSON.parse(responseText);
} catch {
// The error below includes the raw body when Shotstack does not return JSON.
}

if (!response.ok) {
const details = body ? JSON.stringify(body) : responseText;
throw new Error(
`Shotstack returned ${response.status} ${response.statusText}: ${details}`
);
}

if (!body) {
throw new Error('Shotstack returned an unexpected non-JSON response.');
}

return body;
}

async function submitRender(edit) {
const result = await shotstackRequest('/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(edit)
});

const renderId = result?.response?.id;

if (!renderId) {
throw new Error(
`The response did not contain a render ID: ${JSON.stringify(result)}`
);
}

return renderId;
}

async function waitForRender(renderId) {
const startedAt = Date.now();

while (Date.now() - startedAt < MAX_WAIT_MS) {
const result = await shotstackRequest(`/render/${renderId}`);
const render = result?.response;

if (!render?.status) {
throw new Error(`Unexpected status response: ${JSON.stringify(result)}`);
}

console.log(`Render status: ${render.status}`);

if (render.status === 'done') {
return render;
}

if (render.status === 'failed') {
throw new Error(
render.error || 'The render failed without an error message.'
);
}

await delay(POLL_INTERVAL_MS);
}

throw new Error(`Render ${renderId} did not finish within 10 minutes.`);
}

try {
const edit = JSON.parse(
await readFile(new URL('./edit.json', import.meta.url), 'utf8')
);
const renderId = await submitRender(edit);

console.log(`Queued render: ${renderId}`);

const render = await waitForRender(renderId);
console.log(`Temporary output URL: ${render.url}`);
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
108 changes: 108 additions & 0 deletions examples/first-render/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import json
import os
import sys
import time
from pathlib import Path

import requests

API_BASE_URL = "https://api.shotstack.io/edit/stage"
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 10 * 60


def shotstack_request(method, path, api_key, **kwargs):
response = requests.request(
method,
f"{API_BASE_URL}{path}",
headers={
"Accept": "application/json",
"x-api-key": api_key,
},
timeout=30,
**kwargs,
)

try:
body = response.json()
except requests.exceptions.JSONDecodeError as error:
raise RuntimeError(
"Shotstack returned a non-JSON response "
f"with status {response.status_code}."
) from error

if not response.ok:
raise RuntimeError(
f"Shotstack returned {response.status_code}: {json.dumps(body)}"
)

return body


def submit_render(edit, api_key):
result = shotstack_request("POST", "/render", api_key, json=edit)
render_id = result.get("response", {}).get("id")

if not render_id:
raise RuntimeError(
f"The response did not contain a render ID: {json.dumps(result)}"
)

return render_id


def wait_for_render(render_id, api_key):
started_at = time.monotonic()

while time.monotonic() - started_at < MAX_WAIT_SECONDS:
result = shotstack_request("GET", f"/render/{render_id}", api_key)
render = result.get("response", {})
status = render.get("status")

if not status:
raise RuntimeError(f"Unexpected status response: {json.dumps(result)}")

print(f"Render status: {status}")

if status == "done":
return render

if status == "failed":
raise RuntimeError(
render.get("error") or "The render failed without an error message."
)

time.sleep(POLL_INTERVAL_SECONDS)

raise TimeoutError(f"Render {render_id} did not finish within 10 minutes.")


def main():
api_key = os.environ.get("SHOTSTACK_API_KEY")

if not api_key:
raise RuntimeError("Set the SHOTSTACK_API_KEY environment variable first.")

edit_path = Path(__file__).with_name("edit.json")
edit = json.loads(edit_path.read_text(encoding="utf-8"))
render_id = submit_render(edit, api_key)

print(f"Queued render: {render_id}")

render = wait_for_render(render_id, api_key)
print(f"Temporary output URL: {render['url']}")


if __name__ == "__main__":
try:
main()
except (
OSError,
KeyError,
ValueError,
RuntimeError,
TimeoutError,
requests.RequestException,
) as error:
print(error, file=sys.stderr)
raise SystemExit(1) from error
Loading