> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pictory.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed Preview Player

> Integrate the Pictory storyboard preview player into your web application with real-time element updates

This guide shows you how to embed the Pictory storyboard preview player in your web application using an iframe. You'll learn to display video previews, communicate with the player using postMessage, and update preview elements in real-time without re-rendering.

## Preview Player Demo

Here's how the embedded preview player looks when integrated into your application:

<Frame caption="Pictory Preview Player embedded in an iframe with full playback controls">
  <iframe src="https://video.pictory.ai/v2/preview/318e78f8-aec4-4217-a787-42f7e1302a2b?mode=player" width="100%" height="450" style={{ border: 'none', borderRadius: '8px' }} allow="autoplay; fullscreen" />
</Frame>

<Tip>
  The preview player supports play/pause, seek, fullscreen controls, and responds to real-time element updates using postMessage. You can replace background visuals, modify text, and adjust settings without re-rendering the video.
</Tip>

<Note>
  **Stock Visual Watermarks:** The preview may display watermarked stock visuals from media libraries. These watermarks are automatically removed when you render the final video.
</Note>

## What You'll Learn

<CardGroup cols={2}>
  <Card title="Embed Preview" icon="window-restore">
    Display the preview player in an iframe
  </Card>

  <Card title="Player Communication" icon="message">
    Use postMessage API to communicate with the player
  </Card>

  <Card title="Real-time Updates" icon="rotate">
    Update preview elements without re-rendering
  </Card>

  <Card title="Event Handling" icon="bell">
    Handle player events like loaded and error states
  </Card>
</CardGroup>

## Before You Begin

Make sure you have:

