examples

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 0 Imported by: 0

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>

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL