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.
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.
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.
| Property | Value |
|---|---|
| Services | 3 — one AVC HD, one HEVC UHD, one MPEG-2 SD |
| PIDs | 13 total, 3 carrying PCR, 0 unreferenced |
| Bitrate | ~38 Mb/s, PCR-consistent |
| Length | 40,000 packets — about 1 second |
| Signalling | PAT, 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.
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:
- Order matters, and it is the most common source of surprise. Filtering PIDs before analysing gives a different answer from analysing before filtering. Neither is wrong; they answer different questions.
- Any stage can be a measurement rather than a change. Plugins like
analyze,history,statsandpcrverifypass packets through untouched and report on the side, so you can instrument a live pipeline without altering what it delivers.
The plugin inventory in 3.44 breaks down as follows, and knowing the shape of it saves a lot of searching.
| Kind | Examples |
|---|---|
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 modify | analyze, history, stats, pcrverify, pcrextract, pes, tables, psi, iat, influx |
| Select and drop | filter, zap, svremove, rmorphan, slice, until, skip, reduce, limit |
| Rewrite signalling | pat, pmt, sdt, nit, svrename, tsrename, inject, sections, eitinject |
| Timing | pcradjust, pcrbitrate, pcredit, pcrcopy, regulate, timeshift, pidshift, timeref |
| Encapsulation | t2mi, encap, feed, mpe, mpeextract, nip, flute, dsmcc |
| Conditional access | scrambler, descrambler-adjacent tooling, plus standalone tsecmg, tsemmg, tsgenecm, tstestecmg |
| Deliberately break things | fuzz, craft, pattern, trigger, trace |
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.
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.
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.
The real output on the demo stream, abbreviated to the header:
| Field | Value | What it tells you |
|---|---|---|
| Transport Stream Id | 0x0064 (100) | Identity of the multiplex, from the PAT |
| Services | 3 | How many programmes are actually signalled |
| PID's Total / Clear / Scrambled | 13 / 13 / 0 | Whether anything is encrypted, before you waste time on a CAM |
| With PCR's | 3 | One per service here — the expected shape |
| Unreferenced | 0 | The single most useful number. Non-zero means PIDs carry data that no PMT declares |
| With invalid sync / transport error | 0 / 0 | Physical-layer integrity of the file itself |
| Estimated based on PCR's | 37,997,443 b/s | Derived from clock, not from file size — so it is the real rate |
| Broadcast time | 1 sec | Sanity 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":
| 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 --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:
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.
-P analyze \
-P until --seconds 5 \
-O drop
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:
$ 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" # 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:
| Field | Value | What to make of it |
|---|---|---|
| Services | 19 | The inventory you came for |
| PID's Total / Clear / Scrambled | 70 / 70 / 0 | Entirely free-to-air — no CAM needed for any of it |
| With PCR's | 12 | Fewer 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 |
| Unreferenced | 0 | Every PID is declared by a PMT. Clean signalling |
| Invalid sync / transport error | 0 / 0 | The carrier and the IP path are both delivering intact packets |
| Estimated based on PCR's | 36,714,356 b/s | Derived from the stream's own clock |
| Selected reference bitrate | 36,714,356 b/s | Agrees 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 time | 4 sec | Asked for --seconds 5. See 5.4 |
Three readings from the service list that generalise to any multiplex:
- One UHD service took 14,390,135 b/s — 39 % of the entire 36,7 Mb/s multiplex. That single number is the whole argument for why UHD capacity planning is different in kind, not degree. It also tells you immediately which service to drop if you need room.
- Six services reported exactly 328,866 b/s each. Identical to the byte across six services is never a coincidence — it means each is contributing the same small fixed set of packets, typically just its PMT plus a shared component, rather than its own elementary streams. Those services are announced in this multiplex but their content is not flowing here. Confirm it by reading the full per-service PID breakdown, which lists each service's components: a service with only a PMT and no video PID is signalling, not television.
- One service ran at 7,440 b/s. Far too low for audio, let alone video. That is a data service — a software-download or metadata carousel — and a reminder that not everything in a multiplex is a programme. Do not treat it as a broken TV service.
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.
| Parameter | Meaning |
|---|---|
freq, pol | Transponder frequency in MHz and polarisation, H or V |
sr | Symbol 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, fec | Delivery 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 |
isi | Input 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, plsm | Physical 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_plp | De-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 |
pids | all for the full MPTS, or an explicit list to reduce traffic. Client-controlled per session — nothing is filtered for you unless you ask |
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.
freq=12606 pol=V msys=dvbs2 mtype=8psk fec=23 sr=353002. Select the stream
isi=5 — one of several sharing the carrier3. Descramble PL
plsm=gold plsc=131070 — physical layer, not conditional access4. Rebuild the outer TS
bbframe=1 — in software, losslessly5. 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.
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.
| Detail | Why it matters |
|---|---|
Sets DTV_STREAM_ID bit 0x40000000, per tune | Raw-bbframe mode is requested for one tune, not for the card. The same adapter keeps serving ordinary transport-stream transponders concurrently |
| Falls back silently | A 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 4095 | Two conventional values. Give the number explicitly when you know it and want deterministic behaviour |
t2mi_plp=all merges every PLP | Correct 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 TS | Once 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 |
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.
| Option | Effect |
|---|---|
-P until --seconds 5 | Stops five seconds after the first packet arrives. This is wall-clock time by default |
-P until --pcr-based | Measures the interval from the stream's own PCR instead of the wall clock |
-P until --packets N | A fixed packet count — deterministic, and the better choice when comparing two runs |
-O drop | Discards the stream. Do not omit this. Without an output plugin the packets go to standard output and flood your terminal with binary |
--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:
--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.
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
| Option | Effect |
|---|---|
--interval N | Produce 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 |
--cumulative | Keep accumulating instead of resetting. Use when you want totals since start rather than per-interval figures |
--multiple-files | With --interval and --output-file, write each report to its own file rather than overwriting |
--error-analysis | Report on errors specifically rather than producing the full inventory |
--json-udp, --json-tcp | Ship 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:
| Option | Effect |
|---|---|
--infinite | Reconnect and keep reading indefinitely instead of stopping at end of stream |
--reconnect-delay ms | How long to wait before retrying a dropped connection |
--receive-timeout ms | Treat silence longer than this as a failure — the option that turns a stalled connection into a detectable event rather than a hang |
--connection-timeout ms | Bound the initial connect attempt |
--ignore-errors | Continue past HTTP errors rather than terminating |
--headers, --user-agent | Set request headers, occasionally needed by a server that filters on them |
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 --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:
tsanalyze --normalized demo.ts | grep '^pid:.*pid=273:' | tr ':' '\n' \
| grep -E '^(packets|discontinuities)='
packets=10000
discontinuities=0
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.
* 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 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:
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 --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.
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:
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:
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>
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:
<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 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:
-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:
Step 5 — make the continuity counters real. Crafted packets also share a continuity counter, which makes packet loss undetectable. Repair in place:
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.
| Plugin | Behaviour | Use when |
|---|---|---|
zap | Keeps one or more services, removes all others, and rewrites the PAT and PMT so the result is a valid single-programme stream | You want a clean SPTS to hand to a decoder or encoder |
svremove | Removes named services, leaves the rest intact | You want to drop a few services from an otherwise unchanged multiplex |
filter | Operates on raw PIDs with no understanding of services or signalling | You 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:
svremove paired with rmorphan is the combination to remember. Removing a service leaves its elementary PIDs referenced by nothing; rmorphan sweeps them up:
$ 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:
-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:
$ 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:
| Plugin | Typical use |
|---|---|
pat | Add or remove services from the PAT, set the NIT PID |
pmt | Add, remove or remap components; add and remove descriptors; change the PCR PID |
sdt | Change service names, providers, types and running status |
nit | Rewrite network information, including transport and delivery descriptors |
svrename / tsrename | Rename a service, or renumber the transport stream itself |
remap | Move PIDs to new values, updating references in the signalling |
eitinject | Generate and inject EIT, useful when a source carries none |
inject | Insert arbitrary tables from XML or binary files, at a chosen rate |
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:
* 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:
$ 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.
| Plugin | What it does |
|---|---|
pcradjust | Rewrites PCRs to be consistent with a constant bitrate. The repair pass in section 9 |
pcrbitrate | Continuously recomputes the stream bitrate from PCRs, so later plugins see a live value |
pcredit | Shifts or scales PCR, PTS and DTS — for deliberately creating timing faults to test against |
pcrcopy / pcrduplicate | Synchronise a PID's clock from another, or split PCR into its own PID |
regulate | Paces 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.
tsanalyze --normalized gap.ts | grep '^pid:.*pid=273:'
The measured results:
| Packets dropped | Lost on PID 0x0111 | lost mod 16 | Observed CC step | Discontinuities reported | Duplicates reported |
|---|---|---|---|---|---|
| 64 | 16 | 0 | 1 — looks correct | 0 | 0 |
| 68 | 17 | 1 | 2 | 1 | 0 |
| 60 | 15 | 15 | 0 — looks like a repeat | 0 | 1 |
| 44 | 11 | 11 | 12 | 1 | 0 |
The repair tool, for the case where continuity counters are wrong but packets are all present — a common artefact of naive remultiplexing:
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:
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:
* 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:
-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.
-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 command | Purpose |
|---|---|
scrambler --dvb-cissa | Scramble with CISSA — AES-128-CBC, the algorithm specified for DVB-IPTV |
scrambler --aes-cbc / --aes-ctr | Plain AES modes, for non-DVB or proprietary arrangements |
scrambler --cw-file | Rotate control words from a file, so crypto periods actually change |
scrambler --pid-ecm | Insert an ECM stream on a chosen PID alongside the scrambled content |
tsecmg / tsemmg | ECMG and EMMG simulators speaking the TS 103 197 interfaces |
tstestecmg | Conformance-test a vendor's ECMG before it reaches production |
tsgenecm | Generate ECM files for offline testing |
tssmartcard | List 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:
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.
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.
| Task | Command |
|---|---|
| File to multicast, correctly paced | tsp -I file demo.ts -P regulate -O ip 239.0.0.1:1234 --ttl 8 |
| Multicast to file | tsp -I ip 239.0.0.1:1234 -O file capture.ts |
| Multicast to SRT, as caller | tsp -I ip 239.0.0.1:1234 -O srt --caller example.net:9000 --passphrase … --latency 200 |
| SRT listener to multicast | tsp -I srt --listener 9000 -O ip 239.0.0.2:1234 |
| HLS to transport stream | tsp -I hls https://example.net/master.m3u8 -O file out.ts |
| Serve a stream over HTTP | tsp -I file demo.ts -P regulate -O http --server 8080 |
| Analyse a packet capture | tspcap capture.pcapng or tsp -I pcap capture.pcapng -P analyze -O drop |
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.
-I ip 239.0.0.1:1234 \
-I ip 239.0.0.2:1234 \
-O ip 239.0.1.1:1234
| Option | Effect |
|---|---|
--primary-input | Which input is preferred, and reverted to when it recovers |
--fast-switch | Keep all inputs receiving continuously, so a switch does not wait for the backup to start |
--receive-timeout | How long silence must last before the input counts as failed |
--remote | Listen 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.
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:
| Option | Effect |
|---|---|
--corrupt-probability | Rate of byte corruption, as a fraction |
--pid | Restrict damage to specific PIDs or ranges |
--sync-byte | Allow corruption of the 0x47 sync byte — this is what produces sync loss rather than payload damage |
--seed | Seed the generator so a run is reproducible. Use this, or a failure you find cannot be replayed |
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 --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 written | What happened | The 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,256 | value for option --pid (-p) must be <= 8,191 — the list was parsed as one integer | Repeat the option; use a-b for ranges |
pcrverify --time-limit 100 | unknown option --time-limit | The option is --jitter-max, in microseconds |
fuzz --probability 1/50000 | unknown option --probability | It is --corrupt-probability |
slice --pid 273 --stop-packet N | Three unknown option errors at once | slice 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 URL | On 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 '&' instead | Always quote it; prefer single quotes. See section 5 |
plsm=1 instead of plsm=gold | A 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 signal | plsm takes a name — gold or root |
Crafted stream, then tsanalyze | Estimated based on PCR's: Unknown, and dropped packets produced zero discontinuities | Crafted packets share one PCR and one continuity counter. Run pcradjust and tsfixcc before treating the stream as realistic |
--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
| Source | Role here |
|---|---|
| TSDuck 3.44-4676 | The software itself. Every command on this page was executed against this build, and every quoted output is its real output |
tsduck.tables.model.xml | Ships with the install. The authoritative schema for table and descriptor XML |
tsp -P <plugin> --help | Authoritative per-plugin option reference, and the source for every option named here |
| ISO/IEC 13818-1, ETSI EN 300 468 | The structures the tools operate on — packets, tables and descriptors |
| ETSI TS 103 197 | The SimulCrypt interfaces that tsecmg, tsemmg and tstestecmg implement |
| ETSI TS 102 773 | T2-MI, de-encapsulated by the t2mi plugin |
- TSDuck — project site, user guide and downloads
- TSDuck on GitHub — source, releases and issue tracker
- MPEG Transport Stream Explained — the container, and the continuity counter claim tested in section 13
- PSI/SI Tables & Descriptors — every table and descriptor
tstablesdecodes - Conditional Access & DVB SimulCrypt — the mechanisms behind section 15
- DVB Errors & Troubleshooting — the priority levels referenced in section 19
- SAT>IP Servers Pro