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
2 changes: 2 additions & 0 deletions examples/in-app-video-creation/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# https://dashboard.shotstack.io/register
SHOTSTACK_API_KEY=
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
83 changes: 83 additions & 0 deletions examples/in-app-video-creation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# In-app video creation

One promo video, three ways for a user to create it in your app: an embedded Studio SDK editor, a
quick form, and a single button with no interface at all. All three paths submit to the same render
proxy. The proxy holds the API key, records which user owns each render, validates user-supplied
asset URLs, and rate limits each user.

Companion code for [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
(dashboard menu under your account name, top right, under **API Keys**)
- Node.js 20 or later
- A browser with WebGL for the editor tab. If WebGL is not available, the app shows a message and
the other two tabs still work.

Sandbox renders are watermarked, and 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/in-app-video-creation
npm install
```

Copy the environment file. Add your sandbox key to `.env`.

```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. Start it first:

```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 and the web app 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 every 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` to reuse a template between restarts.
- **One click** builds the Edit JSON in code and submits it. No editor and no form.

Each path 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 only shows renders that belong
to the current user, because the proxy records the owner of every render before it responds.

The proxy limits each user to 10 renders per hour. Set `RATE_LIMIT` to change the cap. The proxy
rejects user-supplied asset URLs that are not HTTPS, resolve to a private address, or are 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. Templates belong to the environment they were
created in, so the proxy creates a new one 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
for details.
243 changes: 243 additions & 0 deletions examples/in-app-video-creation/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
<!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;
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