go-astits: demuxing and muxing MPEG transport streams in Go
Demux and mux MPEG Transport Streams (.ts) natively in GO
At a glance
- What is it?
- go-astits is a Go library that demuxes and muxes MPEG transport streams natively, with a demuxer, a muxer, option hooks and two command line tools. Its own documentation warns that it is not yet production ready, and the roadmap shows the muxing side of the PSI tables is unfinished.
- Who is it for?
- go-astits fits Go developers who need to read or write MPEG transport streams in process, for instance a monitoring service that inspects PMT tables or a pipeline that rewraps PES data, and the two CLIs are enough to look at a capture before writing any code. It does not fit a production pipeline yet, since the project states plainly that the library is not yet production ready and the muxing side of EIT, NIT, SDT and TOT tables is still unchecked on the roadmap.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 35 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What go-astits is for
MPEG transport streams are the container format behind broadcast television and a lot of streaming infrastructure, and working with one means dealing with thousands of small packets rather than one large file. go-astits is a Go library that demuxes and muxes that format natively, with no external binary and no cgo dependency in the import path.
The two halves serve different jobs. The demuxer reads a transport stream and hands back structured data: program specific information tables, PES packets, and the elementary stream descriptions that tell you what is inside. The muxer goes the other way, taking PES data and elementary stream declarations and writing a transport stream you can play.
The audience is Go developers building video tooling: a monitoring service that needs to know which streams a capture contains, a recorder that has to split a multiplex into separate elementary streams, or a test harness that generates transport streams. It is a library, not an application framework. There is no pipeline abstraction, no plugin system, and nothing above the packet level.
One thing to know before reading further is that the project's own README opens with a warning: the library is not yet production ready and should be used at your own risk. That is the maintainer's own framing, and it is the first thing an adopter should weigh.
Packets, adaptation fields and the data flow
The project spends its first section on structure before showing any code, and that structure is the mental model you need.
A transport stream is made of packets. Each packet has a header, an optional adaptation field, and a payload. The header carries the PID and other flags, the adaptation field exists for timing and stuffing, and the payload carries the actual elementary stream bytes. Several payloads can be appended and then parsed together as a data unit, and that unit is what the demuxer returns.
The repository layout mirrors this. Files named data_pat.go, data_pmt.go, data_pes.go, data_eit.go, data_nit.go, data_sdt.go and data_tot.go each implement one type of data, alongside a generic data.go and data_psi.go for the shared program specific information handling. packet.go handles the packet itself, and packet_buffer.go plus packet_pool.go and pools.go handle the buffers and pools used while reading, which is where the library avoids allocating per packet.
Two supporting files stand out. clock_reference.go deals with PCR and clock references, and wrapping_counter.go with the continuity counters. The muxer side has the equivalents, and there is a roundtrip_test.go at the top level, which suggests the demux and mux paths are meant to be inverses of each other.
Installing the library and the two command line tools
Installation is the standard Go module fetch. To pull the library into a project:
go get -u github.com/asticode/go-astits/...To install the executables that ship in the cmd/ directory:
go install github.com/asticode/go-astits/cmdThat second command matters more than it looks, because the executables land in GOPATH/bin, and the project notes that they are installed automatically when the go get command runs as well.
The module is github.com/asticode/go-astits and the go.mod file sets the language version to go 1.20. The direct dependency list is short, one library from the same author, go-astikit at v0.30.0, plus github.com/pkg/profile at v1.4.0 and github.com/stretchr/testify at v1.4.0. The testify version is old, and since test dependencies still get resolved into the module graph, a project on a modern toolchain may find itself resolving versions it did not choose. The tree also holds a .devcontainer/ directory, so there is a containerised development environment for anyone who wants to match the maintainer's setup.
Demuxing a file and reading its PMT
The demuxer takes a context and any io.Reader. The context is the mechanism for stopping a read, and the README shows the pattern: create a cancellable context, wire SIGTERM to it through a signal channel, and then cancel when the signal arrives.
The example opens a file, builds the demuxer, and loops on NextData:
// Create a cancellable context in case you want to stop reading packets/data any time you want
ctx, cancel := context.WithCancel(context.Background())
// Handle SIGTERM signal
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM)
go func() {
<-ch
cancel()
}()
// Open your file or initialize any kind of io.Reader
// Buffering using bufio.Reader is recommended for performance
f, _ := os.Open("/path/to/file.ts")
defer f.Close()
// Create the demuxer
dmx := astits.NewDemuxer(ctx, f)
for {
// Get the next data
d, _ := dmx.NextData()
// Data is a PMT data
if d.PMT != nil {
// Loop through elementary streams
for _, es := range d.PMT.ElementaryStreams {
fmt.Printf("Stream detected: %d\n", es.ElementaryPID)
}
return
}
}The shape to copy is the buffering advice. The comment recommends wrapping the reader in a bufio.Reader for performance, because without it every packet read becomes a syscall on an unbuffered reader.
The error handling in this snippet is deliberately omitted, and the README says so in capitals: the code does not handle errors for readability purposes, but you should. Every NextData call returns an error, and a real program has to deal with a truncated capture and a corrupt packet rather than let them pass through.
Muxing PES data back into a transport stream
The muxer mirrors the demuxer. Same context and signal handling, an io.Writer instead of a reader, and a call to astits.NewMuxer.
// Create a cancellable context in case you want to stop writing packets/data any time you want
ctx, cancel := context.WithCancel(context.Background())
// Handle SIGTERM signal
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM)
go func() {
<-ch
cancel()
}()
// Create your file or initialize any kind of io.Writer
// Buffering using bufio.Writer is recommended for performance
f, _ := os.Create("/path/to/file.ts")
defer f.Close()
// Create the muxer
mx := astits.NewMuxer(ctx, f)
// Add an elementary stream
mx.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 1,
StreamType: astits.StreamTypeMetadata,
})
// Write tables
// Using that function is not mandatory, WriteData will retransmit tables from time to time
mx.WriteTables()
// Write data
mx.WriteData(&astits.MuxerData{
PES: &astits.PESData{
Data: []byte("test"),
},
PID: 1,
})Two details carry the design. The muxer is told about an elementary stream through AddElementaryStream, which takes a PMTElementaryStream with a PID and a stream type, and the shown type is astits.StreamTypeMetadata. And writing tables is optional in the sense that WriteData retransmits tables from time to time, so a periodic WriteTables call is an optimisation rather than a requirement.
The muxer writes into any io.Writer, and again the recommendation is to wrap it in a bufio.Writer for performance. As with the demuxer, the muxer takes a context so that a long write can be interrupted on SIGTERM.
DemuxerOpt and MuxerOpt: packet size and a custom parser
Options are passed as variadic arguments to NewDemuxer and NewMuxer, and they are found by looking for methods prefixed with DemuxerOpt or MuxerOpt.
The documented example combines two of them, a non-default packet size and a custom packets parser:
// This is your custom packets parser
p := func(ps []*astits.Packet) (ds []*astits.Data, skip bool, err error) {
// This is your logic
skip = true
return
}
// Now you can create a demuxer with the proper options
dmx := NewDemuxer(ctx, f, DemuxerOptPacketSize(192), DemuxerOptPacketsParser(p))DemuxerOptPacketSize(192) selects 192 byte packets, which is the size used with the 192 byte TS packetisation overhead that IP based delivery adds. Captures that have been through an IP path need this option if their packet size differs from the one the demuxer assumes by default, or the parser will mis-frame.
The packets parser hook is the more interesting one. It takes a slice of packets and returns the data it derived, plus a skip flag. Setting skip to true tells the demuxer to discard those packets rather than process them further, which is how you implement a filter on PID, or an ignore rule for a stream you do not care about. This is the extension point that lets you avoid rewriting the library when a table type it does not implement appears in your capture.
astits-probe and astits-es-split from the terminal
Two command line tools come with the library, and both are the fastest way to understand a capture before writing any Go code.
astits-probe lists the streams in a file:
$ astits-probe -i <path to your file> -f <format: text|json (default: text)>The json output format is the useful one for scripting, since the text format is meant for reading.
The same tool has subcommands. To look at the packets themselves:
$ astits-probe packets -i <path to your file>And to look at specific data types, where the data type argument accepts values such as eit and nit, and is repeatable, and where leaving it empty shows all data types:
$ astits-probe data -i <path to your file> -d <data type: eit|nit|... (repeatable argument | if empty, all data types are shown)>The second tool splits a multiplex into separate .ts files per elementary stream, which is what you run when a capture contains several programs and you want to work on one of them:
$ astits-es-split <path to your file> -o <path to output dir>Neither tool is documented with man page style help text in the README, so the flags above are what the project states, and running the binaries with an invalid argument is the only way to see the rest.
The muxing half of the roadmap is unfinished
The features and roadmap list is the most honest part of the documentation, and it should be read as a status board. Demuxing is broadly covered. PES, PAT, PMT, EIT, NIT, SDT and TOT demuxing are marked done. BAT, DIT, RST, SIT, ST, TDT and TSDT demuxing are not.
The muxing column is thinner. Muxing PES, PAT and PMT is done. Muxing EIT, NIT, SDT and TOT is explicitly not done, and every other table type is unchecked as well. So the muxer can produce a transport stream with program association and program map tables, and it cannot yet emit the other program specific information tables that a receiver may expect to find.
That, combined with the not yet production ready warning, is the practical boundary of the project. A tool that reads captures and reports on them is well within what the code supports today. A tool that generates a compliant broadcast multiplex for a real receiver is not, because the tables such a receiver relies on to find the services are the ones still missing.
On licensing, the project is under the MIT licence, with the text in the LICENSE file. That permits use in closed source software, and the obligation is to keep the copyright notice and licence text with copies and substantial portions of the code. The repository is not archived, and the last push was on 2026-09-18. There are no GitHub releases, so there is no tagged version to pin, which means a dependency on this library tracks master and your build can change without a version bump.
Editorial conclusion
go-astits fits Go developers who need to read or write MPEG transport streams in process, for instance a monitoring service that inspects PMT tables or a pipeline that rewraps PES data, and the two CLIs are enough to look at a capture before writing any code. It does not fit a production pipeline yet, since the project states plainly that the library is not yet production ready and the muxing side of EIT, NIT, SDT and TOT tables is still unchecked on the roadmap. Before adopting it, run astits-probe against your own .ts captures to see which tables they actually carry, confirm your Go version meets the go 1.20 directive in go.mod, and read the MIT licence in the LICENSE file before you vendor it.
Frequently asked questions
How do I install go-astits and its command line tools?
Run go get -u github.com/asticode/go-astits/... to add the library, and go install github.com/asticode/go-astits/cmd for the executables, which land in GOPATH/bin. The module targets go 1.20 according to its go.mod file.
How do I read the elementary streams in an MPEG transport stream with go-astits?
Create a demuxer with astits.NewDemuxer(ctx, reader), call NextData in a loop, and check whether the returned data has a non-nil PMT, then iterate over d.PMT.ElementaryStreams to read each ElementaryPID. Wrapping the reader in a bufio.Reader is recommended for performance.
How do I parse transport streams that use 192 byte packets?
Pass DemuxerOptPacketSize(192) as an option to NewDemuxer. Captures that have been through an IP path use a different packet size than the demuxer's default, so they need this option.
Is go-astits ready for production use?
The project states in its README that the library is not yet production ready. The roadmap also shows that muxing EIT, NIT, SDT and TOT tables is not implemented, so writing a fully compliant multiplex is beyond what the muxer supports today.
What licence does go-astits use and how often is it updated?
The project is released under the MIT licence, with the terms in the LICENSE file. The repository is not archived, the last push was on 2026-09-18, and there are no GitHub releases, so there is no tagged version to pin.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/asticode-go-astits)Was this useful?