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 @@ -8,6 +8,7 @@ Clone this repository, or open the directory of the example you want. Each examp

- [bulk-csv-videos](examples/bulk-csv-videos) renders one video per row of a CSV from a single template with merge fields, tracked in a resumable manifest, with an optional AI step where Claude writes each row's headline and image prompt. Companion code for [Generate videos in bulk with an API and an AI agent](https://shotstack.io/learn/bulk-create-videos-from-csv-and-ai/).
- [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/).
- [in-app-video-creation](examples/in-app-video-creation) lets a user create the same promo video three ways: an embedded Studio SDK editor, a quick form, and a one-click headless render, all through one render proxy that keeps the API key server-side. Companion code for [Add video creation to your app without building an editor](https://shotstack.io/learn/add-video-creation-to-your-app/).
- [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
16 changes: 16 additions & 0 deletions examples/in-app-video-creation/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# https://dashboard.shotstack.io/register
SHOTSTACK_API_KEY=

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STANDARDS — API keys: document every env var this example reads (SHOTSTACK_ENV, SHOTSTACK_TEMPLATE_ID, optional RATE_LIMIT / PORT) with an empty value and a comment+URL above each line. A missing key must fail with your one-line message, not a late API error.


# Optional. stage (sandbox, default) or v1 (production). Use the key for the same environment.
# Both keys are in the dashboard under API Keys: https://dashboard.shotstack.io/
SHOTSTACK_ENV=

# Optional. Reuse a template between restarts. The proxy creates one and prints its id when empty.
# https://shotstack.io/docs/api/#tag/Edit/operation/postTemplate
SHOTSTACK_TEMPLATE_ID=

# Optional. Renders each user can submit per hour. Default 10.
RATE_LIMIT=

# Optional. Port for the render proxy. Default 8787.
PORT=
2 changes: 2 additions & 0 deletions examples/in-app-video-creation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
81 changes: 81 additions & 0 deletions examples/in-app-video-creation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# In-app video creation

Three ways for a user to create the same promo video in your app: an embedded editor, a form, and one
button with no interface. All three submit to one render proxy. The proxy holds the API key, records
the owner of each render, checks user-supplied asset URLs, and rate limits each user.

Related guide: [Add video creation to your app without building an editor](https://shotstack.io/learn/add-video-creation-to-your-app/).

## Requirements

- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key
- Node.js 20 or later
- A browser with WebGL for the editor tab. Without WebGL, the other two tabs still work.

Sandbox renders are watermarked. Your account needs at least one credit to use the sandbox.

## Setup

```bash
git clone https://github.com/shotstack/shotstack-cookbook.git
cd shotstack-cookbook/examples/in-app-video-creation
npm install
```

Copy the environment file. Add your sandbox key to `.env`. Leave the other variables empty.

```bash
cp .env.example .env
```

## Run

The example runs as two processes. Open two terminals.

In terminal 1, load the environment file and start the render proxy. Only this process reads the
API key.

```bash
set -a
source .env
set +a
npm run server
```

In terminal 2, start the web app:

```bash
npm run dev
```

Open http://localhost:5173 in your browser.

## What happens

The render proxy starts on port 8787. The web app starts on port 5173. The app shows three tabs
above one shared gallery:

- **Editor** mounts the Studio SDK with the edit in `public/promo.json`. Change the video, then click
**Render this edit**. The app saves a draft on each change and restores it on reload. **Start over**
discards the draft.
- **Quick form** sends three form fields as merge values for the template in `template.json`. The
proxy creates the template on the first form render and prints the template id. Set
`SHOTSTACK_TEMPLATE_ID` in `.env` to reuse that template after a restart.
- **One click** builds the Edit JSON in code and submits it. No editor and no form.

Each tab shows its progress in a status line. A sandbox render finishes in under a minute. When a
render is done, the app adds the video to the gallery. The gallery shows only the renders of the
current user, because the proxy records the owner of each render before it responds. The proxy reads
the user id from an `x-demo-user` header as a stand-in for your auth. Replace it with your own session
check before real users use the app.

The proxy limits each user to 10 renders per hour. Set `RATE_LIMIT` to change the cap. The proxy
rejects an asset URL that is not HTTPS, that redirects, that resolves to a private address, or that
is larger than 100 MB.

The proxy renders in the sandbox by default. To render without the watermark, set `SHOTSTACK_ENV=v1`
and use your production key. A template belongs to the environment that created it. The proxy
creates a new template on the first form render in each environment.

The webhook receiver at `/hooks/render` does not fire on localhost, so the app polls instead. To test
the webhook, expose the proxy through a tunnel and set `callback` on a render. See the guide.
244 changes: 244 additions & 0 deletions examples/in-app-video-creation/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>In-app video creation: three paths</title>
<style>
:root {
color-scheme: dark;
}
body {
margin: 0;
font:
15px/1.5 system-ui,
-apple-system,
sans-serif;
background: #14161a;
color: #e8eaed;
}
header {
padding: 20px 24px;
border-bottom: 1px solid #2a2e35;
}
h1 {
margin: 0 0 4px;
font-size: 17px;
font-weight: 600;
}
.sub {
color: #9aa0a6;
font-size: 13px;
}
.tabs {
display: flex;
gap: 8px;
padding: 16px 24px 0;
}
.tab {
padding: 8px 16px;
border: 1px solid #2a2e35;
background: none;
color: #9aa0a6;
border-radius: 6px;
cursor: pointer;
font: inherit;
}
.tab[aria-selected='true'] {
background: #2a2e35;
color: #e8eaed;
}
.panel {
padding: 20px 24px;
}
.panel[hidden] {
display: none;
}
[data-shotstack-studio] {
width: 100%;
height: 400px;
background: #0d0f12;
border-radius: 8px;
}
[data-shotstack-timeline] {
width: 100%;
height: 170px;
overflow: hidden;
margin-top: 12px;
}
form {
max-width: 420px;
display: grid;
gap: 14px;
}
label {
display: grid;
gap: 5px;
font-size: 13px;
color: #9aa0a6;
}
input,
select {
padding: 9px 11px;
border: 1px solid #2a2e35;
border-radius: 6px;
background: #0d0f12;
color: #e8eaed;
font: inherit;
}
button.primary {
padding: 10px 18px;
border: 0;
border-radius: 6px;
background: #4c8bf5;
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
button.primary:disabled {
opacity: 0.5;
cursor: default;
}
button.secondary {
padding: 10px 18px;
border: 1px solid #2a2e35;
border-radius: 6px;
background: none;
color: #9aa0a6;
font: inherit;
cursor: pointer;
margin-left: 8px;
}
.hint {
font-size: 12px;
color: #6b7076;
}
.status {
margin-top: 14px;
font-size: 13px;
color: #9aa0a6;
min-height: 20px;
}
.gallery {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
}
.gallery video {
width: 260px;
border-radius: 6px;
background: #000;
}
p.lead {
max-width: 60ch;
color: #9aa0a6;
font-size: 13px;
margin-top: 0;
}
</style>
</head>

<body>
<header>
<h1>Make a promo video</h1>
<div class="sub">One template, three ways to drive it.</div>
</header>

<div class="tabs" role="tablist">
<button class="tab" role="tab" aria-selected="true" data-panel="editor">
Editor
</button>
<button class="tab" role="tab" aria-selected="false" data-panel="form">
Quick form
</button>
<button
class="tab"
role="tab"
aria-selected="false"
data-panel="headless"
>
One click
</button>
</div>

<!-- Option 1: embedded white-label editor -->
<section class="panel" id="panel-editor">
<div data-shotstack-studio></div>
<div data-shotstack-timeline></div>
<p class="status" id="editor-status"></p>
<button class="primary" id="render-from-editor">Render this edit</button>
<button class="secondary" id="reset-draft">Start over</button>
</section>

<!-- Option 3: form-to-video, no editor -->
<section class="panel" id="panel-form" hidden>
<form id="quick-form">
<label>
Main message
<input
name="headline"
maxlength="42"
required
value="Twelve new listings this week."
/>
<span class="hint">
Keep it short. Longer messages wrap to a second line.
</span>
</label>
<label>
Style
<select name="font">
<option value="Montserrat">Montserrat (clean)</option>
<option value="Open Sans">Open Sans (friendly)</option>
<option value="Permanent Marker">Permanent Marker (loud)</option>
</select>
<span class="hint">Built-in fonts. Nothing to host.</span>
</label>
<label>
Background footage
<select name="footage">
<option
value="https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/city-timelapse.mp4"
>
City
</option>
<option
value="https://shotstack-assets.s3.amazonaws.com/footage/beach-overhead.mp4"
>
Beach
</option>
<option
value="https://s3-ap-southeast-2.amazonaws.com/shotstack-assets/footage/skater.hd.mp4"
>
Skate
</option>
</select>
</label>
<button class="primary" type="submit">Generate video</button>
</form>
<p class="status" id="form-status"></p>
</section>

<!-- Option 2: headless, no editor and no form -->
<section class="panel" id="panel-headless" hidden>
<p class="lead">
No editor and no form. The app already knows enough to build the video,
so it assembles the Edit JSON itself and renders it. This is the shape
you want when video is an output of your product rather than its
purpose.
</p>
<button class="primary" id="render-headless">
Generate this week's listing video
</button>
<p class="status" id="headless-status"></p>
</section>

<section class="panel">
<div class="gallery" id="gallery"></div>
</section>

<script type="module" src="/main.js"></script>
</body>
</html>
Loading
Loading