* A Pictory API key ([get one here](https://app.pictory.ai/api-access))
* A completed storyboard preview job with a `previewUrl`
* Basic knowledge of JavaScript and iframe communication

## Workflow Overview

The preview player integration follows a simple four-step process: create a preview, poll for completion, embed the player, and update elements in real-time.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant API as Pictory API
    participant Player as Preview Player (iframe)

    App->>API: Create Storyboard Preview
    API-->>App: jobId
    App->>API: Poll Job Status
    API-->>App: previewUrl + renderParams
    App->>Player: Embed iframe with previewUrl
    Player-->>App: ON_LOADED event
    App->>Player: UPDATE_PREVIEW_ELEMENTS
    Player-->>App: Confirmation
```

## Step 1: Create Storyboard Preview

First, create a storyboard preview using the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview):

<CodeGroup>
  ```javascript Node.js theme={null}
  import axios from "axios";

  const API_BASE_URL = "https://api.pictory.ai/pictoryapis";
  const API_KEY = "YOUR_API_KEY";

  async function createStoryboardPreview() {
    const response = await axios.post(
      `${API_BASE_URL}/v2/video/storyboard`,
      {
        videoName: "my_preview_video",
        voiceOver: {
          enabled: true,
          aiVoices: [{ speaker: "Brian", speed: 100 }]
        },
        scenes: [
          {
            story: "Welcome to our product demo. Discover amazing features that will transform your workflow.",
            createSceneOnEndOfSentence: true
          }
        ]
      },
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: API_KEY
        }
      }
    );

    return response.data.data.jobId;
  }
  ```

  ```python Python theme={null}
  import requests

  API_BASE_URL = 'https://api.pictory.ai/pictoryapis'
  API_KEY = 'YOUR_API_KEY'

  def create_storyboard_preview():
      response = requests.post(
          f'{API_BASE_URL}/v2/video/storyboard',
          json={
              'videoName': 'my_preview_video',
              'voiceOver': {
                  'enabled': True,
                  'aiVoices': [{'speaker': 'Brian', 'speed': 100}]
              },
              'scenes': [
                  {
                      'story': 'Welcome to our product demo. Discover amazing features that will transform your workflow.',
                      'createSceneOnEndOfSentence': True
                  }
              ]
          },
          headers={
              'Content-Type': 'application/json',
              'Authorization': API_KEY
          }
      )
      return response.json()['data']['jobId']
  ```
</CodeGroup>

## Step 2: Get Preview URL and Render Params

Poll the job status until it is completed to get the `previewUrl` and `renderParams`:

<CodeGroup>
  ```javascript Node.js theme={null}
  async function getPreviewData(jobId) {
    while (true) {
      const response = await axios.get(
        `${API_BASE_URL}/v1/jobs/${jobId}`,
        { headers: { Authorization: API_KEY } }
      );

      const { status, previewUrl, renderParams, storyboard } = response.data.data;

      if (status === "completed") {
        console.log("Preview ready!");
        return { previewUrl, renderParams, storyboard };
      } else if (status === "failed") {
        throw new Error("Preview generation failed");
      }

      // Wait 3 seconds before polling again
      await new Promise(resolve => setTimeout(resolve, 3000));
    }
  }

  // Usage
  const jobId = await createStoryboardPreview();
  const { previewUrl, renderParams, storyboard } = await getPreviewData(jobId);

  console.log("Preview URL:", previewUrl);
  console.log("Elements count:", renderParams.elements.length);
  ```

  ```python Python theme={null}
  import time

  def get_preview_data(job_id):
      while True:
          response = requests.get(
              f'{API_BASE_URL}/v1/jobs/{job_id}',
              headers={'Authorization': API_KEY}
          )
          data = response.json()['data']

          if data['status'] == 'completed':
              print("Preview ready!")
              return {
                  'previewUrl': data['previewUrl'],
                  'renderParams': data['renderParams'],
                  'storyboard': data['storyboard']
              }
          elif data['status'] == 'failed':
              raise Exception("Preview generation failed")

          # Wait 3 seconds before polling again
          time.sleep(3)

  # Usage
  job_id = create_storyboard_preview()
  preview_data = get_preview_data(job_id)

  print(f"Preview URL: {preview_data['previewUrl']}")
  print(f"Elements count: {len(preview_data['renderParams']['elements'])}")
  ```
</CodeGroup>

**Example Job Response:**

```json theme={null}
{
  "job_id": "be990033-b282-4809-b733-be5ac961770b",
  "success": true,
  "data": {
    "status": "completed",
    "renderParams": {
      "output": {
          "width": 1920,
          "height": 1080,
          "format": "mp4",
          "name": "demo_text_to_video.mp4"
      },
      "elements": [
        {
          "id": "backgroundElement_1",
          "elementType": "backgroundElement",
          "type": "video",
          "url": "https://example.com/video.mp4",
          "backgroundColor": "rgba(255,255,255,1)",
          "xPercent": "0%",
          "yPercent": "0%",
          "width": "100%",
          "objectMode": "cover",
          "loop": true,
          "mute": true,
          "segments": [],
          "startTime": 0,
          "duration": 6.86,
          "cursor": "default",
          "visualUrl": "https://example.com/video.mp4",
          "visualType": "video",
          "visualDescription": "AI Generated Background Video"
        }
      ]
    },
    "storyboard": { },
    "previewUrl": "https://video.pictory.ai/v2/preview/318e78f8-aec4-4217-a787-42f7e1302a2b?mode=player"
  }
}
```

### Background Element Structure

The `backgroundElement` is the core visual element for each scene. Key properties you can modify:

| Property          | Type    | Description                                     |
| ----------------- | ------- | ----------------------------------------------- |
| `url`             | string  | The media URL for the background video or image |
| `visualUrl`       | string  | Same as `url`, the visual media source          |
| `type`            | string  | Media type: `video` or `image`                  |
| `visualType`      | string  | Same as `type`, the visual media type           |
| `duration`        | number  | Duration in seconds                             |
| `loop`            | boolean | Whether to loop the video                       |
| `mute`            | boolean | Whether to mute video audio                     |
| `backgroundColor` | string  | Fallback background color (RGBA format)         |
| `objectMode`      | string  | How media fits: `cover`, `contain`, `fill`      |

## Step 3: Embed Preview Player in iframe

### Basic HTML Implementation

```html theme={null}
<!DOCTYPE html>
<html>
<head>
  <title>Pictory Preview Player</title>
  <style>
    #preview-container {
      width: 100%;
      max-width: 1280px;
      height: 720px;
      margin: 0 auto;
      position: relative;
    }

    #preview-iframe {
      width: 100%;
      height: 100%;
      border: none;
    }

    .loader {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
  </style>
</head>
<body>
  <div id="preview-container">
    <div id="loader" class="loader">Loading preview...</div>
    <iframe
      id="preview-iframe"
      src="https://video.pictory.ai/v2/preview/YOUR_PREVIEW_ID?mode=player"
      allow="autoplay; fullscreen"
      style="opacity: 0;"
    ></iframe>
  </div>

  <script src="preview-editor.js"></script>
  <script>
    // Initialize after page loads
    document.addEventListener('DOMContentLoaded', initPreview);
  </script>
</body>
</html>
```

### Preview Editor Class

Create a `preview-editor.js` file with the PreviewEditor class that handles iframe communication:

```javascript theme={null}
import { v4 as uuid } from 'uuid';

export class PreviewEditor {
  constructor(containerElement, previewUrl, options = {}) {
    this.options = options;
    this.inProgressTasks = {};
    this.loaded = false;
    this.onLoaded = null;
    this.onError = null;

    // Create iframe
    const iframe = document.createElement('iframe');
    iframe.setAttribute('src', previewUrl);
    iframe.style.border = 'none';
    iframe.style.height = '100%';
    iframe.style.width = '100%';
    iframe.allow = 'autoplay; fullscreen';

    // Clear container and append iframe
    containerElement.innerHTML = '';
    containerElement.appendChild(iframe);

    this.iframe = iframe;

    // Listen for messages from the iframe
    window.addEventListener('message', this.receiveEditorMessages);
  }

  // Clean up resources
  close() {
    window.removeEventListener('message', this.receiveEditorMessages);
    if (this.iframe.parentNode) {
      this.iframe.parentNode.removeChild(this.iframe);
      this.iframe.setAttribute('src', '');
    }
    this.inProgressTasks = {};
  }

  // Update preview elements
  updatePreview = async (renderParams) => {
    await this.executeEditorAction({
      message: 'UPDATE_PREVIEW_ELEMENTS',
      elements: renderParams.elements
    }).catch(error => {
      throw new Error(`Failed to update preview: ${error.message}`);
    });
  }

  // Execute action on the editor iframe
  executeEditorAction = async (message, payload) => {
    if (!this.loaded) {
      throw new Error('The Editor is not loaded.');
    }

    const id = uuid();
    if (this.iframe.contentWindow) {
      this.iframe.contentWindow.postMessage(
        { id, ...JSON.parse(JSON.stringify(message)), ...payload },
        '*'
      );
    }

    // Return promise that resolves when iframe responds
    return new Promise((resolve, reject) => {
      this.inProgressTasks[id] = { resolve, reject };
    });
  }

  // Handle messages from the iframe
  receiveEditorMessages = async (event) => {
    if (!event.data || typeof event.data !== 'object') {
      return;
    }

    // Verify message is from our iframe
    if (this.iframe.contentWindow !== event.source) {
      return;
    }

    const { id, message, error, ...args } = event.data;

    if (id) {
      // Resolve pending promise
      const inProgressTask = this.inProgressTasks[id];
      if (inProgressTask) {
        if (error) {
          inProgressTask.reject(new Error(error));
        } else {
          inProgressTask.resolve(args);
        }
        delete this.inProgressTasks[id];
      }
    } else {
      // Handle broadcast messages
      switch (message) {
        case 'ON_LOADED':
          this.loaded = true;
          if (this.onLoaded) {
            await this.onLoaded(this);
          }
          break;
        case 'ON_ERROR':
          this.errored = true;
          if (this.onError) {
            await this.onError(this, error);
          }
          break;
      }
    }
  }
}
```

## Step 4: React Integration

Here's a complete React component for embedding the preview player:

```jsx theme={null}
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { PreviewEditor } from './previewEditor';

const PreviewPlayer = ({
  previewUrl,
  renderParams,
  onLoaded,
  onError
}) => {
  const containerRef = useRef(null);
  const [editor, setEditor] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  // Initialize preview editor on mount
  useLayoutEffect(() => {
    if (!containerRef.current || !previewUrl) return;

    const previewEditor = new PreviewEditor(
      containerRef.current,
      previewUrl
    );

    // Handle loaded event
    previewEditor.onLoaded = async (editor) => {
      setIsLoading(false);
      onLoaded?.(editor);
    };

    // Handle error event
    previewEditor.onError = (editor, error) => {
      setIsLoading(false);
      onError?.(error);
    };

    setEditor(previewEditor);

    // Cleanup on unmount
    return () => {
      previewEditor.close();
    };
  }, [previewUrl]);

  // Update preview when renderParams change
  useEffect(() => {
    if (editor && renderParams && editor.loaded) {
      editor.updatePreview(renderParams);
    }
  }, [renderParams, editor]);

  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      {isLoading && (
        <div style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
          zIndex: 10
        }}>
          Loading preview...
        </div>
      )}
      <div
        ref={containerRef}
        style={{
          width: '100%',
          height: '100%',
          opacity: isLoading ? 0 : 1,
          transition: 'opacity 0.3s ease'
        }}
      />
    </div>
  );
};

