From 333c0d67132f0ebd2161604fd9851e4b613c4dde Mon Sep 17 00:00:00 2001 From: Sonia Srivastava Date: Thu, 24 Sep 2026 14:04:24 +0700 Subject: [PATCH 1/2] Add automotive-inventory-videos example --- README.md | 1 + .../automotive-inventory-videos/.env.example | 6 + .../automotive-inventory-videos/.gitignore | 2 + .../automotive-inventory-videos/README.md | 72 ++++++++++++ examples/automotive-inventory-videos/api.mjs | 88 ++++++++++++++ examples/automotive-inventory-videos/edit.mjs | 109 ++++++++++++++++++ .../automotive-inventory-videos/render.mjs | 87 ++++++++++++++ .../automotive-inventory-videos/renders.mjs | 40 +++++++ .../automotive-inventory-videos/status.mjs | 43 +++++++ .../automotive-inventory-videos/vehicles.json | 56 +++++++++ 10 files changed, 504 insertions(+) create mode 100644 examples/automotive-inventory-videos/.env.example create mode 100644 examples/automotive-inventory-videos/.gitignore create mode 100644 examples/automotive-inventory-videos/README.md create mode 100644 examples/automotive-inventory-videos/api.mjs create mode 100644 examples/automotive-inventory-videos/edit.mjs create mode 100644 examples/automotive-inventory-videos/render.mjs create mode 100644 examples/automotive-inventory-videos/renders.mjs create mode 100644 examples/automotive-inventory-videos/status.mjs create mode 100644 examples/automotive-inventory-videos/vehicles.json diff --git a/README.md b/README.md index 2485268..766920d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Clone this repository, or open the directory of the example you want. Each examp ## Examples +- [automotive-inventory-videos](examples/automotive-inventory-videos) renders one vertical video per vehicle in a dealer inventory from a JSON feed: the photos in sequence with a pan or zoom on each, and an animated specification card. The Edit JSON is built in code, one clip per photo. - [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/). diff --git a/examples/automotive-inventory-videos/.env.example b/examples/automotive-inventory-videos/.env.example new file mode 100644 index 0000000..77bd08f --- /dev/null +++ b/examples/automotive-inventory-videos/.env.example @@ -0,0 +1,6 @@ +# Your Shotstack sandbox API key: https://dashboard.shotstack.io/register +SHOTSTACK_API_KEY= + +# Optional. stage (sandbox, default) or v1 (production). Use the key from the same environment. +# Both keys are in the dashboard under API Keys: https://dashboard.shotstack.io/ +SHOTSTACK_ENV= diff --git a/examples/automotive-inventory-videos/.gitignore b/examples/automotive-inventory-videos/.gitignore new file mode 100644 index 0000000..09c29c2 --- /dev/null +++ b/examples/automotive-inventory-videos/.gitignore @@ -0,0 +1,2 @@ +.env +renders.jsonl diff --git a/examples/automotive-inventory-videos/README.md b/examples/automotive-inventory-videos/README.md new file mode 100644 index 0000000..4996f02 --- /dev/null +++ b/examples/automotive-inventory-videos/README.md @@ -0,0 +1,72 @@ +# Automotive inventory videos + +Render one vertical video for each vehicle in a dealer inventory. Each vehicle record has a stock +id, specifications, a price and a list of photos. The script builds the Edit JSON in code, with +one clip for each photo, so the video is as long as the vehicle has photos. You get one +1080 x 1920 MP4 per vehicle, and a file that records which render belongs to which stock id. + +## Requirements + +- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key +- Node.js 20 or later + +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/automotive-inventory-videos +``` + +Copy the environment file. Add your sandbox key to `.env`. + +```bash +cp .env.example .env +``` + +Load the file into your shell. Do this in each new terminal: + +```bash +set -a +source .env +set +a +``` + +## Run + +Submit one render per vehicle: + +```bash +node render.mjs +``` + +Then check them: + +```bash +node status.mjs +``` + +Run `status.mjs` again until each render shows `done`. + +## What happens + +`render.mjs` reads `vehicles.json` and checks that each vehicle has every field and at least one +photo. It reports every problem it finds, then stops. For each vehicle it builds an Edit JSON and +submits one render. The video shows the photos in sequence, 3.2 seconds each, cropped to fill the +vertical frame. Each photo fades in, and the pan or zoom changes from one photo to the next. A +vehicle with one or two photos gets at least six seconds. A card with the year, make, model, trim, +price, odometer, transmission and fuel type slides in at the bottom. The dealer name is at the +top. A music track plays under the whole video. The script appends one line per render to +`renders.jsonl` and ends with the count of submitted renders. + +`status.mjs` reads `renders.jsonl` and checks each render once. A `done` render prints its video +URL. + +The card and the dealer name are `html5` assets. The script writes the vehicle data into their +HTML before the render. To render your own inventory, replace the records in `vehicles.json`. +Photo URLs must be public HTTPS URLs. To change the layout, edit `edit.mjs`. + +`renders.jsonl` only appends. Delete the file to start a new batch. + +To render in production, set `SHOTSTACK_ENV=v1` and put your production key in `.env`. diff --git a/examples/automotive-inventory-videos/api.mjs b/examples/automotive-inventory-videos/api.mjs new file mode 100644 index 0000000..45a44bb --- /dev/null +++ b/examples/automotive-inventory-videos/api.mjs @@ -0,0 +1,88 @@ +const API_KEY = process.env.SHOTSTACK_API_KEY; +const ENV = process.env.SHOTSTACK_ENV || 'stage'; +const API = `https://api.shotstack.io/edit/${ENV}`; +const TIMEOUT_MS = 30_000; + +export function fail(message) { + console.error(message); + process.exit(1); +} + +export function requireConfig() { + if (!API_KEY) { + fail('Set SHOTSTACK_API_KEY before you run this script.'); + } + + if (!['stage', 'v1'].includes(ENV)) { + fail('SHOTSTACK_ENV must be stage or v1.'); + } +} + +// A rejected key or a dead network fails every record the same way. The +// caller stops the batch at the first one instead of printing a line per record. +function stopBatch(message) { + return Object.assign(new Error(message), { fatal: true }); +} + +async function apiError(response) { + const text = await response.text(); + + try { + const body = JSON.parse(text); + return ( + body.errors?.[0]?.detail ?? body.response?.error ?? body.message ?? text + ); + } catch { + return text; + } +} + +async function request(path, options, label) { + let response; + + try { + response = await fetch(`${API}${path}`, { + ...options, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'x-api-key': API_KEY + }, + signal: AbortSignal.timeout(TIMEOUT_MS) + }); + } catch { + throw stopBatch( + `${label}: the network request failed. Check your connection and run again.` + ); + } + + if (response.status === 401 || response.status === 403) { + throw stopBatch( + `${label}: the API rejected the key (${response.status}). Check SHOTSTACK_API_KEY and SHOTSTACK_ENV.` + ); + } + + if (!response.ok) { + throw new Error(`${label}: ${response.status} ${await apiError(response)}`); + } + + return (await response.json()).response; +} + +export async function submitRender(edit, label) { + const { id } = await request( + '/render', + { method: 'POST', body: JSON.stringify(edit) }, + label + ); + + if (!id) { + throw new Error(`${label}: the render response did not contain an id.`); + } + + return id; +} + +export function getRender(renderId, label) { + return request(`/render/${renderId}`, { method: 'GET' }, label); +} diff --git a/examples/automotive-inventory-videos/edit.mjs b/examples/automotive-inventory-videos/edit.mjs new file mode 100644 index 0000000..87389af --- /dev/null +++ b/examples/automotive-inventory-videos/edit.mjs @@ -0,0 +1,109 @@ +const MUSIC_URL = + 'https://shotstack-assets.s3.amazonaws.com/music/unminus/ambition.mp3'; +const SECONDS_PER_PHOTO = 3.2; +const MIN_LENGTH = 6; +const EFFECTS = ['zoomIn', 'slideUp', 'zoomOut', 'slideDown']; + +const escapeHtml = value => + String(value).replace( + /[&<>"']/g, + character => + ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + })[character] + ); + +// The specification card is an html5 asset. The vehicle data is written into +// the markup here, so the card can change shape per dealer without a template. +function specCard(vehicle, length) { + const title = escapeHtml(`${vehicle.year} ${vehicle.make} ${vehicle.model}`); + const chips = [vehicle.odometer, vehicle.transmission, vehicle.fuel] + .map( + (text, index) => + `${escapeHtml(text)}` + ) + .join(''); + + return { + asset: { + type: 'html5', + html: `
${title}
${escapeHtml(vehicle.trim)}
${escapeHtml(vehicle.price)}
${chips}
`, + css: 'html,body{margin:0;padding:0;width:960px;height:420px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.card{box-sizing:border-box;width:960px;height:420px;padding:40px 48px;background:rgba(10,15,30,0.86);border-radius:32px;color:#fff;opacity:0;transform:translateY(24px)}.title{font-size:60px;font-weight:800;letter-spacing:-1px;line-height:1.05}.trim{margin-top:10px;font-size:34px;color:#cbd5e1}.price{margin-top:22px;font-size:72px;font-weight:800;color:#facc15;line-height:1}.row{display:flex;gap:14px;margin-top:26px}.chip{font-size:28px;padding:10px 20px;border-radius:999px;background:rgba(255,255,255,0.14);color:#e2e8f0;white-space:nowrap;opacity:0;transform:translateY(12px)}', + js: "const tl=gsap.timeline();tl.to('#card',{opacity:1,y:0,duration:0.6,ease:'power3.out'},0).to(['#chip0','#chip1','#chip2'],{opacity:1,y:0,duration:0.6,ease:'power3.out',stagger:0.13},0.3);" + }, + start: 0.5, + length: length - 0.5, + width: 960, + height: 420, + position: 'bottom', + offset: { y: 0.05 } + }; +} + +function dealerBadge(vehicle, length) { + return { + asset: { + type: 'html5', + html: `
${escapeHtml(vehicle.dealer)}
`, + css: 'html,body{margin:0;padding:0;width:960px;height:110px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.badge{box-sizing:border-box;display:inline-block;height:110px;line-height:110px;padding:0 44px;border-radius:55px;background:rgba(10,15,30,0.7);color:#fff;font-size:42px;font-weight:700}' + }, + start: 0, + length, + width: 960, + height: 110, + position: 'top', + offset: { y: -0.04 } + }; +} + +// One clip per photo on a single track. "auto" starts each clip when the +// previous one ends, so the video is as long as the vehicle has photos. +function photoClips(photos, secondsPerPhoto) { + return photos.map((src, index) => ({ + asset: { type: 'image', src }, + start: index === 0 ? 0 : 'auto', + length: secondsPerPhoto, + fit: 'crop', + effect: EFFECTS[index % EFFECTS.length], + transition: { in: 'fade' } + })); +} + +export function buildEdit(vehicle) { + // A vehicle with one photo still gets a video long enough to read the card. + const secondsPerPhoto = Math.max( + SECONDS_PER_PHOTO, + MIN_LENGTH / vehicle.photos.length + ); + const length = secondsPerPhoto * vehicle.photos.length; + + return { + timeline: { + background: '#000000', + tracks: [ + { clips: [specCard(vehicle, length)] }, + { clips: [dealerBadge(vehicle, length)] }, + { clips: photoClips(vehicle.photos, secondsPerPhoto) }, + { + clips: [ + { + asset: { + type: 'audio', + src: MUSIC_URL, + volume: 0.3, + effect: 'fadeOut' + }, + start: 0, + length: 'end' + } + ] + } + ] + }, + output: { format: 'mp4', size: { width: 1080, height: 1920 } } + }; +} diff --git a/examples/automotive-inventory-videos/render.mjs b/examples/automotive-inventory-videos/render.mjs new file mode 100644 index 0000000..fe513d5 --- /dev/null +++ b/examples/automotive-inventory-videos/render.mjs @@ -0,0 +1,87 @@ +import { readFile } from 'node:fs/promises'; +import { fail, requireConfig, submitRender } from './api.mjs'; +import { buildEdit } from './edit.mjs'; +import { recordRender } from './renders.mjs'; + +const REQUIRED_FIELDS = [ + 'stockId', + 'year', + 'make', + 'model', + 'trim', + 'price', + 'odometer', + 'transmission', + 'fuel', + 'dealer' +]; + +requireConfig(); + +let vehicles; + +try { + vehicles = JSON.parse( + await readFile(new URL('./vehicles.json', import.meta.url), 'utf8') + ); +} catch (error) { + fail(`Could not read vehicles.json: ${error.message}`); +} + +if (!Array.isArray(vehicles) || vehicles.length === 0) { + fail('vehicles.json must contain an array with at least one vehicle.'); +} + +const problems = []; + +vehicles.forEach((vehicle, index) => { + const label = vehicle.stockId ?? `vehicle ${index + 1}`; + + for (const field of REQUIRED_FIELDS) { + if (vehicle[field] === undefined || vehicle[field] === '') { + problems.push(`${label}: ${field} is required.`); + } + } + + if (!Array.isArray(vehicle.photos) || vehicle.photos.length === 0) { + problems.push(`${label}: photos must contain at least one HTTPS URL.`); + return; + } + + for (const photo of vehicle.photos) { + if (!/^https:\/\//.test(photo)) { + problems.push(`${label}: photo ${photo} must be an HTTPS URL.`); + } + } +}); + +if (problems.length > 0) { + fail(problems.join('\n')); +} + +let submitted = 0; + +for (const vehicle of vehicles) { + try { + const edit = buildEdit(vehicle); + const renderId = await submitRender(edit, vehicle.stockId); + + await recordRender({ + renderId, + stockId: vehicle.stockId, + submittedAt: new Date().toISOString() + }); + + console.log(`${vehicle.stockId} → ${renderId}`); + submitted += 1; + } catch (error) { + if (error.fatal) { + fail(error.message); + } + + console.error(error.message); + process.exitCode = 1; + } +} + +console.log(`${submitted}/${vehicles.length} submitted`); diff --git a/examples/automotive-inventory-videos/renders.mjs b/examples/automotive-inventory-videos/renders.mjs new file mode 100644 index 0000000..6debacb --- /dev/null +++ b/examples/automotive-inventory-videos/renders.mjs @@ -0,0 +1,40 @@ +import { appendFile, readFile } from 'node:fs/promises'; + +const FILE = new URL('./renders.jsonl', import.meta.url); + +// One JSON object per line, appended. The API has no endpoint that lists +// renders, so this file is the only record of which render belongs to +// which record. +export async function recordRender(row) { + try { + await appendFile(FILE, JSON.stringify(row) + '\n'); + } catch (error) { + throw new Error(`Could not write renders.jsonl: ${error.message}`); + } +} + +export async function readRenders() { + let text; + + try { + text = await readFile(FILE, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') { + return []; + } + throw new Error(`Could not read renders.jsonl: ${error.message}`); + } + + return text + .split('\n') + .filter(line => line.trim()) + .map((line, index) => { + try { + return JSON.parse(line); + } catch { + throw new Error( + `renders.jsonl line ${index + 1} is not valid JSON. Fix or delete the file and submit again.` + ); + } + }); +} diff --git a/examples/automotive-inventory-videos/status.mjs b/examples/automotive-inventory-videos/status.mjs new file mode 100644 index 0000000..30f6aee --- /dev/null +++ b/examples/automotive-inventory-videos/status.mjs @@ -0,0 +1,43 @@ +import { fail, getRender, requireConfig } from './api.mjs'; +import { readRenders } from './renders.mjs'; + +requireConfig(); + +let rows; + +try { + rows = await readRenders(); +} catch (error) { + fail(error.message); +} + +if (rows.length === 0) { + console.log('No renders recorded yet. Run node render.mjs first.'); + process.exit(0); +} + +for (const row of rows) { + let render; + + try { + render = await getRender(row.renderId, row.stockId); + } catch (error) { + if (error.fatal) { + fail(error.message); + } + + console.error(error.message); + process.exitCode = 1; + continue; + } + + console.log(`${row.stockId} → ${render.status}`); + + if (render.status === 'done') { + console.log(` ${render.url}`); + } + + if (render.status === 'failed') { + console.log(` error: ${render.error}`); + } +} diff --git a/examples/automotive-inventory-videos/vehicles.json b/examples/automotive-inventory-videos/vehicles.json new file mode 100644 index 0000000..a114aad --- /dev/null +++ b/examples/automotive-inventory-videos/vehicles.json @@ -0,0 +1,56 @@ +[ + { + "stockId": "STK-10482", + "year": 2024, + "make": "Mercedes-Benz", + "model": "A 200", + "trim": "Progressive hatch, Jupiter Red", + "price": "$47,990", + "odometer": "12,450 km", + "transmission": "Automatic", + "fuel": "Petrol", + "dealer": "Northside Motors", + "photos": [ + "https://templates.shotstack.io/car-sale-slideshow-video/27efb285-a3b2-47e6-8440-574a6b660183/pexels-photo-9513395.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/e574b011-2c53-47df-acfe-e0759840a106/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/ec960f5b-85be-453e-a951-e1469da2dd6a/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/96fd90d8-c88b-482c-a539-e375314c564b/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/6726f69d-af0f-4701-b5f9-98594f9ccfad/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/a25bb50a-8813-4514-9913-3e323cd7cf8c/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/cda2590c-1033-430a-bd0f-ca28506f99c8/source.jpg" + ] + }, + { + "stockId": "STK-10517", + "year": 2023, + "make": "Mercedes-Benz", + "model": "A 250", + "trim": "AMG Line hatch, Jupiter Red", + "price": "$52,500", + "odometer": "28,900 km", + "transmission": "Automatic", + "fuel": "Petrol", + "dealer": "Northside Motors", + "photos": [ + "https://templates.shotstack.io/car-sale-slideshow-video/ec960f5b-85be-453e-a951-e1469da2dd6a/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/96fd90d8-c88b-482c-a539-e375314c564b/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/a25bb50a-8813-4514-9913-3e323cd7cf8c/source.jpg", + "https://templates.shotstack.io/car-sale-slideshow-video/6726f69d-af0f-4701-b5f9-98594f9ccfad/source.jpg" + ] + }, + { + "stockId": "STK-10533", + "year": 2022, + "make": "Tesla", + "model": "Model 3", + "trim": "Standard Range Plus, Pearl White", + "price": "$58,900", + "odometer": "41,200 km", + "transmission": "Automatic", + "fuel": "Electric", + "dealer": "Northside Motors", + "photos": [ + "https://templates.shotstack.io/electric-car-for-sale/a7bc8cd1-1cf6-4020-841f-5b47b0d0f3b8/source.jpg" + ] + } +] From 91da9afa2b148c22a22c75ec752e6ff5a7115a1e Mon Sep 17 00:00:00 2001 From: Sonia Srivastava Date: Mon, 28 Sep 2026 12:18:10 +0700 Subject: [PATCH 2/2] Polish automotive-inventory-videos: one sample record --- .../automotive-inventory-videos/README.md | 4 +- examples/automotive-inventory-videos/edit.mjs | 44 ++++++++++++++----- .../automotive-inventory-videos/vehicles.json | 33 -------------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/examples/automotive-inventory-videos/README.md b/examples/automotive-inventory-videos/README.md index 4996f02..e4062a7 100644 --- a/examples/automotive-inventory-videos/README.md +++ b/examples/automotive-inventory-videos/README.md @@ -54,7 +54,7 @@ Run `status.mjs` again until each render shows `done`. `render.mjs` reads `vehicles.json` and checks that each vehicle has every field and at least one photo. It reports every problem it finds, then stops. For each vehicle it builds an Edit JSON and submits one render. The video shows the photos in sequence, 3.2 seconds each, cropped to fill the -vertical frame. Each photo fades in, and the pan or zoom changes from one photo to the next. A +vertical frame. Each photo dissolves into the next, and the pan or zoom changes from one photo to the next. A vehicle with one or two photos gets at least six seconds. A card with the year, make, model, trim, price, odometer, transmission and fuel type slides in at the bottom. The dealer name is at the top. A music track plays under the whole video. The script appends one line per render to @@ -64,7 +64,7 @@ top. A music track plays under the whole video. The script appends one line per URL. The card and the dealer name are `html5` assets. The script writes the vehicle data into their -HTML before the render. To render your own inventory, replace the records in `vehicles.json`. +HTML before the render. The sample has one vehicle. Add a record to `vehicles.json` for each vehicle you want to render. Photo URLs must be public HTTPS URLs. To change the layout, edit `edit.mjs`. `renders.jsonl` only appends. Delete the file to start a new batch. diff --git a/examples/automotive-inventory-videos/edit.mjs b/examples/automotive-inventory-videos/edit.mjs index 87389af..59e0798 100644 --- a/examples/automotive-inventory-videos/edit.mjs +++ b/examples/automotive-inventory-videos/edit.mjs @@ -60,17 +60,37 @@ function dealerBadge(vehicle, length) { }; } -// One clip per photo on a single track. "auto" starts each clip when the -// previous one ends, so the video is as long as the vehicle has photos. +// One clip per photo, alternating between two tracks so each photo +// dissolves into the next instead of dipping to black. An odd photo sits on +// the upper track: it fades in over the photo below, then fades out to reveal +// the next photo, which already plays underneath. OVERLAP matches the length +// of the fade transition. +const OVERLAP = 1; + function photoClips(photos, secondsPerPhoto) { - return photos.map((src, index) => ({ - asset: { type: 'image', src }, - start: index === 0 ? 0 : 'auto', - length: secondsPerPhoto, - fit: 'crop', - effect: EFFECTS[index % EFFECTS.length], - transition: { in: 'fade' } - })); + const upper = []; + const lower = []; + + photos.forEach((src, index) => { + const isLast = index === photos.length - 1; + const clip = { + asset: { type: 'image', src }, + start: index * secondsPerPhoto, + length: secondsPerPhoto + (isLast ? 0 : OVERLAP), + fit: 'crop', + effect: EFFECTS[index % EFFECTS.length] + }; + + if (index % 2 === 1) { + clip.transition = isLast ? { in: 'fade' } : { in: 'fade', out: 'fade' }; + upper.push(clip); + } else { + if (index === 0) clip.transition = { in: 'fade' }; + lower.push(clip); + } + }); + + return { upper, lower }; } export function buildEdit(vehicle) { @@ -80,6 +100,7 @@ export function buildEdit(vehicle) { MIN_LENGTH / vehicle.photos.length ); const length = secondsPerPhoto * vehicle.photos.length; + const photos = photoClips(vehicle.photos, secondsPerPhoto); return { timeline: { @@ -87,7 +108,8 @@ export function buildEdit(vehicle) { tracks: [ { clips: [specCard(vehicle, length)] }, { clips: [dealerBadge(vehicle, length)] }, - { clips: photoClips(vehicle.photos, secondsPerPhoto) }, + { clips: photos.upper }, + { clips: photos.lower }, { clips: [ { diff --git a/examples/automotive-inventory-videos/vehicles.json b/examples/automotive-inventory-videos/vehicles.json index a114aad..8961410 100644 --- a/examples/automotive-inventory-videos/vehicles.json +++ b/examples/automotive-inventory-videos/vehicles.json @@ -19,38 +19,5 @@ "https://templates.shotstack.io/car-sale-slideshow-video/a25bb50a-8813-4514-9913-3e323cd7cf8c/source.jpg", "https://templates.shotstack.io/car-sale-slideshow-video/cda2590c-1033-430a-bd0f-ca28506f99c8/source.jpg" ] - }, - { - "stockId": "STK-10517", - "year": 2023, - "make": "Mercedes-Benz", - "model": "A 250", - "trim": "AMG Line hatch, Jupiter Red", - "price": "$52,500", - "odometer": "28,900 km", - "transmission": "Automatic", - "fuel": "Petrol", - "dealer": "Northside Motors", - "photos": [ - "https://templates.shotstack.io/car-sale-slideshow-video/ec960f5b-85be-453e-a951-e1469da2dd6a/source.jpg", - "https://templates.shotstack.io/car-sale-slideshow-video/96fd90d8-c88b-482c-a539-e375314c564b/source.jpg", - "https://templates.shotstack.io/car-sale-slideshow-video/a25bb50a-8813-4514-9913-3e323cd7cf8c/source.jpg", - "https://templates.shotstack.io/car-sale-slideshow-video/6726f69d-af0f-4701-b5f9-98594f9ccfad/source.jpg" - ] - }, - { - "stockId": "STK-10533", - "year": 2022, - "make": "Tesla", - "model": "Model 3", - "trim": "Standard Range Plus, Pearl White", - "price": "$58,900", - "odometer": "41,200 km", - "transmission": "Automatic", - "fuel": "Electric", - "dealer": "Northside Motors", - "photos": [ - "https://templates.shotstack.io/electric-car-for-sale/a7bc8cd1-1cf6-4020-841f-5b47b0d0f3b8/source.jpg" - ] } ]