←Back to Technical Sheets
In-Depth Technical Sheet

The TSDuck Cookbook
Verified Recipes for Analysing, Editing and Testing Transport Streams

TSDuck is the tool that turns everything in the other sheets from theory into something you can measure. This is a working cookbook: every command below was run on TSDuck 3.44-4676 against a purpose-built demo stream, and every output shown is real. Where a command's documented syntax differs from what people usually assume, the difference is called out — because three of these recipes did not work the first time.

  • Analyse
  • Tables & XML
  • Edit & remux
  • PCR & continuity
  • IP, SRT, failover

1. Scope, version, and how these recipes were verified

Every command on this page was executed. Nothing is transcribed from documentation and nothing is reconstructed from memory. The environment was TSDuck 3.44-4676 — the current release at the time of writing, published in May 2026 — installed from Homebrew on macOS. The same commands behave identically on Linux; only the installation step differs.

Why this matters more than usual for a cookbook. TSDuck has a large option surface and several plugins accept option names that look interchangeable but are not. While building this page, several recipes written from reasonable assumptions failed outright, one silently destroyed its own input file, and two more are traps in the SAT>IP query rather than in TSDuck itself. All of them are documented in section 20 rather than quietly corrected, because the failure modes are more instructive than the working commands.

The test material is a synthetic multi-programme transport stream built with TSDuck itself — no captured broadcast content, no operator data. Section 9 gives the full recipe for building it, which makes every other recipe on this page reproducible from nothing but a TSDuck install.

PropertyValue
Services3 — one AVC HD, one HEVC UHD, one MPEG-2 SD
PIDs13 total, 3 carrying PCR, 0 unreferenced
Bitrate~38 Mb/s, PCR-consistent
Length40,000 packets — about 1 second
SignallingPAT, three PMTs, SDT with service and provider names

2. The one idea that makes TSDuck click

TSDuck ships 43 separate commands, which makes it look sprawling. It is not. One command — tsp, the transport stream processor — does almost everything, and it has exactly one shape: an input plugin, a chain of packet processing plugins, and an output plugin.

tsp -I file input.ts -P plugin1 -P plugin2 -P plugin3 -O file output.ts

Packets flow left to right. Each -P stage sees the stream as the previous stage left it. That is the whole model. The other 42 commands are either convenience wrappers around a common pipeline (tsanalyze is close to tsp -P analyze -O drop) or standalone utilities that do not process a stream at all (tstabcomp compiles table XML; tscrc32 computes a checksum).

Two consequences worth internalising early:

The plugin inventory in 3.44 breaks down as follows, and knowing the shape of it saves a lot of searching.

KindExamples
Input (-I)file, ip, srt, rist, hls, http, dvb, pcap, craft, null, fork, memory
Output (-O)file, ip, srt, rist, hls, http, play, drop, fork, memory, vatek
Analyse, never modifyanalyze, history, stats, pcrverify, pcrextract, pes, tables, psi, iat, influx
Select and dropfilter, zap, svremove, rmorphan, slice, until, skip, reduce, limit
Rewrite signallingpat, pmt, sdt, nit, svrename, tsrename, inject, sections, eitinject
Timingpcradjust, pcrbitrate, pcredit, pcrcopy, regulate, timeshift, pidshift, timeref
Encapsulationt2mi, encap, feed, mpe, mpeextract, nip, flute, dsmcc
Conditional accessscrambler, descrambler-adjacent tooling, plus standalone tsecmg, tsemmg, tsgenecm, tstestecmg
Deliberately break thingsfuzz, craft, pattern, trigger, trace
The four SimulCrypt commands are worth noticing. tsecmg and tsemmg are working ECMG and EMMG simulators, and tstestecmg is a conformance tester for a vendor's ECMG. If you are integrating a CA system against the interfaces described in the conditional access sheet, these let you exercise the head-end side before the vendor equipment arrives.

3. Installing

TSDuck is packaged for most platforms and the 3.44 release added binaries for s390x, RISC-V 64-bit and PowerPC alongside the usual targets.

# macOS
brew install tsduck

# Debian / Ubuntu — download the .deb for your release from tsduck.io, then
sudo dpkg -i tsduck_*.deb

# confirm what you actually have
tsversion

On this system that reports 3.44-4676. Check it before following any cookbook, including this one — option names do change between major versions, and a recipe that assumes the wrong version fails in ways that look like your stream is broken rather than your command.

Hardware tuner support is a build-time decision. The dvb input plugin, and the tslsdvb and tsscan commands, only do anything on a build with DVB device support and a tuner present. On a Homebrew macOS install they exist but have nothing to talk to. This is worth knowing before spending time debugging why tslsdvb lists no devices.

4. First look at an unknown stream

One command answers most of the questions you have about a file you have just been handed.

tsanalyze demo.ts

The real output on the demo stream, abbreviated to the header:

FieldValueWhat it tells you
Transport Stream Id0x0064 (100)Identity of the multiplex, from the PAT
Services3How many programmes are actually signalled
PID's Total / Clear / Scrambled13 / 13 / 0Whether anything is encrypted, before you waste time on a CAM
With PCR's3One per service here — the expected shape
Unreferenced0The single most useful number. Non-zero means PIDs carry data that no PMT declares
With invalid sync / transport error0 / 0Physical-layer integrity of the file itself
Estimated based on PCR's37,997,443 b/sDerived from clock, not from file size — so it is the real rate
Broadcast time1 secSanity check that you captured what you meant to

