Repository navigation
Architecture
.NET library and tooling for parsing, filtering, converting, and restriping broadcast teletext and MXF formats.
Plain English overview -- what the system does, no code references.
libopx sits at the centre of a broadcast teletext workflow. It reads files in various broadcast formats, processes them through format-specific parsers, and outputs filtered, converted, or modified results. Three user-facing applications wrap the library: a CLI tool, a web GUI, and a desktop restriper.
flowchart LR
subgraph Sources["Input Formats"]
direction TB
VBI["VBI files"]
T42["T42 files"]
MXF["MXF files"]
TS["Transport streams"]
ANC["ANC data"]
end
LIB{{"libopx"}}
subgraph Apps["Applications"]
direction TB
CLI["CLI tool"]
WEB["Blazor GUI"]
DESK["Desktop restriper"]
end
subgraph Outputs["Output Formats"]
direction TB
OVBI["VBI"]
OT42["T42"]
RCWT["RCWT"]
STL["STL subtitles"]
RAW["Raw essence streams"]
RESTR["Restriped MXF"]
end
Sources --> LIB
LIB --> Apps
LIB --> Outputs
style Sources fill:#e8f5e9,stroke:#4CAF50
style Apps fill:#e3f2fd,stroke:#2196F3
style Outputs fill:#fff3e0,stroke:#FF9800
style LIB fill:#e94560,stroke:#c62828,color:#fff
Legend: Green = input sources, Blue = applications, Orange = outputs, Red = core library
There are four main data flows through the library: filtering (select specific teletext content), conversion (transform between formats), extraction (demux MXF streams), and restriping (modify MXF timecodes in-place).
flowchart LR
subgraph Filter["Filter Flow"]
direction TB
F1["Open file"] --> F2["Parse lines/packets"]
F2 --> F3["Apply magazine/row/page filters"]
F3 --> F4["Output filtered content"]
end
subgraph Convert["Convert Flow"]
direction TB
C1["Open input format"] --> C2["Parse to lines"]
C2 --> C3["Transform to target format"]
C3 --> C4["Write output file"]
end
subgraph Extract["Extract Flow"]
direction TB
E1["Open MXF"] --> E2["Parse KLV packets"]
E2 --> E3["Separate by key type"]
E3 --> E4["Write raw streams"]
end
subgraph Restripe["Restripe Flow"]
direction TB
R1["Open MXF read-write"] --> R2["Parse timecodes"]
R2 --> R3["Calculate new values"]
R3 --> R4["Seek back and overwrite"]
end
Filter ~~~ Convert
Extract ~~~ Restripe
style Filter fill:#e8f5e9,stroke:#4CAF50
style Convert fill:#e3f2fd,stroke:#2196F3
style Extract fill:#fff3e0,stroke:#FF9800
style Restripe fill:#f3e5f5,stroke:#9C27B0
Legend: Green = filter, Blue = convert, Orange = extract, Purple = restripe
When a user opens a file, the library detects the format, selects the right handler, applies any configured filters or transformations, and streams results lazily. No intermediate buffering -- data flows from parser to consumer in a single pass.
flowchart LR
OPEN["Open file"] --> DETECT["Detect format"]
DETECT --> HANDLER["Select handler"]
HANDLER --> PARSE["Stream parse"]
PARSE --> FILTER["Apply filters"]
FILTER --> TRANSFORM["Convert if needed"]
TRANSFORM --> OUTPUT["Yield to consumer"]
Legend: Each step passes data forward without buffering. The consumer pulls results lazily via enumeration.
The core data structures are Lines (individual teletext rows) and Packets (groups of lines with headers and timecodes). Timecodes track SMPTE position. Format handlers produce these structures from raw bytes.
erDiagram
FormatIO ||--o{ Line : "produces"
FormatIO ||--o{ Packet : "produces"
Packet ||--|{ Line : "contains"
Packet ||--|| Timecode : "has"
Line ||--o{ ColorSpan : "has"
Line }o--|| Timecode : "optional"
MXF ||--|{ Packet : "parses from KLV"
VBI ||--|{ Line : "parses from bytes"
T42 ||--|{ Line : "parses from bytes"
TS ||--|{ Line : "parses from PES"
Legend: FormatIO is the entry point. Lines carry teletext content; Packets group lines with timing information.
Technical view -- method names, variables, paths from the code.
The library is structured around a handler pattern. FormatIO is the public fluent API that delegates to internal format handlers via FormatRegistry. Each handler implements either ILineFormatHandler or IPacketFormatHandler.
flowchart LR
subgraph Public["Public API (FormatIO.cs)"]
direction TB
OPEN["FormatIO.Open()"]
FILTER["Filter()"]
CONVERT["ConvertTo()"]
PARSE_L["ParseLinesAsync()"]
PARSE_P["ParsePacketsAsync()"]
SAVE["SaveToAsync()"]
EXTRACT["ExtractToAsync()"]
RESTRIPE["Restripe()"]
end
subgraph Core["Core (lib/Core/)"]
direction TB
REGISTRY["FormatRegistry"]
BASE["FormatIOBase"]
CONVERTER["FormatConverter"]
OPTS["ParseOptions"]
end
subgraph Handlers["Handlers (lib/Handlers/)"]
direction TB
VH["VBIHandler"]
TH["T42Handler"]
AH["ANCHandler"]
MH["MXFHandler"]
TSH["TSHandler"]
end
subgraph Formats["Format Classes (lib/Formats/)"]
direction TB
VBI["VBI.cs"]
T42["T42.cs"]
ANC["ANC.cs"]
MXF["MXF.cs"]
TS["TS.cs"]
end
subgraph Models["Data Structures"]
direction TB
LINE["Line.cs"]
PKT["Packet.cs"]
TC["Timecode.cs"]
TCC["TimecodeComponent.cs"]
end
Public --> Core
Core --> Handlers
Handlers --> Formats
Formats --> Models
style Public fill:#e3f2fd,stroke:#2196F3
style Core fill:#e94560,stroke:#c62828,color:#fff
style Handlers fill:#fff3e0,stroke:#FF9800
style Formats fill:#e8f5e9,stroke:#4CAF50
style Models fill:#f3e5f5,stroke:#9C27B0
Legend: Blue = public API, Red = core routing, Orange = handlers, Green = format classes, Purple = data structures
Handler bypass pattern: T42Handler and VBIHandler set Line properties (Magazine, Row, Text) directly -- they do NOT call Line.ParseLine(). ANCHandler, MXFHandler, and TSHandler DO call ParseLine() / ParseLineAsync(). This is critical when adding new Line properties derived from T42 data.
flowchart LR
subgraph LineBased["Line-Based (VBI, T42)"]
direction TB
LB1["FormatIO.Open()"] --> LB2["FormatRegistry.GetHandler()"]
LB2 --> LB3["VBIHandler / T42Handler"]
LB3 --> LB4["Set Magazine, Row, Text directly"]
LB4 --> LB5["FilterHelpers.MatchesFilter()"]
LB5 --> LB6["yield Line"]
end
subgraph PacketBased["Packet-Based (MXF, TS, ANC)"]
direction TB
PB1["FormatIO.Open()"] --> PB2["FormatRegistry.GetHandler()"]
PB2 --> PB3["MXFHandler / TSHandler / ANCHandler"]
PB3 --> PB4["Line.ParseLine() called"]
PB4 --> PB5["FilterHelpers.MatchesFilter()"]
PB5 --> PB6["yield Packet with Lines"]
end
subgraph Convert["Conversion (FormatConverter)"]
direction TB
CV1["FormatConverter.VBIToT42()"] --> CV2["Strip VBI header bytes"]
CV3["FormatConverter.T42ToVBI()"] --> CV4["Add VBI framing"]
CV5["Line.ToRCWT()"] --> CV6["11-byte header + data"]
CV7["STLExporter.Export()"] --> CV8["BCD timecodes + GSI header"]
end
LineBased ~~~ PacketBased
PacketBased ~~~ Convert
style LineBased fill:#e8f5e9,stroke:#4CAF50
style PacketBased fill:#e3f2fd,stroke:#2196F3
style Convert fill:#fff3e0,stroke:#FF9800
Legend: Green = line-based path (bypass pattern), Blue = packet-based path (calls ParseLine), Orange = conversion utilities
The MXF restripe flow is the most complex path. It opens the file read-write, parses KLV packets looking for TimecodeComponent and System keys, calculates the offset from old to new start timecode, then seeks back in the stream to overwrite each timecode value.
flowchart TB
subgraph Init["Initialization"]
direction LR
I1["FormatIO.Open(path)"] --> I2["MXF constructor opens FileStream"]
I2 --> I3["IsValidMXFFile() checks 4-byte header"]
I3 --> I4["ReadMXFStartTimecode() scans first 128KB"]
end
subgraph Parse["KLV Parsing"]
direction LR
P1["Read 16-byte KLV key"] --> P2["ReadBerLength() for value size"]
P2 --> P3["Keys.GetKeyType() identifies packet"]
P3 --> P4["TimecodeComponent or System key?"]
end
subgraph Restripe["Timecode Rewrite"]
direction LR
R1["Calculate frame offset"] --> R2["Read current timecode bytes"]
R2 --> R3["Timecode.FromBytes() decode"]
R3 --> R4["Apply offset, encode new bytes"]
R4 --> R5["Stream.Seek() back"]
R5 --> R6["Write new bytes in-place"]
end
Init --> Parse
Parse --> Restripe
style Init fill:#e8f5e9,stroke:#4CAF50
style Parse fill:#e3f2fd,stroke:#2196F3
style Restripe fill:#f3e5f5,stroke:#9C27B0
Legend: Green = file open and validation, Blue = KLV packet parsing loop, Purple = in-place timecode modification
erDiagram
FormatIO {
Stream inputStream
Stream outputStream
Format inputFormat
Format outputFormat
ParseOptions options
}
Line {
int Magazine
int Row
string Text
byte[] Data
int SampleCoding
int SampleCount
int Length
Timecode LineTimecode
int LineNumber
}
Packet {
byte[] Header
Timecode Timecode
int LineCount
int Magazine
int[] Row
Line[] Lines
}
Timecode {
int Hours
int Minutes
int Seconds
int Frames
int Field
int Timebase
float FrameRate
bool DropFrame
int FrameNumber
}
ColorSpan {
int Start
int Length
TeletextColor Color
}
TimecodeComponent {
int StartTimecode
int RoundedTimecodeBase
int DropFrame
}
FormatIO ||--o{ Line : "ParseLines()"
FormatIO ||--o{ Packet : "ParsePackets()"
Packet ||--|{ Line : "Lines[]"
Packet ||--|| Timecode : "Timecode"
Line ||--o{ ColorSpan : "ColorSpans"
Line }o--|| Timecode : "LineTimecode"
Legend: FormatIO produces Lines or Packets depending on format type. Packets contain grouped Lines with a shared Timecode. Lines optionally carry ColorSpans for ANSI rendering.