Skip to main content
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.

Caption formats

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

SRT 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 format

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

id
string
Unique identifier for the caption cue
startTime
number
Start time in milliseconds
endTime
number
End time in milliseconds
text
string
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' }
// ]
content
string
required
SRT formatted caption content
Returns: CaptionCue[]

parseWebVTT()

Parses WebVTT format caption content.
import { parseWebVTT } from '@helios-project/core';

const cues = parseWebVTT(webVttContent);
content
string
required
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
content
string
required
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);
cues
CaptionCue[]
required
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);
cues
CaptionCue[]
required
Array of caption cues to search
timeMs
number
required
Time in milliseconds
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);

Build docs developers (and LLMs) love