The per-service breakdown follows, and this is what you show someone who asks "what is in this multiplex":

|  Srv Id  Service Name                            Access          Bitrate  |
|  0x0101  DEMO HD ONE ......................... C   13,768,373 b/s  |
|  0x0102  DEMO UHD TWO ........................ C    9,199,181 b/s  |
|  0x0103  DEMO SD THREE ....................... C    7,276,510 b/s  |

Two shortcuts are worth committing to memory, because they compose into shell pipelines where the full report does not:

tsanalyze --service-list demo.ts    # → 257 258 259
tsanalyze --pid-list demo.ts        # → 0 17 256 273 274 275 512 529 530 768 785 786 8191

Note that --service-list gives decimal service IDs while the report shows hex. 257 is 0x0101. And 8191 in the PID list is 0x1FFF, the null PID — stuffing, always present in a constant-bitrate stream.

For bitrate alone there is a dedicated command, which is faster than parsing the report:

$ tsbitrate demo.ts
TS bitrate: 37,997,415 b/s (188-byte), 41,231,238 b/s (204-byte)

The two figures are the same stream measured with and without the 16 bytes of Reed-Solomon parity that a satellite carrier adds. Quote the 204-byte number when you are sizing a transponder and the 188-byte number when you are sizing an IP link.

5. Analysing a live stream over HTTP

Everything above operates on a file, which is the easy case. In practice the stream you need to analyse is live, arriving from a SAT>IP server over HTTP, and the first thing you want is a service inventory. This is the single most-used command in day-to-day operation.

tsp -I http "http://sat-ip.example:8875/?freq=12606&sr=35300&pol=V&msys=dvbs2&mtype=8psk&fec=23&isi=5&plsc=131070&plsm=gold&t2mi_pid=auto&t2mi_plp=all&pids=all" \
    -P analyze \
    -P until --seconds 5 \
    -O drop
Quote the URL. This is the most common mistake by a wide margin, and on bash it fails silently and wrongly.

A SAT>IP URL is full of & characters, and & is the shell's background-job operator. Unquoted, the shell tears the URL apart at the first ampersand. Verified behaviour:

# bash — silently wrong
$ tsp -I http http://sat-ip.example:8875/?freq=12606&sr=35300&pol=V&msys=dvbs2&pids=all

