# Closed Caption Creator IFrame Embed — LLM Integration Reference > Self-contained reference for AI coding assistants integrating the Closed Caption Creator caption/subtitle editor into a web application via ` ``` `https://iframe-demo.closedcaptioncreator.com` is the public demo origin. Customers with a dedicated build get their own origin — substitute it in `src` and `allowedOrigins`. Dedicated builds may also restrict which parent origins can embed them (CSP `frame-ancestors`); contact Closed Caption Creator to add your domain. ## Connect (canonical pattern) ```js import { WindowMessenger, connect } from 'penpal'; const EDITOR_ORIGIN = 'https://iframe-demo.closedcaptioncreator.com'; const iframe = document.getElementById('editor'); iframe.addEventListener('load', async () => { const messenger = new WindowMessenger({ remoteWindow: iframe.contentWindow, allowedOrigins: [EDITOR_ORIGIN], }); const connection = connect({ messenger, // Methods the PARENT exposes to the editor (optional, see "Parent-side methods"). methods: {}, }); const remote = await connection.promise; console.log('Editor version:', await remote.getVersion()); await remote.setProjectName('My Project'); await remote.setProjectFrameRate(29.97); await remote.setProjectDropFrame(true); const media = await remote.importMedia('https://example.com/video.mp4'); if (!media.success) throw new Error(media.message); const sub = await remote.importSubtitle('https://example.com/captions.srt', 'subRip'); if (!sub.success) throw new Error(sub.message); }); ``` With the CDN build, use `Penpal.WindowMessenger` and `Penpal.connect`. Call `connection.destroy()` when unmounting (e.g. in a React `useEffect` cleanup). ## Parent-side methods The editor can call back into the parent. Expose these on the `methods` object passed to `connect()`. All are optional; only implement the ones you enable. | Parent method | Called when | Expected return | |---|---|---| | `exportSubtitle()` | User clicks the toolbar **Save** button (enable with `remote.setParentToolbarActions({ exportSubtitle: true })`) | anything; the parent decides what Save does | | `cancel()` | User clicks the toolbar **Close** button (enable with `remote.setParentToolbarActions({ cancel: true })`) | anything | | `getVoices()` | Audio Description mode (`remote.setAdLicense(true)`) needs the voice list | `{ success: boolean, message?: string, voices?: { id: string, name: string, language: string \| null, gender?: string }[], defaultVoice?: string }` | | `renderAudio(request)` | Audio Description mode renders a cue to speech. `request = { text: string (≤ 5000 chars), voice?: string, speed?: number (0.25–4.0), format?: "mp3" \| "wav", eventId?: string }` | `{ success: boolean, message?: string, url?: string, format?: string, contentType?: string, cached?: boolean, eventId?: string \| null }` — `url` must be playable by the browser | **Naming clash:** the parent-side `exportSubtitle()` (no arguments, toolbar Save callback) is unrelated to the editor-side `remote.exportSubtitle(profile, encodeOptions, saveAsFile, ...)` which encodes captions. A typical Save handler calls the latter from inside the former: ```js let remote; const connection = connect({ messenger, methods: { async exportSubtitle() { const res = await remote.exportSubtitle('webVtt', {}, false); if (!res.success) return remote.alertUser({ title: 'Export failed', text: res.message }); await fetch('/api/captions', { method: 'PUT', headers: { 'Content-Type': 'text/vtt' }, body: res.data }); await remote.alertUser({ title: 'Saved', text: 'Captions saved.' }); }, cancel() { window.location.href = '/library'; }, }, }); remote = await connection.promise; await remote.setParentToolbarActions({ exportSubtitle: true, cancel: true }); ``` ## Core workflows ### Export edited captions as a string and upload ```js const res = await remote.exportSubtitle('subRip', {}, false); // saveAsFile=false → returns data if (res.success) { await fetch('/api/captions', { method: 'POST', body: res.data }); } ``` `saveAsFile: true` triggers a browser download instead of returning `data`. Optional later arguments: `forcedOption` (`"include"` | `"exclude"` | `"only"`), `eventGroupId` (defaults to the selected group), `exportTracks` (e.g. `["B"]`). ### Import into an existing Event Group / track `importSubtitle` creates a new Event Group by default. Pass the 7th argument `target` to merge or replace instead: ```js const { id: groupId } = await remote.getSelectedEventGroupId(); await remote.importSubtitle(secondLanguageUrl, 'subRip', null, {}, {}, false, { eventGroupId: groupId, importOption: 'merge', // or 'replace' (default) track: 'B', // 'A' | 'B' | 'C' | 'D' }); ``` Set Event Group metadata on a new group via the 4th argument, e.g. `{ name: 'French Subtitles', type: 'subtitle', language: 'fr-FR' }`. ### Quality-control gate before publishing ```js const guide = await remote.loadStyleGuide({ id: 'my-platform-guide', // stable ID → re-loading updates it in place name: 'My Platform Guide', enabled: true, maxLines: 2, maxChars: 42, maxCps: 17, minDuration: 1, maxDuration: 7, minEventGap: 0.083, overlap: true, illegalChars: true, leadingTrailingSpace: true, }); const { id: groupId } = await remote.getSelectedEventGroupId(); const qc = await remote.qcEventGroup(groupId, guide.id); if (qc.errorCount > 0) { await remote.alertUser({ title: 'Fix QC errors', text: `${qc.errorCount} issues found.` }); return; // block publish } ``` Style guide fields (all optional): `id, name, enabled, totalMaxChars, minLines, maxLines, maxChars, maxDuration, minDuration, minCps, maxCps, minWpm, maxWpm, minWordsPerLine, maxWordsPerLine, minEventGap, maxEventGap, minEventGapTolerance, maxEventGapTolerance` (numbers; durations/gaps in seconds) and boolean checks `overlap, illegalChars, hyphenSpace, hasUnderscore, periods, missingSpeaker, useEllipses, spellNumbers, spellNumbersAtStart, netflixGlyphs, partialItalics, fullItalics, partialBold, fullBold, partialUnderline, fullUnderline, repeatWords, fitSubtitles, leadingTrailingSpace, whitespace, blankLines, positionTopLeft, positionTopCenter, positionTopRight, positionCenterLeft, positionCenter, positionCenterRight, positionBottomLeft, positionBottomCenter, positionBottomRight, positionYOffset, positionXOffset, approvalPassed, approvalFailed, approvalNotSet, notes, tags, forced`. ### Save / restore full project state ```js const ccprj = await remote.getProjectData(); // full project JSON await saveToBackend(ccprj); // later await remote.loadProjectData(ccprj); // resets undo stack, sets clean baseline ``` ### Warn about unsaved changes ```js // beforeunload handlers cannot await, so poll and cache the dirty flag. let dirty = false; setInterval(async () => { dirty = await remote.hasUnsavedChanges(); }, 2000); window.addEventListener('beforeunload', (e) => { if (dirty) e.preventDefault(); }); ``` ### Progress modal while the parent prepares content ```js await remote.toggleStatusModal(true); await remote.updateModalTitle('Preparing media'); await remote.updateModalStatus({ progress: 30, msg: 'Fetching captions…' }); // ... do work ... await remote.completeImport(); // or: await remote.failImport('Could not fetch media'); ``` ### Theming (call right after connecting, before importMedia) ```js await remote.setTheme({ primary: '#408fe0', bodyBg: '#181c24', bodyColor: 'rgba(255, 255, 255, 0.9)', borderColor: 'rgba(255, 255, 255, 0.1)', fontFamily: 'Inter, system-ui, sans-serif', }); ``` Tokens: `primary, primaryRgb, bodyBg, bodyBgRgb, bodyColor, bodyColorRgb, secondaryColor, borderColor, fontFamily, linkColor, linkHoverColor, light, danger, warning, success, info`, or raw `--bs-*` CSS variable names. Theming affects UI chrome only — not the timeline canvas, caption preview font (`setPreviewFontFamily`), or QC rules (`loadStyleGuide`). Use `loadCustomStyles(cssText)` for advanced CSS and `clearTheme()` to reset. ### Watermark the player (overlay only; never burned into exports) ```js await remote.setWatermark({ enabled: true, showDateTime: true, dateTime: '2026-09-14 07:22:43', // host supplies the value; static until called again email: 'user@example.com', ipAddress: null, customText: 'CONFIDENTIAL', alignment: 'bottom', textOpacity: 100, shadowOpacity: 100, }); ``` ### Customize the secondary toolbar ```js await remote.configureSecondaryToolbar({ left: ['undo', 'redo', 'shortcuts'], center: ['addEvent', 'removeEvent', 'split', 'merge'], right: ['alignLeft', 'alignCenter', 'alignRight', 'copy', 'cut', 'paste'], }); // remote.configureSecondaryToolbar(null) restores the default layout ``` ## API Reference (editor-side `remote` methods) These are all 77 methods on the `remote` object returned by `await connection.promise`. Call them as `await remote.methodName(...)`. No other methods exist. - Modal Control: `getVersion`, `updateModalStatus`, `toggleStatusModal`, `updateModalTitle`, `completeImport`, `failImport`, `alertUser` - Project Settings: `setProjectId`, `setProjectName`, `setProjectDescription`, `setProjectFrameRate`, `setProjectDropFrame`, `setProjectIncode`, `setProjectState`, `setAdLicense`, `setParentToolbarActions`, `configureSecondaryToolbar` - Display Settings: `setDisplaySettings`, `enableSubtitleMode`, `enableCaptionMode`, `setPreviewFontSize`, `setPreviewFontFamily`, `setWatermark` - App Theme / Branding: `setTheme`, `loadCustomStyles`, `clearTheme` - Keyboard Shortcuts: `getKeyboardShortcuts`, `setKeyboardShortcuts` - Media & Timeline: `setMediaPeaksPath`, `setMediaPeaksData`, `useFallbackTimeline`, `useDefaultTimeline`, `toggleTimeline`, `setPoster`, `importMedia` - Quick Tools: `setQuickToolsSettings`, `selectQuickToolsPanel` - Transcript Sync: `setTranscript`, `checkSync`, `resyncEventGroup`, `loadTranscript` - Subtitle Import & Export: `importSubtitle`, `exportSubtitle` - Markers: `getMarkers`, `setMarkers`, `createMarkerList`, `insertMarker` - Style Guide & QC: `loadStyleGuide`, `getStyleGuides`, `qcEventGroup` - Event Groups: `getSelectedEventGroupId`, `getEventGroupInfo`, `addEventGroup`, `removeEventGroup`, `mergeEventGroups` - Speakers: `addSpeaker`, `getSpeakers` - Project Data: `loadProjectData`, `getHistory`, `hasUnsavedChanges`, `getProjectData` - Event Selection & Navigation: `getSelectedEvents`, `selectEventById`, `selectEventByIndex`, `scrollToEventById`, `scrollToEventByIndex` - Lock State: `enableVideoLock`, `disableVideoLock`, `enableCaptionLock`, `disableCaptionLock`, `enablePreviewLock`, `disablePreviewLock` - Auto Formatting & Correction: `autoFormat`, `autoCorrectReadingSpeed`, `fixEventOverlap`, `setProjectFrameGap`, `insertBlankFrames` ### Modal Control #### `getVersion()` - **Arguments:** none - **Returns:** `Promise` Returns the current application version. #### `updateModalStatus(statusInfo)` - **Arguments:** - statusInfo: { progress: number, msg: string } - **Returns:** `Promise` Updates the progress bar and status message in the IFrame remote modal. #### `toggleStatusModal(show)` - **Arguments:** - show: boolean - **Returns:** `Promise` Controls the IFrame remote status modal. If `show` is true, displays the modal. If falsy: hides the modal when it is already open, otherwise shows it. #### `updateModalTitle(title)` - **Arguments:** - title: string - **Returns:** `Promise` Updates the title text displayed in the IFrame remote modal header. #### `completeImport()` - **Arguments:** none - **Returns:** `Promise` Displays a success alert and closes the modal after a successful import operation. #### `failImport(msg)` - **Arguments:** - msg: string - **Returns:** `Promise` Displays an error alert with the provided message and closes the modal after a failed import. #### `alertUser(alertObj)` - **Arguments:** - alertObj: { title?: string, text?: string } - **Returns:** `Promise` Displays a SweetAlert2 modal with a custom title and message to alert the user. ### Project Settings #### `setProjectId(id)` - **Arguments:** - id: string - **Returns:** `Promise` Sets the unique identifier for the current project. #### `setProjectName(name)` - **Arguments:** - name: string - **Returns:** `Promise` Sets the display name of the current project. #### `setProjectDescription(description)` - **Arguments:** - description: string - **Returns:** `Promise` Sets the description for the current project. #### `setProjectFrameRate(frameRate)` - **Arguments:** - frameRate: number (e.g., 23.976, 25, 29.97, 30) - **Returns:** `Promise` Sets the project's video frame rate. #### `setProjectDropFrame(dropFrame)` - **Arguments:** - dropFrame: boolean - **Returns:** `Promise` Sets whether the project uses drop-frame timecode. #### `setProjectIncode(incode)` - **Arguments:** - incode: string (timecode string, e.g., "00:00:00:00") - **Returns:** `Promise` Sets the project's incode (start timecode). #### `setProjectState(state)` - **Arguments:** - state: object (Partial project state object) - **Returns:** `Promise` Updates multiple project state properties at once. #### `setAdLicense(enabled)` - **Arguments:** - enabled: boolean - **Returns:** `Promise (resulting adLicense value)` Enables or disables Audio Description license mode. When true, voice listing and text-to-speech rendering are routed through the parent host instead of the Firebase cloud TTS path. The parent must expose `getVoices()` and `renderAudio(request)` on its Penpal methods for AD features to work. #### `setParentToolbarActions(actions)` - **Arguments:** - actions: { exportSubtitle?: boolean, cancel?: boolean } (Optional; omitted keys are left unchanged) - **Returns:** `Promise<{ exportSubtitle: boolean, cancel: boolean }> (resulting visibility flags)` Shows or hides the iframe toolbar Close and Save buttons. Defaults come from envStore (`parentCancel` / `parentExportSubtitle`, both false). When Save is shown, clicks call the parent's `exportSubtitle()`; when Close is shown, clicks call the parent's `cancel()`. This is separate from the child `exportSubtitle` encode/retrieve API. #### `configureSecondaryToolbar(settings)` - **Arguments:** - settings: { left?: string[], center?: string[], right?: string[] } or `null` / omitted to restore defaults. - Customizable IDs: exportFile, importFile, undo, redo, shortcuts, addEvent, removeEvent, alignLeft, alignCenter, alignRight, writingMode, position, positionLineUp, positionLineDown, copy, cut, paste, shiftWordBack, shiftWordForward, shiftLineBack, shiftLineForward, split, merge, expand, compress, autoFormat, snapToStart, snapToEnd. - Exempt (ignored): open, save, mediaImport, transcription, translation, settings. - **Returns:** `Promise<{ success: boolean, message?: string, layout?: { left: string[], center: string[], right: string[] }, ignored?: { id: string, reason: string }[] }>` Replaces the iframe secondary-toolbar layout. Only listed customizable icons appear, in array order, in the given slot. Omitted slot keys become empty arrays. `null` / no argument restores the default iframe layout (undo/redo/shortcuts on the left, caption tools on the right). Unknown, duplicate, and exempt IDs are skipped and returned in `ignored`. Session-only; does not persist across reloads. Keyboard shortcuts remain available when an icon is hidden. ### Display Settings #### `setDisplaySettings(settings)` - **Arguments:** - settings: { displayWidth?: number (1-100), displayHeight?: number (1-50), xPadding?: number (0-50), yPadding?: number (0-50), lineSpacing?: number (0-50) } - **Returns:** `Promise<{ success: boolean, message: string, settings?: object }>` Updates caption display settings including width, height, padding, and line spacing. Settings are validated and saved to localStorage. #### `enableSubtitleMode()` - **Arguments:** none - **Returns:** `Promise` Enables subtitle preview mode (sets non-CC mode for rendering). #### `enableCaptionMode()` - **Arguments:** none - **Returns:** `Promise` Enables caption preview mode (sets CC mode for rendering). #### `setPreviewFontSize(size)` - **Arguments:** - size: number - **Returns:** `Promise` Sets the preview font size. #### `setPreviewFontFamily(family)` - **Arguments:** - family: string - **Returns:** `Promise` Sets the preview font family. #### `setWatermark(settings)` - **Arguments:** - settings: { enabled: boolean, showDateTime: boolean, dateTime: string | null ("YYYY-MM-DD HH:mm:ss", local), email: string | null, ipAddress: string | null, customText: string | null, alignment: "top" | "center" | "bottom", textOpacity: number (0-100), shadowOpacity: number (0-100) } - **Returns:** `Promise<{ success: boolean, message: string, settings?: object }>` Draws a visual watermark over the media player, beneath the caption/subtitle preview. The host resolves the display values, so `dateTime`, `email`, `ipAddress`, and `customText` are passed as strings (or `null` when that field is off). The date/time is static: push a new call to refresh it. Host settings take precedence over any team watermark configured in Options for the rest of the session. The watermark is an overlay only and is never burned into exported media. ### App Theme / Branding #### `setTheme(tokens)` - **Arguments:** - tokens: object (aliases such as primary, primaryRgb, bodyBg, bodyBgRgb, bodyColor, bodyColorRgb, secondaryColor, borderColor, fontFamily, linkColor, linkHoverColor, light, danger, warning, success, info — or raw --bs-* CSS variable names) - **Returns:** `Promise<{ success: boolean, message: string }>` Applies structured UI chrome theme overrides as CSS variables on the document root. Hex `primary` auto-derives `primaryRgb` when omitted. Call after the PenPal connection resolves and before media import when possible. Does not recolor the Konva timeline. Separate from caption QC `loadStyleGuide`. #### `loadCustomStyles(cssText)` - **Arguments:** - cssText: string (CSS stylesheet text) - **Returns:** `Promise<{ success: boolean, message: string }>` Injects or replaces a runtime