Documentation
¶
Overview ¶
Package examples holds runnable, self-contained snippets demonstrating the go-608 packages. Each package ticket adds an example for its feature; this file keeps the directory a buildable package until then.
Example (CarriageFrameSEINALU) ¶
Example_carriageFrameSEINALU builds one video frame's CEA-608 SEI NAL unit from a field-1 byte pair and parses it straight back, recovering the same pair. This is the encode → decode round-trip a consumer performs per frame: it prepends a 4-byte length to the bare NAL and splices it before the first VCL NALU on the way out, and hands a sample's NAL units to FieldPairs on the way in.
package main
import (
"fmt"
"github.com/Eyevinn/go-608/carriage"
)
func main() {
// One field-1 byte pair (here the Resume Caption Loading control code, odd
// parity already applied) and no field-2 data. ccCount 20 matches a 29.97/30 fps
// frame (SPEC §5.3); the remaining constructs are DTVCC padding.
field1 := []byte{0x94, 0x20}
nalu := carriage.FrameSEINALU(field1, nil, 20, carriage.CodecAVC)
fmt.Printf("SEI NAL unit: %d bytes, header %#02x\n", len(nalu), nalu[0])
// Decode: hand the NAL back as one of a sample's NAL units.
f1, f2, err := carriage.FieldPairs([][]byte{nalu}, carriage.CodecAVC)
if err != nil {
panic(err)
}
fmt.Printf("field1: % x\n", f1)
fmt.Printf("field2 empty: %t\n", len(f2) == 0)
}
Output: SEI NAL unit: 75 bytes, header 0x06 field1: 94 20 field2 empty: true
Example (Cta608Decode) ¶
Example_cta608Decode authors a two-line pop-on caption, serializes it to cc_data byte pairs, then feeds those bytes to a Decoder to recover the rendered Screen — the encode → wire → decode round-trip the core is built around.
package main
import (
"fmt"
"github.com/Eyevinn/go-608/cta608"
)
func main() {
var enc cta608.Encoder
toks := enc.SetScreen(cta608.Screen{Rows: []cta608.Row{
{Index: 14, Runs: []cta608.Run{{Column: 0, Text: "HELLO", Pen: cta608.Pen{Color: cta608.White}}}},
{Index: 15, Runs: []cta608.Run{{Column: 0, Text: "WORLD", Pen: cta608.Pen{Color: cta608.Yellow}}}},
}})
data := cta608.Serialize(toks, cta608.SerializeOptions{})
var dec cta608.Decoder
if err := dec.Feed(data); err != nil {
panic(err)
}
for _, r := range dec.Screen().Rows {
for _, run := range r.Runs {
fmt.Printf("row %d col %d %s: %q\n", r.Index, run.Column, run.Pen.Color, run.Text)
}
}
}
Output: row 14 col 0 white: "HELLO" row 15 col 0 yellow: "WORLD"
Example (Cta608Encoder) ¶
Example_cta608Encoder authors a two-line, bottom-anchored, centered pop-on caption as a CaptionBlock, then lets the Encoder diff it against the empty display into a token stream. The Encoder is the single per-channel diff engine: it lowers each line's absolute columns to PAC indent + Tab Offset and compensates the colored line's mid-row cell (SPEC §7), and wraps the build in RCL/ENM … EOC. The tokens then serialize to odd-parity cc_data byte pairs.
block := cta608.CaptionBlock{
Mode: cta608.PopOn,
Anchor: cta608.AnchorBottom,
Lines: []cta608.Line{
{Align: cta608.AlignCenter, Runs: []cta608.Run{{Text: "HELLO", Pen: cta608.Pen{Color: cta608.White}}}},
{Align: cta608.AlignCenter, Runs: []cta608.Run{{Text: "WORLD", Pen: cta608.Pen{Color: cta608.Yellow}}}},
},
}
var enc cta608.Encoder // zero value: pop-on, empty display
tokens := enc.Apply(block)
for _, tok := range tokens {
fmt.Println(tok)
}
// Serialize with control-code doubling off for a compact, deterministic
// pair sequence.
data := cta608.Serialize(tokens, cta608.SerializeOptions{Doubling: cta608.DoublingOff})
fmt.Printf("cc_data (%d bytes): % x\n", len(data), data)
Output: SetMode(pop-on) Command(ENM) PAC(row=14 indent=12 white) Tab(1) Chars("HELLO") PAC(row=15 indent=12 white) MidRow(yellow) Chars("WORLD") Command(EOC) cc_data (26 bytes): 94 20 94 ae 94 d6 97 a1 c8 45 4c 4c 4f 80 94 76 91 2a 57 4f 52 4c c4 80 94 2f
Example (Cta608RoundTrip) ¶
Example_cta608RoundTrip builds a pop-on caption as a token stream, serializes it to odd-parity cc_data byte pairs, and parses it straight back — the core wire round-trip the whole library pivots on.
tokens := []cta608.Token{
cta608.SetMode{Mode: cta608.PopOn},
cta608.PAC{Row: 15, Indent: cta608.NoIndent, Pen: cta608.Pen{Color: cta608.White}},
cta608.Chars{Text: "HELLO"},
cta608.MidRow{Pen: cta608.Pen{Color: cta608.Red}},
cta608.Chars{Text: "WORLD"},
cta608.Command{Op: cta608.EOC},
}
// Field 1, channel 1, control-code doubling on (the standard default).
data := cta608.Serialize(tokens, cta608.SerializeOptions{})
fmt.Printf("cc_data (%d bytes): % x\n", len(data), data)
back, err := cta608.Parse(data, cta608.ParseOptions{})
if err != nil {
panic(err)
}
for _, tok := range back {
fmt.Println(tok)
}
Output: cc_data (28 bytes): 94 20 94 20 94 e0 94 e0 c8 45 4c 4c 4f 80 91 a8 91 a8 57 4f 52 4c c4 80 94 2f 94 2f SetMode(pop-on) PAC(row=15 white) Chars("HELLO") MidRow(red) Chars("WORLD") Command(EOC)
Example (CueCompile) ¶
Example_cueCompile shows the text->608 direction. cue.Compile merges any overlapping cues by position and drives the core cta608.Encoder diff engine to re-flip a pop-on caption at each boundary, emitting wall-time-tagged token transitions (SPEC §8.2). Frame scheduling is the schedule package's job.
cues := []cue.TimedCue{
{Start: 0, End: 2 * time.Second, Content: lineScreen(15, "HELLO")},
{Start: 2 * time.Second, End: 4 * time.Second, Content: lineScreen(15, "WORLD")},
}
for _, tt := range cue.Compile(cues) {
fmt.Printf("@%v\n", tt.Time)
for _, tok := range tt.Tokens {
fmt.Printf(" %s\n", tok)
}
}
Output: @0s SetMode(pop-on) Command(ENM) PAC(row=15 white) Chars("HELLO") Command(EOC) @2s SetMode(pop-on) Command(ENM) PAC(row=15 white) Chars("WORLD") Command(EOC) @4s Command(EDM)
Example (CueSegment) ¶
Example_cueSegment shows the 608->text direction. A cta608.Decoder driven by timed byte pairs reports a displayed-Screen change whenever the caption changes; cue.Segment cuts that timeline into cues with one unified rule — a change closes the current cue and opens a new one, an empty screen is a gap, and a caption still shown at the end takes a configurable end (SPEC §8.2).
changes := []cue.TimedScreen{
{Time: 1 * time.Second, Screen: lineScreen(15, "HELLO")},
{Time: 3 * time.Second, Screen: cta608.Screen{}}, // erase: gap, no cue
{Time: 4 * time.Second, Screen: lineScreen(15, "WORLD")}, // still shown at stream end
}
// No StreamEnd is known, so the dangling final cue runs for DefaultDur.
cues := cue.Segment(changes, cue.SegmentOptions{DefaultDur: 2 * time.Second})
for _, c := range cues {
fmt.Printf("%v-%v %q\n", c.Start, c.End, screenText(c.Content))
}
Output: 1s-3s "HELLO" 4s-6s "WORLD"
Example (Generate) ¶
Example_generate drives the wall-clock Generator one call per frame for three seconds at 30 fps, wraps each frame's triple as a carriage SEI NAL, decodes it back, and prints the clock caption each time it flips — the full generate → schedule → carriage → cta608.Decoder loop the consumers use.
package main
import (
"fmt"
"math"
"time"
"github.com/Eyevinn/go-608/carriage"
"github.com/Eyevinn/go-608/cta608"
"github.com/Eyevinn/go-608/generate"
)
// Example_generate drives the wall-clock Generator one call per frame for three
// seconds at 30 fps, wraps each frame's triple as a carriage SEI NAL, decodes it
// back, and prints the clock caption each time it flips — the full
// generate → schedule → carriage → cta608.Decoder loop the consumers use.
func main() {
const fps = 30.0
g := generate.NewGenerator(fps, generate.DefaultConfig())
var dec cta608.Decoder
start := time.Date(2026, 1, 2, 15, 4, 5, 0, time.UTC).UnixMilli()
for frame := 0; frame < 3*30; frame++ {
wall := start + int64(math.Round(float64(frame)*1000.0/fps))
f := g.NextFrame(wall)
if len(f.Field1) == 0 {
continue // idle frame (cc_count padded by carriage)
}
nalu := carriage.FrameSEINALU(f.Field1, f.Field2, f.CCCount, carriage.CodecAVC)
fld1, _, err := carriage.FieldPairs([][]byte{nalu}, carriage.CodecAVC)
if err != nil {
panic(err)
}
if err := dec.Feed(fld1); err != nil {
panic(err)
}
if dec.Changed() {
fmt.Printf("flip @frame %d: %s | %s\n", frame, rowText(dec.Screen(), 14), rowText(dec.Screen(), 15))
}
}
}
func rowText(s cta608.Screen, idx int) string {
for _, r := range s.Rows {
if r.Index != idx {
continue
}
txt := ""
for _, run := range r.Runs {
txt += run.Text
}
return txt
}
return ""
}
Output: flip @frame 29: 2026-01-02T15:04:06Z | MEDIA 00:00:01 flip @frame 59: 2026-01-02T15:04:07Z | MEDIA 00:00:02 flip @frame 89: 2026-01-02T15:04:08Z | MEDIA 00:00:03
Example (SccReadParseWrite) ¶
Example_sccReadParseWrite reads a Scenarist SCC file, flattens its entries to per-frame byte pairs, parses those into the cta608 token stream, and writes the file back out byte-exact. It is the SCC container's whole job: own the text structure and timecodes, hand the verbatim 608 bytes to the core, and lose nothing on the way back.
// A minimal SCC document: a pop-on "HELLO WORLD" caption at 00:00:01:00 and an
// erase-displayed at 00:00:04:00. The ':' separators and low frame fields make
// the reader infer the 29.97 NTSC default (non-drop).
const doc = "Scenarist_SCC V1.0\n" +
"\n" +
"00:00:01:00\t9420 9420 94ae 94ae 94e0 94e0 c845 4c4c 4f20 574f 524c c480 942f 942f\n" +
"\n" +
"00:00:04:00\t942c 942c\n"
f, err := scc.Read(strings.NewReader(doc))
if err != nil {
panic(err)
}
fmt.Printf("fps=%.2f drop=%v entries=%d\n", f.FPS, f.DropFrame, len(f.Entries))
// Flatten to per-frame pairs (pair i of an entry sits at Frame+i) and feed the
// concatenated channel-1 bytes to cta608.Parse for tokens with per-frame timing.
timed := f.TimedPairs()
fmt.Printf("first pair at frame %d\n", timed[0].Frame)
var data []byte
for _, p := range timed {
data = append(data, p.Pair...)
}
tokens, err := cta608.Parse(data, cta608.ParseOptions{ValidateParity: true})
if err != nil {
panic(err)
}
for _, tok := range tokens {
fmt.Println(tok)
}
// Write the file back and confirm the round-trip is byte-exact.
var out bytes.Buffer
if err := scc.Write(&out, f); err != nil {
panic(err)
}
fmt.Printf("byte-exact round-trip: %v\n", out.String() == doc)
Output: fps=29.97 drop=false entries=2 first pair at frame 30 SetMode(pop-on) Command(ENM) PAC(row=15 white) Chars("HELLO WORLD") Command(EOC) Command(EDM) byte-exact round-trip: true
Example (ScheduleToCarriage) ¶
Example_scheduleToCarriage schedules a short pop-on caption onto video frames, wraps each frame with carriage, and decodes it straight back — the shared encode path both the wall-clock generator and the subtitle-compile path use. schedule serializes the tokens and drains at most one byte pair per field per frame (padding to cc_count); carriage builds the per-frame SEI NAL unit. In production the caller splices the NAL into the elementary stream rather than decoding it in place.
package main
import (
"fmt"
"github.com/Eyevinn/go-608/carriage"
"github.com/Eyevinn/go-608/cta608"
"github.com/Eyevinn/go-608/schedule"
)
func main() {
// A one-line pop-on caption as a token stream.
tokens := []cta608.Token{
cta608.SetMode{Mode: cta608.PopOn},
cta608.PAC{Row: 15, Indent: cta608.NoIndent, Pen: cta608.Pen{Color: cta608.White}},
cta608.Chars{Text: "HI"},
cta608.Command{Op: cta608.EOC},
}
// Schedule at 30 fps (cc_count 20), starting at wall-clock time 0.
s := schedule.NewScheduler(30)
s.Push(schedule.TimedTokens{TimeMS: 0, Tokens: tokens})
// Pull ten frames (33 ms apart); wrap each with carriage and recover the
// field-1 byte pairs, concatenating them across frames.
var field1 []byte
framesWithData := 0
for frame := 0; frame < 10; frame++ {
f := s.Frame(int64(frame) * 33)
nalu := carriage.FrameSEINALU(f.Field1, f.Field2, f.CCCount, carriage.CodecAVC)
got1, _, err := carriage.FieldPairs([][]byte{nalu}, carriage.CodecAVC)
if err != nil {
panic(err)
}
if len(got1) > 0 {
framesWithData++
}
field1 = append(field1, got1...)
}
// Parse the recovered pairs back into the token stream (Parse collapses the
// field-1 control-code doubling). Full Screen reconstruction awaits the
// cta608 Decoder.
back, err := cta608.Parse(field1, cta608.ParseOptions{ValidateParity: true})
if err != nil {
panic(err)
}
fmt.Printf("recovered %d field-1 pairs over %d frames\n", len(field1)/2, framesWithData)
for _, tok := range back {
fmt.Println(tok)
}
}
Output: recovered 7 field-1 pairs over 7 frames SetMode(pop-on) PAC(row=15 white) Chars("HI") Command(EOC)
Example (Srt) ¶
Example_srt shows the srt package as a thin, two-way serializer over the cue model (SPEC §8.2). It reads an SRT document into []cue.TimedCue — quantizing the inline <font color> to the nearest of 608's 8 colors and anchoring the position-less text to the bottom of the grid — then writes the cues straight back out. Styling survives (color as <font color>, italic as <i>); SRT carries no positioning, so none is emitted.
package main
import (
"fmt"
"strings"
"github.com/Eyevinn/go-608/cta608"
"github.com/Eyevinn/go-608/srt"
)
// srtCueText flattens a cue's Screen into a single plain string (runs joined in
// row then column order), enough to show what a cue carries without re-serializing
// its styling.
func srtCueText(s cta608.Screen) string {
var parts []string
for _, row := range s.Rows {
for _, run := range row.Runs {
parts = append(parts, run.Text)
}
}
return strings.Join(parts, "")
}
// Example_srt shows the srt package as a thin, two-way serializer over the cue
// model (SPEC §8.2). It reads an SRT document into []cue.TimedCue — quantizing the
// inline <font color> to the nearest of 608's 8 colors and anchoring the
// position-less text to the bottom of the grid — then writes the cues straight
// back out. Styling survives (color as <font color>, italic as <i>); SRT carries
// no positioning, so none is emitted.
func main() {
const doc = "1\n" +
"00:00:01,000 --> 00:00:03,000\n" +
`<font color="#ff0000">Red</font> alert` + "\n" +
"\n" +
"2\n" +
"00:00:04,000 --> 00:00:06,000\n" +
"Plain <i>caption</i>\n"
cues, err := srt.Read(strings.NewReader(doc))
if err != nil {
panic(err)
}
for _, c := range cues {
fmt.Printf("%v-%v %q\n", c.Start, c.End, srtCueText(c.Content))
}
fmt.Println("--- written back ---")
var out strings.Builder
if err := srt.Write(&out, cues); err != nil {
panic(err)
}
fmt.Print(out.String())
}
Output: 1s-3s "Red alert" 4s-6s "Plain caption" --- written back --- 1 00:00:01,000 --> 00:00:03,000 <font color="#ff0000">Red</font> alert 2 00:00:04,000 --> 00:00:06,000 Plain <i>caption</i>
Example (WebvttRead) ¶
Example_webvttRead shows the WebVTT -> cue direction. webvtt.Read parses the WEBVTT document into []cue.TimedCue, quantizing the STYLE-class color to the 608 palette and the line:/position: settings to the 15x32 grid (SPEC §8.2). All the 608 mapping lives in the cue package; webvtt only serializes.
const doc = `WEBVTT
STYLE
::cue(.green) { color: #00ff00; }
00:00:01.000 --> 00:00:03.000 line:100% position:0% align:start
<c.green>HELLO</c> <i>WORLD</i>
`
cues, err := webvtt.Read(strings.NewReader(doc))
if err != nil {
panic(err)
}
for _, c := range cues {
fmt.Printf("%v-%v\n", c.Start, c.End)
for _, row := range c.Content.Rows {
for _, r := range row.Runs {
fmt.Printf(" row=%d col=%d %q color=%s italic=%v\n",
row.Index, r.Column, r.Text, r.Pen.Color, r.Pen.Italic)
}
}
}
Output: 1s-3s row=15 col=0 "HELLO" color=green italic=false row=15 col=5 " " color=white italic=false row=15 col=6 "WORLD" color=white italic=true
Example (WebvttWrite) ¶
Example_webvttWrite shows the cue -> WebVTT direction. webvtt.Write serializes a cue list, emitting the WEBVTT header, a STYLE block for every color class used, and one positioned cue block per TimedCue (SPEC §8.2).
cues := []cue.TimedCue{{
Start: 1 * time.Second, End: 3 * time.Second,
Content: cta608.Screen{Rows: []cta608.Row{{
Index: 15, Displayed: true,
Runs: []cta608.Run{
{Column: 0, Text: "HELLO ", Pen: cta608.Pen{Color: cta608.White}},
{Column: 6, Text: "RED", Pen: cta608.Pen{Color: cta608.Red}},
},
}}},
}}
var buf bytes.Buffer
if err := webvtt.Write(&buf, cues); err != nil {
panic(err)
}
fmt.Print(buf.String())
Output: WEBVTT STYLE ::cue(.red) { color: #ff0000; } 1 00:00:01.000 --> 00:00:03.000 line:100% position:0% align:start HELLO <c.red>RED</c>