Skip to content

Architecture

Nathan Butler edited this page May 1, 2026 · 1 revision

libopx -- Architecture

.NET library and tooling for parsing, filtering, converting, and restriping broadcast teletext and MXF formats.


Simple View

Plain English overview -- what the system does, no code references.

1. System Architecture (Overview)

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
Loading

Legend: Green = input sources, Blue = applications, Orange = outputs, Red = core library

2. Core Data Flows (Overview)

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
Loading

Legend: Green = filter, Blue = convert, Orange = extract, Purple = restripe

3. Execution Flow (Overview)

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"]
Loading

Legend: Each step passes data forward without buffering. The consumer pulls results lazily via enumeration.

4. Data Model / File Schema (Overview)

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"
Loading

Legend: FormatIO is the entry point. Lines carry teletext content; Packets group lines with timing information.


Detailed View

Technical view -- method names, variables, paths from the code.

1. System Architecture (Implementation)

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
Loading

Legend: Blue = public API, Red = core routing, Orange = handlers, Green = format classes, Purple = data structures

2. Core Data Flows (Implementation)

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
Loading

Legend: Green = line-based path (bypass pattern), Blue = packet-based path (calls ParseLine), Orange = conversion utilities

3. Execution Flow (Implementation)

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
Loading

Legend: Green = file open and validation, Blue = KLV packet parsing loop, Purple = in-place timecode modification

4. Data Model / File Schema (Implementation)

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"
Loading

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.