Helios provides built-in support for captions and subtitles using standard SRT and WebVTT formats. Captions are automatically synchronized with your timeline and can be easily styled and positioned.
Helios supports two standard caption formats:
- SRT (SubRip): Simple, widely-supported format with timecodes in
HH:MM:SS,mmm format
- WebVTT: Modern web standard with timecodes in
HH:MM:SS.mmm or MM:SS.mmm format
1
00:00:00,500 --> 00:00:02,000
Hello, welcome to Helios!
2
00:00:02,000 --> 00:00:03,500
This is a subtitle example.
WEBVTT
1
00:00:00.500 --> 00:00:02.000
Hello, welcome to Helios!
2
00:00:02.000 --> 00:00:03.500
This is a subtitle example.
Loading captions
Captions can be loaded when creating a Helios instance or added later.
During initialization
import { Helios } from '@helios-project/core';
const srtContent = `
1
00:00:00,500 --> 00:00:02,000
Welcome to the video
2
00:00:02,000 --> 00:00:04,000
Enjoy the content!
`;
const helios = new Helios({
duration: 10,
fps: 30,
captions: srtContent // Can be SRT or WebVTT string
});
Setting captions at runtime
// Load from string
helios.setCaptions(srtContent);
// Or use parsed caption objects
import { parseSrt, parseWebVTT } from '@helios-project/core';
const cues = parseSrt(srtContent);
helios.setCaptions(cues);
Accessing captions
Helios provides two caption-related signals:
// All captions in the composition
const allCaptions = helios.captions.value;
// Currently active captions at the current frame
const activeCaptions = helios.activeCaptions.value;
Caption cue structure
Unique identifier for the caption cue
Start time in milliseconds
Caption text content (may contain newlines)
Displaying captions
Create a caption component that reacts to the active captions:
import { useEffect, useState } from 'react';
import { Helios } from '@helios-project/core';
function CaptionOverlay({ helios }) {
const [activeCaptions, setActiveCaptions] = useState([]);
useEffect(() => {
return helios.subscribe((state) => {
setActiveCaptions(state.activeCaptions);
});
}, [helios]);
return (
<div style={{
position: 'absolute',
bottom: '10%',
left: '50%',
transform: 'translateX(-50%)',
textAlign: 'center',
color: 'white',
fontSize: '24px',
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.8)',
padding: '8px 16px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: '4px'
}}>
{activeCaptions.map((cue) => (
<div key={cue.id}>
{cue.text.split('\n').map((line, i) => (
<div key={i}>{line}</div>
))}
</div>
))}
</div>
);
}
Parsing utilities
Helios provides utilities for parsing and working with caption files.
parseSrt()
Parses SRT format caption content.
import { parseSrt } from '@helios-project/core';
const cues = parseSrt(srtContent);
// [
// { id: '1', startTime: 500, endTime: 2000, text: 'Hello' },
// { id: '2', startTime: 2000, endTime: 3500, text: 'World' }
// ]
SRT formatted caption content
Returns: CaptionCue[]
parseWebVTT()
Parses WebVTT format caption content.
import { parseWebVTT } from '@helios-project/core';
const cues = parseWebVTT(webVttContent);
WebVTT formatted caption content (must start with “WEBVTT”)
Returns: CaptionCue[]
parseCaptions()
Automatically detects and parses SRT or WebVTT format.
import { parseCaptions } from '@helios-project/core';
const cues = parseCaptions(content); // Auto-detects format
Caption content in SRT or WebVTT format
Returns: CaptionCue[]
stringifySrt()
Converts caption cues back to SRT format.
import { stringifySrt } from '@helios-project/core';
const srtContent = stringifySrt(cues);
Array of caption cues to serialize
Returns: string - SRT formatted content
findActiveCues()
Finds captions active at a specific time.
import { findActiveCues } from '@helios-project/core';
const currentTimeMs = (frame / fps) * 1000;
const active = findActiveCues(cues, currentTimeMs);
Array of caption cues to search
Returns: CaptionCue[] - Captions active at the specified time
Common patterns
Loading captions from file
import { Helios } from '@helios-project/core';
async function loadComposition() {
const response = await fetch('/captions/video.srt');
const srtContent = await response.text();
const helios = new Helios({
duration: 10,
fps: 30,
captions: srtContent
});
return helios;
}
Styled caption component
function StyledCaptions({ helios }) {
const [activeCaptions, setActiveCaptions] = useState([]);
useEffect(() => {
return helios.subscribe((state) => {
setActiveCaptions(state.activeCaptions);
});
}, [helios]);
if (activeCaptions.length === 0) return null;
return (
<div className="caption-container">
{activeCaptions.map((cue) => (
<div key={cue.id} className="caption-text">
{cue.text.split('\n').map((line, i) => (
<div key={i} className="caption-line">{line}</div>
))}
</div>
))}
</div>
);
}
.caption-container {
position: absolute;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
max-width: 80%;
text-align: center;
}
.caption-text {
display: inline-block;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
color: white;
font-size: 24px;
line-height: 1.4;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9);
}
.caption-line {
margin: 4px 0;
}
Animated captions
import { transition, Easing } from '@helios-project/core';
function AnimatedCaptions({ helios }) {
const [activeCaptions, setActiveCaptions] = useState([]);
const frame = useVideoFrame(helios);
const fps = helios.fps.value;
useEffect(() => {
return helios.subscribe((state) => {
setActiveCaptions(state.activeCaptions);
});
}, [helios]);
return (
<div className="caption-container">
{activeCaptions.map((cue) => {
const currentTimeMs = (frame / fps) * 1000;
const cueProgress = (currentTimeMs - cue.startTime) / (cue.endTime - cue.startTime);
// Fade in/out at start and end
let opacity = 1;
if (cueProgress < 0.1) {
opacity = cueProgress / 0.1;
} else if (cueProgress > 0.9) {
opacity = (1 - cueProgress) / 0.1;
}
// Slide up slightly at start
const y = cueProgress < 0.2 ? (0.2 - cueProgress) * 50 : 0;
return (
<div
key={cue.id}
style={{
opacity,
transform: `translateY(${y}px)`,
transition: 'transform 0.3s ease-out'
}}
>
{cue.text}
</div>
);
})}
</div>
);
}
Multi-language captions
import { useState } from 'react';
function MultiLanguageCaptions({ helios }) {
const [language, setLanguage] = useState('en');
const [captionsByLanguage] = useState({
en: parseSrt(englishSrt),
es: parseSrt(spanishSrt),
fr: parseSrt(frenchSrt)
});
useEffect(() => {
helios.setCaptions(captionsByLanguage[language]);
}, [language, helios]);
return (
<>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
<CaptionOverlay helios={helios} />
</>
);
}
Caption search and navigation
function CaptionTimeline({ helios }) {
const captions = helios.captions.value;
const fps = helios.fps.value;
const seekToCaption = (cue) => {
const frame = (cue.startTime / 1000) * fps;
helios.seek(frame);
};
return (
<div className="caption-timeline">
{captions.map((cue) => (
<button
key={cue.id}
onClick={() => seekToCaption(cue)}
className="caption-marker"
>
{cue.text.substring(0, 30)}...
</button>
))}
</div>
);
}
Export captions
import { stringifySrt } from '@helios-project/core';
function ExportCaptions({ helios }) {
const exportToFile = () => {
const captions = helios.captions.value;
const srtContent = stringifySrt(captions);
const blob = new Blob([srtContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'captions.srt';
a.click();
URL.revokeObjectURL(url);
};
return (
<button onClick={exportToFile}>
Export Captions
</button>
);
}
Creating captions programmatically
import { CaptionCue } from '@helios-project/core';
function generateCaptions(segments: string[], intervalSeconds: number): CaptionCue[] {
return segments.map((text, i) => ({
id: `${i + 1}`,
startTime: i * intervalSeconds * 1000,
endTime: (i + 1) * intervalSeconds * 1000,
text
}));
}
const captions = generateCaptions(
['First caption', 'Second caption', 'Third caption'],
2 // 2 seconds each
);
helios.setCaptions(captions);