>FFmpegLab
FFmpegLab Deep Dive

Render Videos Using SQL – A Deep Dive into FFmpegLab Server

Translate the SDK example into raw SQL using the actual TypeORM schemas and Media types from the ffmpeglab/server repository.

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 Gap: From SDK to Database

Most developers interact with FFmpegLab Server through the SDK. But understanding the underlying SQL is valuable for:

SDK API TypeORM PostgreSQL
(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:

// SDK Example — Create and run a render
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

// src/model/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:

ColumnTypeDescription
iduuid (PK)Primary key, auto-generated
titletextRender title
projecttextProject identifier
statustextStatus: queued, processing, completed, failed, cancelled
publicbooleanPublic visibility flag
user_iduuidOwner user ID
progressnumericProgress percentage (0-100)
logstextRender logs (text) — FFmpeg output and errors
datajsonbRender configuration (RenderData type)
resultjsonbResult 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

// src/types/Media.ts – Media class
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

// src/types/Media.ts – MinimalMedia type
export type MinimalMedia = Pick<Media, 'id' | 'duration' | 'filename' | 'width' | 'height' | 'title' | 'filePath' | 'url' | 'userId'>;

RenderData

// src/types/Media.ts – RenderData class
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.

Step 1
Create the render record
Insert a new render with initial status queued. The data column must be a valid RenderData JSON object.
-- Create a new render
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;
💡 The 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:

-- Capture the render ID
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.

Step 2
Push the job to pgmq
Send a message to the rendering_queue with the render ID.
-- Push the job to the pgmq queue
SELECT pgmq.send(
  'rendering_queue',
  '{"renderId": "abc-123-def-456", "action": "process"}'
);
💡 The runner polls the 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:

-- Push with a 10‑second 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).

-- Get all logs for a specific render
SELECT date, logs, user_id
FROM logpiece
WHERE render = 'abc-123-def-456'
ORDER BY date ASC;
-- Get error logs only (if any)
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.

-- Get the render result
SELECT id, status, result
FROM "render"
WHERE id = 'abc-123-def-456';

Example result JSON (MinimalMedia):

"result": {
  "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 a function that notifies when a render status changes
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:

-- In your PostgreSQL client:
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:

// Node.js example
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:

-- Notify when a new log is inserted
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

High-Level Abstraction
  • 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

Raw Database Operations
  • 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

Troubleshooting

IssueLikely CauseFix
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:

And with the database's built‑in monitoring capabilities, you can:

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.