moontsinspect

    Bounded offline MPEG-2 TS program structure and media timestamp inspection

    mpegts
    transport-stream
    media
    diagnostics
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    16 hours ago
    Downloads
    3

    #MoonTSInspect

    原创 MoonBit 离线 MPEG-2 Transport Stream 结构与媒体时钟检查库。面向录制/转封装流水线、媒体工具与 SDK 作者。输入保存的 TS 字节,输出节目清单、逐 PID 连续性问题、PCR 相对时钟与 PES 时间戳。

    开发状态:公开源码已发布;四后端验证由 GitHub Actions 执行,发布状态以 Release 和 CI 记录为准;尚未上传 MoonCakes。 本地测试或 CI 通过不代表比赛审核通过。

    源码仓库: https://github.com/xubowen1234/moontsinspect

    #三个真实用途

    1. 盘点节目:PAT → PMT → PCR/音视频 PID、stream_type、未知 descriptor。
    2. 定位损坏:删包/过量重复包、CRC 损坏、截断、TEI、加扰内容、同步丢失;给出偏移和 PID,不把合法计数器回绕或一次重复包误判为丢包。
    3. 分析媒体时间:PCR 27 MHz 相对分段与回绕;PES 33 位 PTS/DTS。正常 PTS 显示重排序不判为错误,无网络到达时间则不声称测量网络抖动。

    #本地运行

    已测试工具链:Moon CLI 0.1.20260904 / moonc v0.10.12+1634b282e(精确版本见 toolchain.json;旧版格式器输出不同);CLI 使用 Node.js,交叉测试额外需要 ffprobe。不要将下面的模块名误当成已经可从 MoonCakes 安装。

    从公开仓库克隆后,在仓库根目录执行。先使用 node scripts/check-toolchain.cjs 检查 toolchain.json 中的工具版本;如使用隔离 SDK,仅在当前终端设置 MOON_HOME 和 PATH,不需要修改全局 SDK。

    git clone https://github.com/xubowen1234/moontsinspect.git cd moontsinspect node scripts/check-toolchain.cjs

    然后执行:

    moon test --target wasm-gc moon run examples/inventory --target wasm-gc moon run cmd/inspect --target js -- --summary fixtures/synthetic.ts node scripts/cli-smoke.cjs node scripts/crosscheck.cjs

    样本清单:节目 7;PMT PID 4096;PCR/视频 PID 256(MPEG-2 video);音频 PID 257(MPEG audio)。真实 fixture 来自 FFmpeg 合成色块和正弦波,不是第三方录影。交叉脚本将清单与首个 PTS/DTS 和 ffprobe 比较,并实测故障注入。

    #库 API

    let inspector = @ts.Inspector::new().unwrap()
    inspector.feed(bytes, fn(event) { println(@ts.event_json(event).stringify()) })
    inspector.finish(fn(event) { println(@ts.event_json(event).stringify()) })
    let inventory = inspector.program_maps()

    在调用方 moon.pkg 中 import xubowen1234/moontsinspect 并命名为 @ts;完整、可运行的调用方见 examples/inventory。底层也开放 parse_packet、parse_section、parse_pat、parse_pmt、parse_pes_header 及独立状态机。生成的 公共接口 列出实际导出,不用 README 描述代替 API。

    #验收

    PowerShell 一键执行:./scripts/verify-local.ps1。48 个测试在 wasm-gc、wasm、JS 三后端各自通过;native 仅静态检查通过,运行验证留给 CI。实际执行状态及发布前缺口见 验收说明

    #CLI

    moon run cmd/inspect --target js -- recording.ts moon run cmd/inspect --target js -- --summary recording.ts moon run cmd/inspect --target js -- --recover recording.ts

    默认输出 NDJSON 事件;--summary 仅输出问题和最终清单。退出码:0=未发现已实现检查项的问题;1=诊断问题;2=参数/文件 IO 失败。0 不等于所有标准合规。大整数偏移/时钟输出为十进制字符串,见 输出契约

    默认严格对齐;--recover 每次最多跳过 65536 字节,连续验证三个完整 packet 后再对齐。失步会清除拼接基线并输出诊断;恢复后仍以退出码 1 提醒输入曾有损坏。末尾不足三包的未确认对齐不猜测接受。

    #范围与限制

    • 仅 188 字节 TS;不是 192/204 字节变体、M3U8/HLS 下载器、PCAP 分析器、播放器或通用 framing 库。
    • PAT 多节表原子激活;PMT 仅规范的单节。只对声明为已支持 PES 类型的流解析 PES,其他 stream_type 保留在清单中而不猜测。
    • Adaptation extension、非时间戳 PES optional 字段、未知 descriptor 保留但不做完整语义验证
    • 不解密、不解码音视频、不执行广播合规认证;不推断缺失的外部 HLS 初始化信息。
    • PCR 使用最近模差,超过半个回绕周期的采样间隙无法确定实际时钟绕回次数。
    • 对同步前/丢包后的字节不臆造节目表;EOF 的 missing 诊断是“此输入中未观察到”,不是对整个原始频道的断言。

    License: MIT。

    Adaptation

    pub struct Adaptation {
    discontinuity : Bool
    random_access : Bool
    pcr : Int64?
    opcr : Int64?
    splice_countdown : Int?
    private_data : Bytes
    extension : Bytes
    } derive(Eq,
    Debug
    )

    Parsed optional adaptation fields. Extension bytes are opaque, not semantically validated.

    ClockSample

    pub struct ClockSample {
    segment : Int
    relative_ticks : Int64
    delta_ticks : Int64
    wrapped : Bool
    } derive(Eq,
    Debug
    )

    Continuity

    pub struct Continuity {
    // private fields
    }

    One slot per legal PID. Packet history is bounded by the TS PID namespace.

    Continuity::new

    fn Continuity::new() -> Continuity

    Continuity::observe

    fn Continuity::observe(self : Continuity, packet : Packet) -> Result[ContinuityEvent, Diagnostic]

    Check continuity independently of PSI/PES parsing. At most one consecutive duplicate is legal.

    Continuity::reset_pid

    fn Continuity::reset_pid(self : Continuity, pid : Int) -> Result[Unit, Diagnostic]

    Explicitly discard one PID's baseline after an external loss or seek.

    ContinuityEvent

    pub(all) enum ContinuityEvent {
    First
    Continuous
    Duplicate
    Discontinuity
    IgnoredNull
    TransportError
    Gap(expected~ : Int, actual~ : Int)
    } derive(Eq,
    Debug
    )

    Descriptor

    pub struct Descriptor {
    tag : Int
    data : Bytes
    } derive(Eq,
    Debug
    )

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    offset : Int64
    pid : Int?
    } derive(Eq,
    Debug
    )

    A positioned, machine-readable failure. Offset is an absolute input byte offset.

    ElementaryStream

    pub struct ElementaryStream {
    stream_type : Int
    pid : Int
    descriptors : Array[Descriptor]
    } derive(Eq,
    Debug
    )

    InspectEvent

    pub(all) enum InspectEvent {
    Issue(Diagnostic)
    ProgramsChanged(Array[ProgramRef])
    ProgramMapChanged(Pmt)
    MediaClock(pid~ : Int, offset~ : Int64, ClockSample)
    PesTimestamp(pid~ : Int, PesHeader)
    } derive(Eq,
    Debug
    )

    Inspector

    pub struct Inspector {
    // private fields
    }

    Bounded offline inspection session. Issues are streamed, never accumulated internally.

    Inspector::feed

    fn Inspector::feed(self : Inspector, bytes : Bytes, emit : (InspectEvent) -> Unit) -> Unit

    Inspector::finish

    fn Inspector::finish(self : Inspector, emit : (InspectEvent) -> Unit) -> Unit

    Missing PAT/PMT is reported at EOF, not prematurely while acquiring a stream.

    Inspector::new

    fn Inspector::new(max_programs? : Int, recover_scan_limit? : Int) -> Result[Inspector, Diagnostic]

    Inspector::packet_count

    fn Inspector::packet_count(self : Inspector) -> Int64

    Inspector::program_maps

    fn Inspector::program_maps(self : Inspector) -> Array[Pmt]

    Inspector::programs

    fn Inspector::programs(self : Inspector) -> Array[ProgramRef]

    Returns independent mutable arrays; callers cannot change the inspector's active tables.

    Inspector::summary_json

    fn Inspector::summary_json(self : Inspector) -> Json

    LongSection

    pub struct LongSection {
    table_id : Int
    extension : Int
    version : Int
    current : Bool
    number : Int
    last_number : Int
    body : Bytes
    bytes : Bytes
    offset : Int64
    // private fields
    } derive(Eq,
    Debug
    )

    Packet

    pub struct Packet {
    offset : Int64
    pid : Int
    continuity : Int
    payload_start : Bool
    transport_error : Bool
    scrambling : Int
    adaptation_control : Int
    payload : Bytes
    raw : Bytes
    } derive(Eq,
    Debug
    )

    Parsed 188-byte TS packet. The owned input and payload cannot alias caller mutation.

    Packet::adaptation

    fn Packet::adaptation(self : Packet) -> Result[Adaptation?, Diagnostic]

    Validate lengths, PCR/OPCR encoding and outer stuffing; extension payload is preserved.

    PacketReader

    pub struct PacketReader {
    // private fields
    }

    Strict incremental packet reader. Retains at most 187 bytes between calls. Diagnostics terminate this reader; create another reader to establish a new alignment.

    PacketReader::buffered

    fn PacketReader::buffered(self : PacketReader) -> Int

    PacketReader::feed

    fn PacketReader::feed(self : PacketReader, chunk : Bytes, emit : (Result[Packet, Diagnostic]) -> Unit) -> Unit

    Synchronous callback avoids accumulating one result per packet in library memory.

    PacketReader::finish

    fn PacketReader::finish(self : PacketReader) -> Result[Unit, Diagnostic]

    Finish once: a partial final packet is diagnosed instead of silently dropped.

    PacketReader::new

    Pat

    pub struct Pat {
    section : LongSection
    programs : Array[ProgramRef]
    network_pid : Int?
    } derive(Eq,
    Debug
    )

    PcrClock

    pub struct PcrClock {
    // private fields
    }

    Relative PCR clock in 27 MHz ticks. No arrival-time or network jitter interpretation.

    PcrClock::new

    fn PcrClock::new() -> PcrClock

    PcrClock::observe

    fn PcrClock::observe(self : PcrClock, ticks : Int64, discontinuity? : Bool) -> Result[ClockSample, Diagnostic]

    Uses nearest signed modular difference. Gaps of half a wrap period or more are ambiguous. Negative deltas start a fresh segment and are returned as clock_backward rather than hidden.

    PcrClock::reset

    fn PcrClock::reset(self : PcrClock) -> Unit

    PesAssembler

    pub struct PesAssembler {
    // private fields
    }

    Single elementary PID header reassembler. Never buffers elementary media payload.

    PesAssembler::buffered

    fn PesAssembler::buffered(self : PesAssembler) -> Int

    PesAssembler::feed

    fn PesAssembler::feed(self : PesAssembler, payload : Bytes, start : Bool, offset : Int64, emit : (Result[PesHeader, Diagnostic]) -> Unit) -> Unit

    Caller resets on continuity loss, skips scrambled/TEI data, and removes duplicate packets.

    PesAssembler::finish

    fn PesAssembler::finish(self : PesAssembler) -> Result[Unit, Diagnostic]

    PesAssembler::new

    PesAssembler::reset

    fn PesAssembler::reset(self : PesAssembler) -> Unit

    PesHeader

    pub struct PesHeader {
    stream_id : Int
    packet_length : Int
    pts : Int64?
    dts : Int64?
    header_bytes : Int
    opaque_optional : Bytes
    offset : Int64
    } derive(Eq,
    Debug
    )

    Pmt

    pub struct Pmt {
    section : LongSection
    pcr_pid : Int
    descriptors : Array[Descriptor]
    streams : Array[ElementaryStream]
    } derive(Eq,
    Debug
    )

    ProgramRef

    pub struct ProgramRef {
    number : Int
    pmt_pid : Int
    } derive(Eq,
    Debug
    )

    RecoveryReader

    pub struct RecoveryReader {
    // private fields
    }

    Opt-in resynchronizing reader. Recovery requires THREE complete structurally valid packets. A 564-byte window bounds memory; scan_limit bounds skipped bytes per loss episode.

    RecoveryReader::buffered

    fn RecoveryReader::buffered(self : RecoveryReader) -> Int

    RecoveryReader::feed

    fn RecoveryReader::feed(self : RecoveryReader, bytes : Bytes, emit : (Result[Packet, Diagnostic]) -> Unit) -> Unit

    RecoveryReader::finish

    fn RecoveryReader::finish(self : RecoveryReader) -> Result[Unit, Diagnostic]

    RecoveryReader::new

    fn RecoveryReader::new(scan_limit : Int) -> Result[RecoveryReader, Diagnostic]

    SectionAssembler

    pub struct SectionAssembler {
    // private fields
    }

    Bounded single-PID long-PSI assembler. Caller must remove duplicates and reset on loss.

    SectionAssembler::buffered

    fn SectionAssembler::buffered(self : SectionAssembler) -> Int

    SectionAssembler::feed

    fn SectionAssembler::feed(self : SectionAssembler, payload : Bytes, start : Bool, offset : Int64, emit : (Result[LongSection, Diagnostic]) -> Unit) -> Unit

    offset points to the first payload byte, including pointer_field when start is true. Empty or non-start payloads during initial acquisition are ignored.

    SectionAssembler::finish

    fn SectionAssembler::finish(self : SectionAssembler) -> Result[Unit, Diagnostic]

    SectionAssembler::new

    fn SectionAssembler::new(pid : Int) -> Result[SectionAssembler, Diagnostic]

    SectionAssembler::reset

    fn SectionAssembler::reset(self : SectionAssembler) -> Unit

    TableCollector

    pub struct TableCollector {
    // private fields
    }

    Atomic long-table version collector. One active and one candidate version, at most 512 sections. Table-specific semantics must be checked with parse_pat/parse_pmt before accept.

    TableCollector::accept

    fn TableCollector::accept(self : TableCollector, section : LongSection) -> Result[Array[LongSection]?, Diagnostic]

    A new version is activated only after all its numbered sections arrive; next tables are ignored.

    TableCollector::active

    TableCollector::discard_pending

    fn TableCollector::discard_pending(self : TableCollector) -> Unit

    TableCollector::new

    fn TableCollector::new(table_id : Int) -> TableCollector

    event_json

    fn event_json(event : InspectEvent) -> Json

    Version 1 NDJSON event schema. Int64 byte offsets and clock ticks are decimal strings.

    mpeg_crc32

    fn mpeg_crc32(bytes : Bytes) -> UInt

    MPEG-2 PSI CRC-32: poly 04C11DB7, init FFFFFFFF, non-reflected, xorout 0.

    parse_packet

    fn parse_packet(input : Bytes, offset? : Int64) -> Result[Packet, Diagnostic]

    Parse one complete packet, without guessing alignment or discarding trailing data.

    parse_pat

    fn parse_pat(section : LongSection) -> Result[Pat, Diagnostic]

    parse_pes_header

    fn parse_pes_header(bytes : Bytes, offset? : Int64) -> Result[PesHeader, Diagnostic]

    Parse the bounded PES header prefix; elementary payload is neither consumed nor decoded. Non-timestamp optional fields are retained as opaque bytes, not semantically validated.

    parse_pmt

    fn parse_pmt(section : LongSection) -> Result[Pmt, Diagnostic]

    parse_section

    fn parse_section(bytes : Bytes, offset? : Int64, pid? : Int) -> Result[LongSection, Diagnostic]

    Long PSI section limited to 1024 total bytes (PAT/PMT profile).