The FFmpegLab SDK provides a clean, TypeScript‑first interface for rendering videos. But behind the scenes, every SDK call maps to database operations. By understanding these operations, you can render videos directly from SQL — useful for debugging, integration with legacy systems, or building custom automation pipelines.
This guide takes the SDK example and translates it step‑by‑step into raw SQL, using the actual TypeORM schemas and Media types from the ffmpeglab/server repository.
Key takeaways
- The SDK example creates a render and triggers it — all in two API calls.
- SQL does the same with
INSERTstatements across therendertable andpgmqqueue. - TypeORM models map to PostgreSQL tables:
Render,ApiKey, andLogPiece(though we skip those in this guide). - Media types define the structure of
dataandresultJSONB columns:RenderData,EditorProject,EditorLayer,MinimalMedia, andMedia. - pgmq handles the asynchronous job queue — we push directly to it.
- Logs can be retrieved from
logpiece, results fromrender.result, and you can use SQL triggers andLISTEN/NOTIFYfor real‑time monitoring.
The Gap: From SDK to Database
Most developers interact with FFmpegLab Server through the SDK. But understanding the underlying SQL is valuable for:
- Debugging — when something goes wrong, you need to know what the database looks like
- Batch operations — inserting hundreds of renders at once via SQL is faster than API calls
- Integration — connecting FFmpegLab to existing ETL pipelines
- Learning — understanding the data model helps you build better applications
(This guide skips the SDK and API layers to work directly with SQL)
The SDK Example
Here's the SDK example we'll be translating:
import * as ffmpeglab from 'ffmpeglab-sdk';
const mediaUrl = 'https://www.ffmpeglab.com/media/zoompan.mp4';
const clientConfig = new ffmpeglab.Configuration({
accessToken: 'API_KEY',
basePath: 'https://api.ffmpeglab.com',
});
const client = new ffmpeglab.RendersApi(clientConfig);
client.rendersControllerCreate({
renderDto: {
project: {
id: 'myproject',
editor: {
code: '-i $MEDIA_1 -movflags +faststart myproject.mp4',
selectedCode: 'custom'
}
},
layers: [
{
id: 'layer1',
media: [
{
id: 'media1',
url: mediaUrl
}
]
}
]
}
})
.then((render) => client.rendersControllerRunRender({
runDto: {
id: render.id
}
}));
Database Models – The Actual TypeORM Schemas
FFmpegLab Server uses TypeORM models that map to PostgreSQL tables. Here are the actual schemas from the repository:
Render Entity
Source: render.entity.ts
import type { Media, MinimalMedia, RenderData } from '../types';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity() export class Render {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column()
project: string;
@Column()
status: string;
@Column()
public: boolean;
@Column({ type: 'uuid' })
user_id: string;
@Column()
progress: number;
@Column()
logs: string;
@Column('simple-json')
data: RenderData;
@Column('simple-json')
result: MinimalMedia | Media;
}
Render table schema:
| Column | Type | Description |
|---|---|---|
| id | uuid (PK) | Primary key, auto-generated |
| title | text | Render title |
| project | text | Project identifier |
| status | text | Status: queued, processing, completed, failed, cancelled |
| public | boolean | Public visibility flag |
| user_id | uuid | Owner user ID |
| progress | numeric | Progress percentage (0-100) |
| logs | text | Render logs (text) — FFmpeg output and errors |
| data | jsonb | Render configuration (RenderData type) |
| result | jsonb | Result metadata (MinimalMedia or Media) |
Media Types – The Building Blocks
Source: types/Media.ts
The data and result columns in the render table use the following types:
Media
export class Media {
id: string;
uri?: string;
url?: string;
date: number;
folderId: string;
title: string;
description?: string;
filename: string;
fileType: string;
userId: string;
size?: number;
type?: string;
hasCloud?: boolean;
hasAudio?: boolean;
width: number;
height: number;
orderId?: number;
duration?: number;
isCopy?: string;
filePath?: string;
isVideo?: boolean;
isAudio?: boolean;
isTextFile?: string;
isReplace?: boolean;
}
MinimalMedia
export type MinimalMedia = Pick<Media, 'id' | 'duration' | 'filename' | 'width' | 'height' | 'title' | 'filePath' | 'url' | 'userId'>;
RenderData
export class RenderData {
@ApiProperty({ type: EditorProject })
project: EditorProject;
@ApiProperty({ type: [EditorLayer] })
layers: EditorLayer[];
}
Step 1: Create the Render
The SDK's rendersControllerCreate creates a render record. In SQL, this is an INSERT into the render table.
queued. The data column must be a valid RenderData JSON object.INSERT INTO "render" (
id, title, project, status, public, user_id, progress, logs, data, result
)
VALUES (
gen_random_uuid(),
'My Render',
'myproject',
'queued',
false,
'550e8400-e29b-41d4-a716-446655440000', -- user_id
0, -- progress
'', -- logs (will be updated by runner)
'{
"project": {
"editor": {
"code": "-i $MEDIA_1 -movflags +faststart myproject.mp4",
"selectedCode": "custom",
"length": 5,
"width": 1920,
"height": 1080,
"lastUpdated": 1743348893,
"compressionLevel": 23,
"preset": "medium",
"output": "mp4"
}
},
"layers": [
{
"id": "layer1",
"editor": {},
"media": [
{
"id": "media1",
"url": "https://www.ffmpeglab.com/media/zoompan.mp4",
"title": "zoompan",
"filename": "zoompan.mp4",
"encoding": {}
}
]
}
]
}',
'{}' -- result (MinimalMedia | Media)
)
RETURNING id;
data column is a RenderData object containing project (EditorProject) and layers (EditorLayer[]). The logs column will be populated by the render runner as it executes FFmpeg.
To capture the render ID for later use:
WITH new_render AS (
INSERT INTO "render" (
id, title, project, status, public, user_id, progress, logs, data, result
)
VALUES (
gen_random_uuid(),
'My Render',
'myproject',
'queued',
false,
'550e8400-e29b-41d4-a716-446655440000',
0,
'',
'{
"project": {
"editor": {
"code": "-i $MEDIA_1 -movflags +faststart myproject.mp4",
"selectedCode": "custom",
"length": 5,
"width": 1920,
"height": 1080,
"lastUpdated": 1743348893,
"compressionLevel": 23,
"preset": "medium",
"output": "mp4"
}
},
"layers": [
{
"id": "layer1",
"editor": {},
"media": [
{
"id": "media1",
"url": "https://www.ffmpeglab.com/media/zoompan.mp4",
"title": "zoompan",
"filename": "zoompan.mp4",
"encoding": {}
}
]
}
]
}',
'{}'
)
RETURNING id
)
SELECT id FROM new_render;
Step 2: Push to pgmq
The SDK's rendersControllerRunRender triggers the render by pushing a job to the pgmq queue. In SQL, this is a call to pgmq.send.
rendering_queue with the render ID.SELECT pgmq.send(
'rendering_queue',
'{"renderId": "abc-123-def-456", "action": "process"}'
);
rendering_queue for new messages. When a message arrives, the runner picks up the job, executes FFmpeg, and updates the render status.
You can also add a delay:
SELECT pgmq.send(
'rendering_queue',
'{"renderId": "abc-123-def-456"}',
10 );
Monitoring – Logs, Results, and Notifications
Once your render is queued and processed, you need to monitor its progress and retrieve the results. FFmpegLab Server stores all this information in the database.
Retrieving Logs from LogPiece
The runner writes detailed logs to the logpiece table. Each log entry is tied to a specific render by the render column (which stores the render's UUID).
SELECT date, logs, user_id
FROM logpiece
WHERE render = 'abc-123-def-456'
ORDER BY date ASC;
SELECT date, logs
FROM logpiece
WHERE render = 'abc-123-def-456'
AND logs LIKE '%error%';
Checking the Result
When the render completes successfully, the runner updates the result column of the render table. This column contains a MinimalMedia or Media object with details about the output video.
SELECT id, status, result
FROM "render"
WHERE id = 'abc-123-def-456';
Example result JSON (MinimalMedia):
"id": "media-123",
"duration": 10.5,
"filename": "output.mp4",
"width": 1920,
"height": 1080,
"title": "My Render",
"url": "https://your-bucket.s3.amazonaws.com/output.mp4",
"userId": "550e8400-e29b-41d4-a716-446655440000"
}
Real‑time Notifications with Triggers and LISTEN/NOTIFY
You can set up database triggers to automatically react to status changes or log entries, and use PostgreSQL's LISTEN/NOTIFY for real‑time updates.
Example: Trigger on Render Status Change
CREATE OR REPLACE FUNCTION notify_render_status()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(
'render_status_channel',
'{"renderId": "' || NEW.id || '", "status": "' || NEW.status || '"}'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create a trigger on the render table
CREATE TRIGGER render_status_trigger
AFTER UPDATE OF status ON "render"
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION notify_render_status();
Listening for Notifications
In your application (e.g., Node.js, Python), you can listen for these notifications:
LISTEN render_status_channel;
-- When a render completes, you'll receive a notification with the render ID and new status.
In Node.js with pg:
client.query('LISTEN render_status_channel');
client.on('notification', (msg) => {
const payload = JSON.parse(msg.payload);
console.log(`Render ${payload.renderId} status: ${payload.status}`);
});
Trigger on New Log Entries
Similarly, you can set up a trigger on the logpiece table:
CREATE OR REPLACE FUNCTION notify_new_log()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('log_channel', '{"renderId": "' || NEW.render || '", "log": "' || NEW.logs || '"}');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER new_log_trigger
AFTER INSERT ON logpiece
FOR EACH ROW
EXECUTE FUNCTION notify_new_log();
These mechanisms allow you to build reactive applications that respond to render completions, errors, and log updates without constant polling.
The Complete SQL Script
Here's the complete SQL script that replicates the SDK example using the actual TypeORM schemas and Media types:
-- Step 1: Create the render
-- ============================================================
WITH new_render AS (
INSERT INTO "render" (
id, title, project, status, public, user_id, progress, logs, data, result
)
VALUES (
gen_random_uuid(),
'My Render',
'myproject',
'queued',
false,
'550e8400-e29b-41d4-a716-446655440000',
0,
'',
'{
"project": {
"editor": {
"code": "-i $MEDIA_1 -movflags +faststart myproject.mp4",
"selectedCode": "custom",
"length": 5,
"width": 1920,
"height": 1080,
"lastUpdated": 1743348893,
"compressionLevel": 23,
"preset": "medium",
"output": "mp4"
}
},
"layers": [
{
"id": "layer1",
"editor": {},
"media": [
{
"id": "media1",
"url": "https://www.ffmpeglab.com/media/zoompan.mp4",
"title": "zoompan",
"filename": "zoompan.mp4",
"encoding": {}
}
]
}
]
}',
'{}'
)
RETURNING id
)
-- Store the render ID for later use
SELECT id INTO TEMP render_id FROM new_render;
-- ============================================================
-- Step 2: Push the job to pgmq
-- ============================================================
SELECT pgmq.send(
cast('render' as TEXT),
cast('{"renderId": "' || (SELECT id FROM render_id) || '"}' as JSONB)
);
SDK vs. SQL – How They Compare
🔵 SDK
- 1 API call to create the render
- 1 API call to run the render
- Handles authentication automatically
- Validates input types
- Returns typed objects
- Works with JavaScript/TypeScript
- Error handling built-in
🟢 SQL
- 2 SQL statements — insert render, push to pgmq
- Requires manual authentication (you need the API key)
- No type validation
- Returns raw row data
- Works with any PostgreSQL client
- Manual error handling
What the SDK Does That SQL Doesn't
- Transaction management — the SDK wraps everything in a database transaction
- Validation — the SDK checks that the render data is valid
- Authentication — the SDK handles API key validation
- Error handling — the SDK catches and reports errors
- Logging — the SDK automatically logs to LogPiece
Troubleshooting
| Issue | Likely Cause | Fix |
|---|---|---|
| Render stays in "queued" | pgmq queue not created or runner not running | Create the rendering_queue in Supabase Queues; start the runner |
| JSONB validation fails | data doesn't match RenderData type |
Ensure the JSON matches the RenderData structure |
| Column not found | Schema mismatch | Run the TypeORM migrations to ensure the schema is up to date |
| No logs in logpiece | Runner not writing logs or render not processed | Check runner status; ensure render ID is correct |
Frequently Asked Questions (FAQ)
Why would I use SQL instead of the SDK?
SQL is useful for batch operations, debugging, and integration with legacy systems. For example, inserting 1,000 renders via a single SQL script is faster than 1,000 API calls.
Does SQL trigger the rendering pipeline?
Yes. When you push a message to the pgmq queue, the render-runner picks it up and starts the FFmpeg job. The runner polls the queue for new messages.
How do I get the logs for a render?
You can query the logpiece table using the render's UUID. For example: SELECT * FROM logpiece WHERE render = 'your-render-id' ORDER BY date ASC;
How do I get the rendered video result?
After the render finishes, the result column of the render table contains a MinimalMedia or Media object with the output file URL and metadata.
Can I get real‑time updates when a render completes?
Yes. You can set up a PostgreSQL trigger that calls pg_notify on status changes, and then use LISTEN/NOTIFY in your application to receive real‑time updates.
Final Word
The SDK provides a clean, type‑safe interface for rendering videos, but understanding the underlying SQL gives you superpowers. You can debug more effectively, build batch pipelines, and integrate FFmpegLab Server with any system that speaks SQL.
The complete flow is just two SQL steps:
- Step 1:
INSERT INTO "render"— create the render withRenderDatain thedatacolumn - Step 2:
SELECT pgmq.send(...)— push the job to the queue
And with the database's built‑in monitoring capabilities, you can:
- Retrieve logs from
logpieceusing the render ID - Check the result in the
render.resultcolumn once processing is complete - Set up triggers and
LISTEN/NOTIFYto get real‑time notifications
Whether you use the SDK or raw SQL, the result is the same: a rendered video, with a complete audit trail in your PostgreSQL database.