argv[3] = [http://sat-ip.example:8875/?freq=12606]    ← everything after & is gone
+ sr=35300                                 ← became a shell variable
+ pol=V                                     ← became a shell variable
...

Two things go wrong at once. The tuning parameters vanish, so the server receives a request with a frequency and nothing else — no symbol rate, no polarisation, no modulation — and cannot tune. And because the first & backgrounds the command, your prompt returns immediately, so it looks as though tsp exited cleanly. The symptom presents as a tuning or signal fault when the problem never left your shell.

zsh behaves differently, and better: it refuses with parse error near '&', and a bare ? in the URL also triggers no matches found because zsh globs it. So the same unquoted command is a loud error on zsh and a silent wrong answer on bash — which is exactly why "it works on my machine" reports diverge between colleagues.

Either quote style protects the ampersands. Single quotes are the safer habit, because inside double quotes a $ in the URL would still be expanded by the shell:

tsp -I http 'http://sat-ip.example:8875/?freq=12606&sr=35300&pol=V&pids=all'  # safest
tsp -I http "http://sat-ip.example:8875/?freq=12606&sr=35300&pol=V&pids=all"  # fine unless the URL contains $

5.1 What the report tells you about a live multiplex

Real output from a DVB-S2 multistream transponder carrying a national public broadcaster's services, abbreviated to the header and the service list:

FieldValueWhat to make of it
Services19The inventory you came for
PID's Total / Clear / Scrambled70 / 70 / 0Entirely free-to-air — no CAM needed for any of it
With PCR's12Fewer than the 19 services, so several services share a PCR PID. Normal when one encoder feeds several services, and worth knowing before you split the mux
Unreferenced0Every PID is declared by a PMT. Clean signalling
Invalid sync / transport error0 / 0The carrier and the IP path are both delivering intact packets
Estimated based on PCR's36,714,356 b/sDerived from the stream's own clock
Selected reference bitrate36,714,356 b/sAgrees with the PCR-derived figure — the healthy case. A divergence between these two rows is the signal worth chasing, because it means the rate the input reports and the rate the clock implies disagree
Broadcast time4 secAsked for --seconds 5. See 5.4

Three readings from the service list that generalise to any multiplex:

5.2 The SAT>IP query parameters

The URL is the SAT>IP tuning API. Each parameter maps onto something in the other sheets on this shelf.

ParameterMeaning
freq, polTransponder frequency in MHz and polarisation, H or V
srSymbol rate in ksym/s. Feed it to the bitrate calculator with the modulation and code rate below to predict the payload this carrier can hold
msys, mtype, fecDelivery system, modulation and code rate — dvbs2, 8psk, 23 is 8PSK 2/3, which the C/N reference puts at 6,62 dB ideal Es/N0
isiInput Stream Identifier. Its presence means this is a multistream carrier: several independent transport streams share one physical carrier, and this selects one of them. Omit it on a multistream carrier and you get nothing usable
plsc, plsmPhysical Layer Scrambling code and mode (gold, root). Required alongside isi to lock a multistream carrier. Not conditional access — this is physical-layer, and it protects nothing; it exists so multiple streams can coexist
t2mi_pid, t2mi_plpDe-encapsulate T2-MI server-side and select the Physical Layer Pipe. auto finds the T2-MI PID; all takes every PLP. See section 17 of the MPEG-TS sheet
pidsall for the full MPTS, or an explicit list to reduce traffic. Client-controlled per session — nothing is filtered for you unless you ask
Why isi and plsc travelling together matters. A multistream carrier cannot be locked by frequency and symbol rate alone. If a transponder appears in a satellite directory with a PLS code listed, treat the stream identifier and the PLS pair as part of the tuning parameters, not as optional extras — a request missing them locks nothing, and the failure looks identical to a signal problem.

5.3 What this one request actually does

Those parameters are not an arbitrary list — read together they describe a five-stage pipeline, and this transponder happens to exercise every stage. It is the clearest single example of how raw BBFrame reconstruction and T2-MI de-encapsulation fit together in SAT>IP Servers Pro.

1. Tune the carrier    freq=12606 pol=V msys=dvbs2 mtype=8psk fec=23 sr=35300
2. Select the stream   isi=5 — one of several sharing the carrier
3. Descramble PL     plsm=gold plsc=131070 — physical layer, not conditional access
4. Rebuild the outer TS  bbframe=1 — in software, losslessly
5. Extract the inner TS  t2mi_pid=auto t2mi_plp=all → the 19 services

Stage 4 is the one that needs explaining, because it exists to work around a hardware limitation rather than to implement a standard.

Why raw BBFrame mode exists. On a multistream carrier the demodulator normally de-encapsulates MIS to transport stream in hardware. For a bursty T2-MI feed that hardware path drops packets — typically around 9 %. Nine percent sounds survivable until you remember what the T2-MI CRC covers: header, payload and padding, across the whole packet. Lose fragments at that rate and you get near-total T2-MI CRC failure and an undecodable inner multiplex while the RF measures perfect — no errored packets, good C/N, nothing wrong with the carrier. It is a genuinely confusing failure, because every RF indicator says the link is healthy.

bbframe=1 sidesteps it. On STiD135-based tuners with the raw-bbframe driver patch, the demodulator is told to emit raw DVB-S2 BBFrames instead of de-encapsulating them, and SAT>IP Servers Pro reconstructs the outer transport stream losslessly in software before handing it to the T2-MI demux. The hardware stops doing the thing it does badly.

DetailWhy it matters
Sets DTV_STREAM_ID bit 0x40000000, per tuneRaw-bbframe mode is requested for one tune, not for the card. The same adapter keeps serving ordinary transport-stream transponders concurrently
Falls back silentlyA driver that does not understand the flag reverts to normal tuning. So bbframe=1 is safe to leave in a URL, but its absence of effect is also silent — if CRC failures persist, confirm the driver patch is actually present rather than assuming the parameter did something
Pair it with t2mi_pid=The reconstructed stream is the outer TS. Without a T2-MI PID to demux, you have solved the loss problem and still not reached the services
t2mi_pid=auto tries 4096, then 4095Two conventional values. Give the number explicitly when you know it and want deterministic behaviour
t2mi_plp=all merges every PLPCorrect when signalling lives in a common PLP and content in others — which is why the example uses it rather than plp=0. auto takes the first PLP carrying data, which may not be the one you want
The outer pids= does not filter the inner TSOnce t2mi_pid is set the output is the inner stream, and inner PID selection is a separate parameter. Filtering the outer list will not reduce what you receive
The trap that costs an afternoon: plsm takes a name, not a number. Write plsm=gold&plsc=131070. A numeric plsm is interpreted as root mode, which then re-derives the code — so the tuner searches for a different scrambling sequence than the one you meant and never locks. The failure looks exactly like a wrong PLS code or a weak signal, and the parameter you would most naturally suspect is the one that is right.

This is also why the analysis at the top of this section is worth running as an acceptance test rather than only when something breaks. Zero invalid sync, zero transport errors and zero unreferenced PIDs across a five-second window is positive evidence that all five stages are working — the carrier locked, the right stream was selected, the physical layer descrambled, the outer TS reconstructed without loss, and the inner multiplex de-encapsulated cleanly. Any one of those failing shows up in the report rather than in a complaint from a customer.

5.4 Bounding a live capture

A live input never ends, so an unbounded tsp runs until you interrupt it — and analyze prints its report only at termination. Two plugins make it terminate on purpose.

OptionEffect
-P until --seconds 5Stops five seconds after the first packet arrives. This is wall-clock time by default
-P until --pcr-basedMeasures the interval from the stream's own PCR instead of the wall clock
-P until --packets NA fixed packet count — deterministic, and the better choice when comparing two runs
-O dropDiscards the stream. Do not omit this. Without an output plugin the packets go to standard output and flood your terminal with binary
Why --seconds 5 reported a broadcast time of 4 seconds. They measure different things. until --seconds counts five seconds of wall clock from the first packet; the broadcast time in the report is derived from the stream's own clock across the packets that actually arrived. Connection setup, the server's own tuning delay before it starts sending, and the difference between wall-clock and PCR-derived duration all land in that gap. Nothing was lost. If you need the two to agree, use --pcr-based or count packets instead of seconds.

5.5 Getting service names right

Service names on European satellites are frequently signalled with an encoding the standard does not strictly permit, and the result is mangled accented characters. TSDuck has a shortcut for exactly this:

tsp -I http '…' -P analyze --europe -P until --seconds 5 -O drop

--europe is a synonym for --dvb --default-charset ISO-8859-15, described in the plugin's own help as a handy shortcut for commonly incorrect signalling on some European satellites. If service names come back with question marks or wrong accents, try it before assuming the broadcaster's signalling is broken. Equivalent presets exist for other regions — --japan, --brazil, --china, --atsc, --isdb, --abnt.

5.6 Running it continuously

For monitoring rather than a one-off inventory, drop until and let analyze emit reports on a schedule.

# a fresh report file every 60 seconds, each one independent
tsp -I http '…' \
    -P analyze --europe --interval 60 --output-file /var/log/mux.txt --multiple-files \
    -O drop

# JSON straight to a collector instead of files
tsp -I http '…' -P analyze --interval 60 --json-udp 10.0.0.5:9000 -O drop
OptionEffect
--interval NProduce a new report every N seconds. The analysis context is reset each time, so each report is fully independent — a per-interval snapshot, not a running total
--cumulativeKeep accumulating instead of resetting. Use when you want totals since start rather than per-interval figures
--multiple-filesWith --interval and --output-file, write each report to its own file rather than overwriting
--error-analysisReport on errors specifically rather than producing the full inventory
--json-udp, --json-tcpShip each report as JSON to a collector — the path to a dashboard without parsing boxed text

For an unattended monitor, the HTTP input's own resilience options matter as much as the analysis:

OptionEffect
--infiniteReconnect and keep reading indefinitely instead of stopping at end of stream
--reconnect-delay msHow long to wait before retrying a dropped connection
--receive-timeout msTreat silence longer than this as a failure — the option that turns a stalled connection into a detectable event rather than a hang
--connection-timeout msBound the initial connect attempt
--ignore-errorsContinue past HTTP errors rather than terminating
--headers, --user-agentSet request headers, occasionally needed by a server that filters on them
A note on analyze versus tsanalyze. They produce the same report, but -P analyze is a pipeline stage, so it can sit anywhere in the chain and measure the stream at that point. Putting it before a zap and again after it tells you what the filtering actually removed. tsanalyze is the convenience command for the common case of measuring an input and nothing else. Order matters here as everywhere in tsp: in the command at the top of this section analyze precedes until, so it counts every packet that flows before termination.

6. Analysis a script can read

The boxed report is for humans. For monitoring, two other formats exist and both are stable enough to build on.

tsanalyze --normalized demo.ts    # colon-separated key=value records
tsanalyze --json demo.ts          # JSON to standard output

The normalized form is one record per line, each beginning with a record type. Extracting a single field is a one-liner, which makes it the better choice inside a shell-based check:

# packets and discontinuities for PID 273, from the normalized output
tsanalyze --normalized demo.ts | grep '^pid:.*pid=273:' | tr ':' '\n' \
  | grep -E '^(packets|discontinuities)='

packets=10000
discontinuities=0
A trap that cost real time while writing this page. tsanalyze --json writes JSON to standard output, but several other TSDuck commands spell the equivalent option --json-output file-name, which takes a filename argument. Because TSDuck accepts unambiguous option abbreviations, writing tstables --json demo.ts is resolved as --json-output demo.ts — and your input file is overwritten with the JSON result. In this case that was a two-byte empty array, and every subsequent command in the session reported an empty stream. Prefer the long form, and never let the output filename position be occupied by your input.

7. Reading the signalling

tstables collects PSI/SI and prints it decoded. Select by PID or by table id, and cap the collection so it terminates on a live input.

$ tstables --pid 0 --max-tables 1 demo.ts

* PAT, TID 0x00 (0), PID 0x0000 (0)
  Version: 1, sections: 1, total size: 28 bytes
  - Section 0:
    TS id:     100 (0x0064)
    NIT:        0 (0x0000)  PID:   16 (0x0010)
    Program:   257 (0x0101)  PID:  256 (0x0100)
    Program:   258 (0x0102)  PID:  512 (0x0200)
    Program:   259 (0x0103)  PID:  768 (0x0300)

Selecting by table id rather than PID is usually what you want, because it finds the table wherever it lives:

tstables --tid 0x00 demo.ts    # PAT
tstables --tid 0x02 demo.ts    # every PMT
tstables --tid 0x42 demo.ts    # SDT actual
tstables --tid 0x01 demo.ts    # CAT — see the conditional access sheet

A PMT from the demo stream shows why the PSI/SI sheet insists that stream type and codec are different questions:

* PMT, TID 0x02 (2), PID 0x0200 (512)
    Program: 0x0102 (258), PCR PID: 0x0211 (529)
    Elementary stream: type 0x24 (HEVC video), PID: 0x0211 (529)
    Elementary stream: type 0x06 (MPEG-2 PES private data), PID: 0x0212 (530)
    - Descriptor 0: AC-3 (0x6A, 106), 1 bytes

PID 530 has stream_type 0x06 — "private data", which says nothing. The AC-3 descriptor is what identifies it as audio. Reading only the stream type would leave you thinking this service has no audio at all.

For structured output, use the explicit long options with real filenames:

tstables --pid 0 --max-tables 1 --json-output pat.json demo.ts
tstables --pid 0 --max-tables 1 --xml-output pat.xml demo.ts

# or one JSON object per line on stdout, for log ingestion
tstables --pid 0 --max-tables 1 --log-json-line demo.ts

8. Tables as editable XML

This is the capability that most distinguishes TSDuck from a viewer. Tables round-trip losslessly between binary sections and XML, so signalling becomes something you can edit in a text editor and put back.

# binary sections → XML
tstabcomp -d tables.bin -o tables.xml

# XML → binary sections
tstabcomp -c tables.xml -o tables.bin

The XML dialect is strict and its schema ships with the install. When a table fails to compile, the error names the offending node and line, which is usually enough:

tstabcomp: unexpected node <language> in <subtitling_descriptor>, line 14

That was a real error from building this page's demo stream. The correct child element is <subtitling language_code="eng" …>, not <language code="eng" …>. Rather than guess, read the shipped model:

# the authoritative schema for every table and descriptor
find /opt/homebrew /usr/share -name 'tsduck.tables.model.xml' 2>/dev/null

# it documents each element's required attributes and types, e.g.
<subtitling_descriptor>
  <subtitling language_code="char3, required" subtitling_type="uint8, required"
    composition_page_id="uint16, required" ancillary_page_id="uint16, required" />
</subtitling_descriptor>
Read the model file, not a tutorial. It is generated from the same definitions the compiler uses, so it cannot drift from the implementation, and it covers every table and descriptor TSDuck knows. Any XML question about signalling is answered there faster than by searching.

9. Building a test stream from nothing

You do not need captured broadcast content to test a pipeline, and using it brings rights and privacy problems that a synthetic stream avoids entirely. TSDuck can generate a fully valid multi-programme stream from a few XML files. This is exactly how the demo stream on this page was made.

Step 1 — describe the signalling. One XML file per PID, since inject places one file's sections on one PID:

<?xml version="1.0" encoding="UTF-8"?>
<tsduck>
  <PAT version="1" current="true" transport_stream_id="0x0064" network_PID="0x0010">
    <service service_id="0x0101" program_map_PID="0x0100"/>
    <service service_id="0x0102" program_map_PID="0x0200"/>
  </PAT>
</tsduck>

Step 2 — craft the elementary streams. craft generates packets on one PID with a chosen payload pattern:

tsp -I craft --pid 0x111 --count 4000 --pcr 0 --payload-pattern 4711 -O file es_video.ts
tsp -I craft --pid 0x112 --count 3000 --payload-pattern 4711 -O file es_audio.ts

Step 3 — multiplex. Start from null packets, inject the signalling, mux in the elementary streams. --inter-packet sets how many packets of the outer stream sit between two packets of the new PID, which is how you control each component's share of the multiplex:

tsp -I null 40000 \
    -P inject pat.xml  --pid 0x0000 --inter-packet 100 \
    -P inject pmt1.xml --pid 0x0100 --inter-packet 250 \
    -P inject sdt.xml  --pid 0x0011 --inter-packet 500 \
    -P mux es_video.ts --inter-packet 4 \
    -P mux es_audio.ts --inter-packet 12 \
    -O file raw.ts

Step 4 — make the clocks real. Crafted packets all carry the same PCR value, so bitrate cannot be derived and tsanalyze reports Estimated based on PCR's: Unknown. One pass fixes it:

tsp --bitrate 38000000 -I file raw.ts -P pcradjust -O file demo.ts

Step 5 — make the continuity counters real. Crafted packets also share a continuity counter, which makes packet loss undetectable. Repair in place:

tsfixcc demo.ts

After those two passes the stream analyses as a genuine 38 Mb/s multiplex: PCR-derived bitrate, correct per-service rates, incrementing continuity counters, zero unreferenced PIDs. Both passes are easy to forget, and forgetting them produces a stream that looks fine in a hex dump but behaves nothing like broadcast under test.

10. Extracting one service from a multiplex

Three plugins do overlapping jobs here and the difference matters.

PluginBehaviourUse when
zapKeeps one or more services, removes all others, and rewrites the PAT and PMT so the result is a valid single-programme streamYou want a clean SPTS to hand to a decoder or encoder
svremoveRemoves named services, leaves the rest intactYou want to drop a few services from an otherwise unchanged multiplex
filterOperates on raw PIDs with no understanding of services or signallingYou know exactly which PIDs you want and do not care about validity

zap accepts a service name, which is far more readable than a PID list:

tsp -I file demo.ts -P zap 'DEMO UHD TWO' -O file uhd.ts

svremove paired with rmorphan is the combination to remember. Removing a service leaves its elementary PIDs referenced by nothing; rmorphan sweeps them up:

$ tsp -I file demo.ts -P svremove 'DEMO SD THREE' -P rmorphan -O file slim.ts

$ tsanalyze --service-list slim.ts
257 258

$ tsanalyze --pid-list slim.ts
0 17 256 273 274 275 512 529 530 8191

PIDs 768, 785 and 786 — the removed service's PMT and elementary streams — are gone. Without rmorphan they would still be carried, consuming bandwidth while tsanalyze reported them as unreferenced.

--pid is not a comma-separated list. Writing --pid 0,17,256 fails with value for option --pid (-p) must be <= 8,191, because the whole string is parsed as one integer. The option takes a single PID or a pid1-pid2 range and is repeated for more:
tsp -I file demo.ts -P filter --pid 0 --pid 17 --pid 256 --pid 273-275 -O file svc1.ts
The same convention applies to every plugin whose help shows -p pid1[-pid2], which is most of them.

11. Rewriting signalling in flight

Renaming a service is a one-liner, and it edits the SDT rather than repacking anything:

$ tsp -I file demo.ts -P svrename 'DEMO SD THREE' --name 'RENAMED SD' -O file ren.ts

$ tstables --tid 0x42 --max-tables 1 ren.ts | grep 'Service:'
      Service: "DEMO HD ONE", Provider: "SATLINE DEMO"
      Service: "DEMO UHD TWO", Provider: "SATLINE DEMO"
      Service: "RENAMED SD", Provider: "SATLINE DEMO"

The related plugins cover the rest of the signalling surface. Each performs targeted transformations rather than wholesale replacement, so unspecified fields are preserved:

PluginTypical use
patAdd or remove services from the PAT, set the NIT PID
pmtAdd, remove or remap components; add and remove descriptors; change the PCR PID
sdtChange service names, providers, types and running status
nitRewrite network information, including transport and delivery descriptors
svrename / tsrenameRename a service, or renumber the transport stream itself
remapMove PIDs to new values, updating references in the signalling
eitinjectGenerate and inject EIT, useful when a source carries none
injectInsert arbitrary tables from XML or binary files, at a chosen rate
Where this is genuinely useful. Regional feeds, service renumbering to fit a downstream platform's expectations, and repairing a source whose signalling is legal but inconvenient. Because these plugins sit in a pipeline, the fix happens in flight — you are not storing a corrected copy and re-serving it.

12. Timing: PCR extraction and verification

pcrverify checks PCR jitter against a threshold and reports a single summary line, which makes it suitable for an automated check:

$ tsp -I file demo.ts -P pcrverify --jitter-max 100000 -O drop

* pcrverify: 21,664 PCR OK, 0 with jitter > 2,700,000 (100,000 micro-seconds), 3 unchecked

Note how the threshold is echoed: 100,000 microseconds is shown as 2,700,000 in 27 MHz units, the clock PCR is actually counted in. The "3 unchecked" are the first PCR on each of the three PCR-carrying PIDs — there is nothing to compare a first sample against.

For analysis rather than a pass/fail, pcrextract writes every timestamp to CSV:

$ tsp -I file demo.ts -P pcrextract --pcr --output-file pcr.csv -O drop
$ head -4 pcr.csv
PID,Packet index in TS,Packet index in PID,Type,Count in PID,Value,Value offset in PID,Offset from PCR
273,5,0,PCR,1,0,0,
273,6,1,PCR,2,1069,1069,
273,8,2,PCR,3,3206,3206,

Add --pts and --dts to include presentation and decode timestamps in the same file, which is how you investigate lip-sync complaints: compare PTS progression against the PCR that is meant to be pacing it.

PluginWhat it does
pcradjustRewrites PCRs to be consistent with a constant bitrate. The repair pass in section 9
pcrbitrateContinuously recomputes the stream bitrate from PCRs, so later plugins see a live value
pcreditShifts or scales PCR, PTS and DTS — for deliberately creating timing faults to test against
pcrcopy / pcrduplicateSynchronise a PID's clock from another, or split PCR into its own PID
regulatePaces output to real time using PCR or a fixed bitrate. Essential when sending a file to IP — without it you transmit as fast as the disk allows

13. Continuity counters, and proving the 16-packet blind spot

The MPEG-TS sheet states that the continuity counter is 4 bits, so it wraps every 16 packets, and that a loss of exactly 16 packets on a PID is therefore invisible. That is a claim worth testing rather than repeating, and TSDuck can test it directly.

The method: take the demo stream with correct continuity counters, drop a controlled number of consecutive packets, and ask tsanalyze what it noticed. PID 0x0111 appears roughly every fourth packet, so dropping n packets of the multiplex removes about n/4 packets of that PID.

tsp -I file demo.ts -P slice --drop 20000 --pass 20064 -O file gap.ts
tsanalyze --normalized gap.ts | grep '^pid:.*pid=273:'

The measured results:

Packets droppedLost on PID 0x0111lost mod 16Observed CC stepDiscontinuities reportedDuplicates reported
641601 — looks correct00
68171210
6015150 — looks like a repeat01
4411111210
The blind spot is real, and it is worse than usually described. Losing exactly 16 packets produces an observed continuity step of 1 — indistinguishable from a perfectly continuous stream. Zero discontinuities, zero duplicates, no indication whatsoever that 16 packets are missing. And a loss of 15 is reported as a duplicate rather than a loss, because the counter appears to repeat: a monitor that alarms on discontinuities but treats duplicates as benign will miss it too. Continuity counting detects loss only when the number of lost packets is not a multiple of 16, which means it is a useful signal but never a sufficient one. Byte-accurate accounting has to come from somewhere else — PCR jitter, bitrate deviation, or a transport-layer sequence number such as RTP's.

The repair tool, for the case where continuity counters are wrong but packets are all present — a common artefact of naive remultiplexing:

tsfixcc demo.ts            # repair in place
tsfixcc --no-action demo.ts  # report what would change, change nothing

Use --no-action first. If it reports a large number of corrections, the problem is upstream and rewriting the counters hides it rather than fixing it.

14. Monitoring a live stream

Three plugins pass packets through untouched while reporting, so they can be inserted into a production pipeline.

stats reports per-PID packet counts and inter-packet distance, which is the fastest way to see whether a component's share of the multiplex is stable:

$ tsp -I file demo.ts -P stats -O drop

          Total nb  ......Inter-packet distance.......
PID     of packets     min     max      mean   std dev
------  ----------  ------  ------  --------  --------
0x0000         401      99     100    100.00  5.00e-02
0x0011          80     499     500    499.99      0.11
0x0111      10,000       1       8      4.00      0.45

A standard deviation far above the mean's neighbours indicates a bursty PID — which for a PCR-carrying PID is a direct cause of jitter downstream.

history logs structural events as they happen: PIDs appearing and disappearing, table versions changing, scrambling starting and stopping:

$ tsp -I file demo.ts -P history -O drop

* history: 0: PID 0x0000 (0) first packet, clear
* history: 0: PAT v1, TS 0x0064 (100)
* history: 1: PID 0x0100 (256) first packet, clear
* history: 1: PMT v1, service 0x0101 (257)

Add --milli-seconds to timestamp events in wall-clock rather than packet numbers. This is the plugin to reach for when a service intermittently disappears and you need to know whether the signalling changed or the packets simply stopped.

For dashboards, influx pushes live metrics to InfluxDB, which Grafana reads directly:

tsp -I ip 239.0.0.1:1234 \
    -P influx --org myorg --bucket ts-metrics --interval 5 --pid 0-8191 \
    -O drop

Two more worth knowing: iat analyses inter-arrival time for datagram inputs, which is the right measurement for diagnosing a network path rather than a stream; and tslatencymonitor compares two inputs to measure end-to-end latency between them.

15. Conditional access

TSDuck implements the scrambling side of the mechanisms in the conditional access sheet, which makes it useful for validating a CA integration without touching a live encrypted service.

$ tsp -I file demo.ts \
    -P scrambler --dvb-cissa --cw 000102030405060708090A0B0C0D0E0F 'DEMO HD ONE' \
    -O file scr.ts

$ tsanalyze scr.ts | grep -E 'Clear:|Scrambled:'
|          Clear: ......... 11  |
|          Scrambled: ...... 2  |

The service's access column flips from C to S while the other two remain clear — a self-contained way to produce a scrambled stream for testing a descrambling path. The control word above is a published test value, not a key from any real system.

Option or commandPurpose
scrambler --dvb-cissaScramble with CISSA — AES-128-CBC, the algorithm specified for DVB-IPTV
scrambler --aes-cbc / --aes-ctrPlain AES modes, for non-DVB or proprietary arrangements
scrambler --cw-fileRotate control words from a file, so crypto periods actually change
scrambler --pid-ecmInsert an ECM stream on a chosen PID alongside the scrambled content
tsecmg / tsemmgECMG and EMMG simulators speaking the TS 103 197 interfaces
tstestecmgConformance-test a vendor's ECMG before it reaches production
tsgenecmGenerate ECM files for offline testing
tssmartcardList and interrogate smartcard readers on the system

On the inspection side, tstables --tid 0x01 reads the CAT, and the CA descriptors in each PMT identify the ECM PIDs. Both are in the clear in any stream, so you can map a CA deployment's signalling without holding a single key.

16. T2-MI

Where a DVB-T2 modulator feed is carried inside a transport stream, the inner stream has to be de-encapsulated before anything else works. The MPEG-TS sheet covers the packet structure; the extraction is one plugin:

# what is in the T2-MI stream?
tsp -I file t2mi-feed.ts -P t2mi -O drop

# extract the inner TS, selecting a specific PLP
tsp -I file t2mi-feed.ts -P t2mi --extract --pid 4096 --plp 0 -O file inner.ts

Without --extract the plugin analyses and reports on the T2-MI structure while passing the outer stream through. With it, the output becomes the inner transport stream and everything downstream in the pipeline operates on that instead.

The PLP selection is not optional in practice. A T2-MI feed can carry several Physical Layer Pipes, and extracting without specifying which one gives you whichever the plugin defaults to. If a service you expect is absent from the extracted stream, the PLP is the first thing to check — not the PID.

17. Inputs and outputs beyond files

The pipeline model pays off here: any input can feed any chain into any output, so TSDuck doubles as a protocol converter.

TaskCommand
File to multicast, correctly pacedtsp -I file demo.ts -P regulate -O ip 239.0.0.1:1234 --ttl 8
Multicast to filetsp -I ip 239.0.0.1:1234 -O file capture.ts
Multicast to SRT, as callertsp -I ip 239.0.0.1:1234 -O srt --caller example.net:9000 --passphrase … --latency 200
SRT listener to multicasttsp -I srt --listener 9000 -O ip 239.0.0.2:1234
HLS to transport streamtsp -I hls https://example.net/master.m3u8 -O file out.ts
Serve a stream over HTTPtsp -I file demo.ts -P regulate -O http --server 8080
Analyse a packet capturetspcap capture.pcapng or tsp -I pcap capture.pcapng -P analyze -O drop
Always insert regulate when a file feeds a network output. Without it, tsp reads the file as fast as the disk allows and transmits at that rate — tens or hundreds of times real time. The receiver sees a burst it cannot buffer, and the resulting symptoms look like a network fault rather than a missing plugin.

The pcap input deserves particular note: it reads transport stream packets straight out of a Wireshark capture. When a stream problem has already happened and all you have is a capture, this turns that capture into something tsanalyze can measure.

18. Input failover

tsswitch is a separate command rather than a plugin, because switching between inputs is not something a linear pipeline expresses. It takes several inputs and delivers one output, switching on failure.

tsswitch --primary-input 0 --fast-switch --receive-timeout 2000 \
    -I ip 239.0.0.1:1234 \
    -I ip 239.0.0.2:1234 \
    -O ip 239.0.1.1:1234
OptionEffect
--primary-inputWhich input is preferred, and reverted to when it recovers
--fast-switchKeep all inputs receiving continuously, so a switch does not wait for the backup to start
--receive-timeoutHow long silence must last before the input counts as failed
--remoteListen for remote control commands to switch on demand

--fast-switch is the option that determines whether failover is seamless or merely automatic. Without it, the backup input is only started once the primary has been declared dead, so the gap includes the backup's own startup time — for a satellite receiver, potentially seconds. With it, both inputs run continuously and the switch is a decision about which buffer to read from.

Where this fits SATLINE's architecture. A SAT>IP tuner delivered from inside a satellite footprint is an IP input like any other, so a facility can run two independent tuner sources and let tsswitch arbitrate. That is redundancy across receiving sites rather than across tuner cards in one chassis — which is the failure mode that actually takes services off air. See the locked and unlocked tuners sheet for how the sources differ.

19. Deliberately breaking a stream

Monitoring that has never seen a fault is untested monitoring. fuzz introduces controlled corruption:

tsp -I file demo.ts -P fuzz --corrupt-probability 1/20000 --pid 273-275 -O file broken.ts
OptionEffect
--corrupt-probabilityRate of byte corruption, as a fraction
--pidRestrict damage to specific PIDs or ranges
--sync-byteAllow corruption of the 0x47 sync byte — this is what produces sync loss rather than payload damage
--seedSeed the generator so a run is reproducible. Use this, or a failure you find cannot be replayed
Payload corruption does not set the transport error indicator. Running fuzz with only --corrupt-probability and then checking tsanalyze for transport errors reports zero — because corrupting payload bytes damages content without touching the header flag that signals a known-bad packet. That flag is set by demodulators when FEC fails, not by whatever mangled the bytes. To produce header-level faults, add --sync-byte. This distinction is exactly why the DVB errors sheet treats sync loss and content corruption as different priority levels.

For precise rather than random damage, slice drops or nulls packets at exact positions — which is how the continuity measurements in section 13 were produced:

tsp -I file demo.ts -P slice --drop 20000 --pass 20064 -O file gap.ts  # remove packets
tsp -I file demo.ts -P slice --null 20000 --pass 20064 -O file nul.ts  # replace with stuffing

The difference matters: dropping packets changes the bitrate, while nulling them preserves it. For testing a constant-bitrate path, null rather than drop.

20. Traps, all encountered while writing this page

These are not hypothetical. Each one produced a wrong or confusing result during the preparation of this sheet.

What was writtenWhat happenedThe fix
tstables --json demo.ts--json was resolved as an abbreviation of --json-output, which takes a filename. The input file was overwritten with a 4-byte empty JSON array. Every later command reported an empty stream, which looked like a tool bug for some time.Use the full option name, and keep input paths out of any position an output filename could occupy
filter --pid 0,17,256value for option --pid (-p) must be <= 8,191 — the list was parsed as one integerRepeat the option; use a-b for ranges
pcrverify --time-limit 100unknown option --time-limitThe option is --jitter-max, in microseconds
fuzz --probability 1/50000unknown option --probabilityIt is --corrupt-probability
slice --pid 273 --stop-packet NThree unknown option errors at onceslice takes --drop, --pass and --null with packet numbers, and has no --pid
<language code="eng"…/>tstabcomp: unexpected node <language> in <subtitling_descriptor>The element is <subtitling language_code="eng"…/>. Read tsduck.tables.model.xml
Unquoted SAT>IP URLOn bash, the shell splits at the first &, backgrounds the command and turns the rest of the query into shell variables — tsp receives only ?freq=…, so the server cannot tune. The prompt returns immediately, so it looks like a clean exit. zsh refuses with parse error near '&' insteadAlways quote it; prefer single quotes. See section 5
plsm=1 instead of plsm=goldA numeric plsm is parsed as root mode and re-derives the scrambling code, so the tuner hunts a sequence you did not ask for and never locks. Presents exactly like a wrong PLS code or a weak signalplsm takes a name — gold or root
Crafted stream, then tsanalyzeEstimated based on PCR's: Unknown, and dropped packets produced zero discontinuitiesCrafted packets share one PCR and one continuity counter. Run pcradjust and tsfixcc before treating the stream as realistic
The general lesson. TSDuck's option abbreviation is convenient and occasionally dangerous, and its plugins do not share a common vocabulary — what one calls --probability another calls --corrupt-probability, and --pid means different things depending on the plugin. tsp -P <plugin> --help is authoritative, takes two seconds, and would have prevented every row in that table.

21. Sources and related sheets

SourceRole here
TSDuck 3.44-4676The software itself. Every command on this page was executed against this build, and every quoted output is its real output
tsduck.tables.model.xmlShips with the install. The authoritative schema for table and descriptor XML
tsp -P <plugin> --helpAuthoritative per-plugin option reference, and the source for every option named here
ISO/IEC 13818-1, ETSI EN 300 468The structures the tools operate on — packets, tables and descriptors
ETSI TS 103 197The SimulCrypt interfaces that tsecmg, tsemmg and tstestecmg implement
ETSI TS 102 773T2-MI, de-encapsulated by the t2mi plugin

22. About the author

Gleb Sazanov

Project Leader

Gleb Sazanov is an accomplished Chief Technology Officer (CTO) with over 20 years of experience in software development, system architecture, and cloud-based solutions. As the CTO of SATLINE, a leading provider of virtual and colocation services tailored to SATCOM businesses, Gleb drives the company’s technological strategy, fostering innovation and efficiency in data center services. His expertise spans various domains, including DevOps, system scaling, and high-performance infrastructure management. With a deep passion for cutting-edge technologies, Gleb plays a pivotal role in shaping the future of the SATCOM industry.