export default PreviewPlayer;
```

### Usage in Your App

```jsx theme={null}
import React, { useState, useEffect } from 'react';
import PreviewPlayer from './PreviewPlayer';
import { createStoryboardPreview, getPreviewData } from './api';

function App() {
  const [previewUrl, setPreviewUrl] = useState(null);
  const [renderParams, setRenderParams] = useState(null);
  const [isGenerating, setIsGenerating] = useState(false);

  const generatePreview = async () => {
    setIsGenerating(true);
    try {
      const jobId = await createStoryboardPreview();
      const data = await getPreviewData(jobId);

      setPreviewUrl(data.previewUrl);
      setRenderParams(data.renderParams);
    } catch (error) {
      console.error('Failed to generate preview:', error);
    } finally {
      setIsGenerating(false);
    }
  };

  // Find and update background element by ID
  const updateBackgroundVisual = (elementId, newVisualUrl, visualType = 'video') => {
    const updatedElements = renderParams.elements.map(element => {
      if (element.id === elementId && element.elementType === 'backgroundElement') {
        return {
          ...element,
          url: newVisualUrl,
          visualUrl: newVisualUrl,
          type: visualType,
          visualType: visualType
        };
      }
      return element;
    });

    // Update renderParams to trigger preview update
    setRenderParams({
      ...renderParams,
      elements: updatedElements
    });
  };

  // Get background elements from renderParams
  const backgroundElements = renderParams?.elements?.filter(
    el => el.elementType === 'backgroundElement'
  ) || [];

  return (
    <div style={{ padding: '20px' }}>
      <button onClick={generatePreview} disabled={isGenerating}>
        {isGenerating ? 'Generating...' : 'Generate Preview'}
      </button>

      {previewUrl && (
        <div style={{ marginTop: '20px', height: '500px' }}>
          <PreviewPlayer
            previewUrl={previewUrl}
            renderParams={renderParams}
            onLoaded={() => console.log('Preview loaded!')}
            onError={(err) => console.error('Preview error:', err)}
          />
        </div>
      )}

      {backgroundElements.length > 0 && (
        <div style={{ marginTop: '20px' }}>
          <h3>Change Background Visual:</h3>
          {backgroundElements.map((element, index) => (
            <div key={element.id} style={{ marginBottom: '15px' }}>
              <label>Scene {index + 1} Background ({element.visualType}):</label>
              <input
                type="text"
                defaultValue={element.visualUrl}
                placeholder="Enter new video or image URL"
                onChange={(e) => updateBackgroundVisual(element.id, e.target.value)}
                style={{ width: '100%', padding: '10px', marginTop: '5px' }}
              />
              <small style={{ color: '#666' }}>
                Current: {element.visualDescription || 'No description'}
              </small>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default App;
```

## Step 5: Update Preview Elements in Real-time

The key feature is updating preview elements without re-rendering the entire video. When you modify the `elements` array and call `updatePreview()`, the player updates instantly.

### Replacing Background Visual

To replace a scene's background video or image, find the `backgroundElement` and update its `url` and `visualUrl` properties:

```javascript theme={null}
// Get the current elements from renderParams
const elements = [...renderParams.elements];

// Find the background element you want to update
const backgroundElement = elements.find(
  el => el.id === 'backgroundElement_1' && el.elementType === 'backgroundElement'
);

if (backgroundElement) {
  // Update the visual URL (both url and visualUrl should be updated)
  backgroundElement.url = "https://your-cdn.com/new-background-video.mp4";
  backgroundElement.visualUrl = "https://your-cdn.com/new-background-video.mp4";
}

// Update the preview
await previewEditor.updatePreview({ elements });
```

### Background Element Properties

| Property          | Type    | Description                    | Example                          |
| ----------------- | ------- | ------------------------------ | -------------------------------- |
| `url`             | string  | Primary media URL              | `"https://cdn.com/video.mp4"`    |
| `visualUrl`       | string  | Visual media URL (same as url) | `"https://cdn.com/video.mp4"`    |
| `type`            | string  | Media type                     | `"video"` or `"image"`           |
| `visualType`      | string  | Visual media type              | `"video"` or `"image"`           |
| `duration`        | number  | Duration in seconds            | `6.86`                           |
| `loop`            | boolean | Loop video playback            | `true`                           |
| `mute`            | boolean | Mute video audio               | `true`                           |
| `backgroundColor` | string  | Fallback color                 | `"rgba(255,255,255,1)"`          |
| `objectMode`      | string  | Media fit mode                 | `"cover"`, `"contain"`, `"fill"` |

### Example: Building a Background Visual Editor

```jsx theme={null}
const BackgroundVisualEditor = ({ elements, onElementsChange }) => {
  // Filter only background elements
  const backgroundElements = elements.filter(
    el => el.elementType === 'backgroundElement'
  );

  const updateBackgroundElement = (elementId, updates) => {
    const newElements = elements.map(element => {
      if (element.id === elementId) {
        return { ...element, ...updates };
      }
      return element;
    });
    onElementsChange(newElements);
  };

  return (
    <div>
      <h3>Background Visuals</h3>
      {backgroundElements.map((element, index) => (
        <div
          key={element.id}
          style={{
            marginBottom: '20px',
            padding: '15px',
            border: '1px solid #ccc',
            borderRadius: '8px'
          }}
        >
          <h4>Scene {index + 1} Background</h4>

          {/* Visual URL Editor */}
          <div style={{ marginBottom: '10px' }}>
            <label>Visual URL:</label>
            <input
              type="text"
              value={element.visualUrl || ''}
              onChange={(e) => {
                updateBackgroundElement(element.id, {
                  url: e.target.value,
                  visualUrl: e.target.value
                });
              }}
              placeholder="Enter video or image URL"
              style={{ width: '100%', padding: '8px', marginTop: '4px' }}
            />
          </div>

          {/* Visual Type Selector */}
          <div style={{ marginBottom: '10px' }}>
            <label>Media Type:</label>
            <select
              value={element.visualType || 'video'}
              onChange={(e) => {
                updateBackgroundElement(element.id, {
                  type: e.target.value,
                  visualType: e.target.value
                });
              }}
              style={{ width: '100%', padding: '8px', marginTop: '4px' }}
            >
              <option value="video">Video</option>
              <option value="image">Image</option>
            </select>
          </div>
        </div>
      ))}
    </div>
  );
};
```

### Replacing Multiple Backgrounds

```javascript theme={null}
// Replace all background visuals with new URLs from an array
function replaceAllBackgrounds(elements, newVisuals) {
  let visualIndex = 0;

  return elements.map(element => {
    if (element.elementType === 'backgroundElement' && newVisuals[visualIndex]) {
      const newVisual = newVisuals[visualIndex];
      visualIndex++;

      return {
        ...element,
        url: newVisual.url,
        visualUrl: newVisual.url,
        type: newVisual.type || 'video',
        visualType: newVisual.type || 'video',
        visualDescription: newVisual.description || element.visualDescription
      };
    }
    return element;
  });
}

// Usage
const newVisuals = [
  { url: 'https://cdn.com/scene1-background.mp4', type: 'video', description: 'Intro animation' },
  { url: 'https://cdn.com/scene2-background.jpg', type: 'image', description: 'Product shot' },
  { url: 'https://cdn.com/scene3-background.mp4', type: 'video', description: 'Demo footage' }
];

const updatedElements = replaceAllBackgrounds(renderParams.elements, newVisuals);
await previewEditor.updatePreview({ elements: updatedElements });
```

## PostMessage API Reference

### Messages Sent to the Preview Player

| Message                   | Payload            | Description                              |
| ------------------------- | ------------------ | ---------------------------------------- |
| `UPDATE_PREVIEW_ELEMENTS` | `{ elements: [] }` | Update the preview with new element data |

### Messages Received from the Preview Player

| Message     | Payload             | Description                              |
| ----------- | ------------------- | ---------------------------------------- |
| `ON_LOADED` | None                | Player has finished loading and is ready |
| `ON_ERROR`  | `{ error: string }` | An error occurred in the player          |

### Message Format

All messages follow this structure:

```javascript theme={null}
// Outgoing message (to player)
{
  id: "unique-message-id",  // For tracking response
  message: "UPDATE_PREVIEW_ELEMENTS",
  elements: [...]
}

// Incoming message (from player)
{
  id: "unique-message-id",  // Matches outgoing message
  // ... response data
}

// Broadcast message (from player, no id)
{
  message: "ON_LOADED"
}
```

## Complete Integration Example

Here's a complete end-to-end example:

<CodeGroup>
  ```javascript Complete Example theme={null}
  import axios from 'axios';
  import { v4 as uuid } from 'uuid';

  // ============================================
  // API Functions
  // ============================================

  const API_BASE_URL = 'https://api.pictory.ai/pictoryapis';
  const API_KEY = 'YOUR_API_KEY';

  async function createPreview(story) {
    const response = await axios.post(
      `${API_BASE_URL}/v2/video/storyboard`,
      {
        videoName: 'embedded_preview_demo',
        voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] },
        scenes: [{ story, createSceneOnEndOfSentence: true }]
      },
      { headers: { Authorization: API_KEY, 'Content-Type': 'application/json' } }
    );
    return response.data.data.jobId;
  }

  async function waitForPreview(jobId) {
    while (true) {
      const response = await axios.get(
        `${API_BASE_URL}/v1/jobs/${jobId}`,
        { headers: { Authorization: API_KEY } }
      );

      const { status, previewUrl, renderParams } = response.data.data;

      if (status === 'completed') {
        return { previewUrl, renderParams };
      } else if (status === 'failed') {
        throw new Error('Preview generation failed');
      }

      await new Promise(resolve => setTimeout(resolve, 3000));
    }
  }

  // ============================================
  // Preview Editor Class
  // ============================================

  class PreviewEditor {
    constructor(container, url) {
      this.inProgressTasks = {};
      this.loaded = false;

      const iframe = document.createElement('iframe');
      iframe.src = url;
      iframe.style.cssText = 'border:none;width:100%;height:100%';
      iframe.allow = 'autoplay; fullscreen';

      container.innerHTML = '';
      container.appendChild(iframe);
      this.iframe = iframe;

      window.addEventListener('message', this.handleMessage);
    }

    close() {
      window.removeEventListener('message', this.handleMessage);
      this.iframe?.remove();
    }

    async updatePreview(renderParams) {
      if (!this.loaded) throw new Error('Not loaded');

      const id = uuid();
      this.iframe.contentWindow.postMessage({
        id,
        message: 'UPDATE_PREVIEW_ELEMENTS',
        elements: renderParams.elements
      }, '*');

      return new Promise((resolve, reject) => {
        this.inProgressTasks[id] = { resolve, reject };
      });
    }

    handleMessage = (event) => {
      if (event.source !== this.iframe?.contentWindow) return;

      const { id, message, error } = event.data || {};

      if (id && this.inProgressTasks[id]) {
        const task = this.inProgressTasks[id];
        error ? task.reject(new Error(error)) : task.resolve(event.data);
        delete this.inProgressTasks[id];
      } else if (message === 'ON_LOADED') {
        this.loaded = true;
        this.onLoaded?.(this);
      } else if (message === 'ON_ERROR') {
        this.onError?.(this, error);
      }
    };
  }

  // ============================================
  // Usage
  // ============================================

  async function initializePreview() {
    const container = document.getElementById('preview-container');
    const statusEl = document.getElementById('status');

    statusEl.textContent = 'Creating preview...';

    // Create and wait for preview
    const jobId = await createPreview(
      'Welcome to our product demo. Discover amazing features. Start your journey today.'
    );

    statusEl.textContent = 'Generating preview...';
    const { previewUrl, renderParams } = await waitForPreview(jobId);

    statusEl.textContent = 'Loading player...';

    // Initialize editor
    const editor = new PreviewEditor(container, previewUrl);

    editor.onLoaded = () => {
      statusEl.textContent = 'Preview ready!';

      // Store for later updates
      window.previewEditor = editor;
      window.renderParams = renderParams;
    };

    editor.onError = (_, error) => {
      statusEl.textContent = `Error: ${error}`;
    };
  }

  // Update background visual with new URL
  function updateBackgroundVisual(elementId, newUrl, visualType = 'video') {
    const { previewEditor, renderParams } = window;

    // Find and update the background element
    const updatedElements = renderParams.elements.map(element => {
      if (element.id === elementId && element.elementType === 'backgroundElement') {
        return {
          ...element,
          url: newUrl,
          visualUrl: newUrl,
          type: visualType,
          visualType: visualType
        };
      }
      return element;
    });

    renderParams.elements = updatedElements;
    previewEditor.updatePreview(renderParams);
  }

  // Example: Replace first background with a new video
  function replaceFirstBackground() {
    updateBackgroundVisual(
      'backgroundElement_1',
      'https://your-cdn.com/new-product-video.mp4',
      'video'
    );
  }

  // Start initialization
  initializePreview();
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Handle Loading States">
    Always show a loading indicator while the preview is initializing. The player may take a few seconds to load depending on network conditions.

    ```jsx theme={null}
    {isLoading && <LoadingSpinner />}
    <div style={{ opacity: isLoading ? 0 : 1 }}>
      {/* Preview container */}
    </div>
    ```
  </Accordion>

  <Accordion title="Clean Up Resources">
    Always call `close()` when unmounting the component or navigating away to prevent memory leaks and remove event listeners.

    ```jsx theme={null}
    useEffect(() => {
      return () => {
        editor?.close();
      };
    }, [editor]);
    ```
  </Accordion>

  <Accordion title="Debounce Updates">
    When building live editors, debounce updates to prevent overwhelming the player with rapid changes.

    ```javascript theme={null}
    import { debounce } from 'lodash';

    const debouncedUpdate = debounce((elements) => {
      editor.updatePreview({ elements });
    }, 300);
    ```
  </Accordion>

  <Accordion title="Error Handling">
    Implement comprehensive error handling for network failures and player errors.

    ```jsx theme={null}
    editor.onError = (_, error) => {
      console.error('Player error:', error);
      showErrorNotification('Failed to load preview. Please try again.');
    };
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Preview not loading">
    **Problem:** The iframe shows a blank screen or loading forever.

    **Solutions:**

    * Verify the `previewUrl` is valid and not expired
    * Check browser console for CORS or security errors
    * Verify your domain is allowed to embed the preview
  </Accordion>

  <Accordion title="Updates not reflecting">
    **Problem:** Calling `updatePreview()` does not change the preview.

    **Solutions:**

    * Ensure `editor.loaded` is `true` before calling update
    * Check that elements array structure is correct
    * Verify the message is being received (check browser console)
    * Wait for the `ON_LOADED` event before updating
  </Accordion>

  <Accordion title="postMessage not working">
    **Problem:** Messages are not being received by the iframe.

    **Solutions:**

    * Verify `iframe.contentWindow` is not null
    * Check that you are listening for messages correctly
    * Ensure the message origin matches expected sources
    * Use browser dev tools to inspect postMessage traffic
  </Accordion>

  <Accordion title="Memory leaks">
    **Problem:** Application becomes slow after multiple preview loads.

    **Solutions:**

    * Always call `editor.close()` when done
    * Remove event listeners in cleanup functions
    * Clear the iframe src before removing
    * Use React's useEffect cleanup or componentWillUnmount
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Render Final Video" icon="film" href="/api-reference/videos/render-from-preview">
    Convert the preview to a final rendered video
  </Card>

  <Card title="Render with Modifications" icon="wand-magic-sparkles" href="/api-reference/videos/render-video">
    Apply modifications and render the final video
  </Card>

  <Card title="Create Storyboard Preview" icon="eye" href="/api-reference/videos/create-storyboard-preview">
    Full API reference for preview creation
  </Card>

  <Card title="Get Storyboard Preview Job" icon="clock" href="/api-reference/jobs/get-storyboard-preview-job-by-id">
    Monitor job progress and retrieve results
  </Card>
</CardGroup>
