asticode / go-astits

Demux and mux MPEG Transport Streams (.ts) natively in GO

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

demuxer.NextData not returning PAT/PMT when seen

tmm1 opened this issue · comments

I have a mpegts segment which starts with three packets (SDT, PAT, PMT), followed by a ES data.

With astits-probe packets -i file.ts, I can confirm the first three packets:

2021/04/01 17:04:25 Fetching packets...
2021/04/01 17:04:25 PKT: 17
2021/04/01 17:04:25   Continuity Counter: 0
2021/04/01 17:04:25   Payload Unit Start Indicator: true
2021/04/01 17:04:25   Has Payload: true
2021/04/01 17:04:25   Has Adaptation Field: false
2021/04/01 17:04:25   Transport Error Indicator: false
2021/04/01 17:04:25   Transport Priority: false
2021/04/01 17:04:25   Transport Scrambling Control: 0
2021/04/01 17:04:25 PKT: 0
2021/04/01 17:04:25   Continuity Counter: 0
2021/04/01 17:04:25   Payload Unit Start Indicator: true
2021/04/01 17:04:25   Has Payload: true
2021/04/01 17:04:25   Has Adaptation Field: false
2021/04/01 17:04:25   Transport Error Indicator: false
2021/04/01 17:04:25   Transport Priority: false
2021/04/01 17:04:25   Transport Scrambling Control: 0
2021/04/01 17:04:25 PKT: 4096
2021/04/01 17:04:25   Continuity Counter: 0
2021/04/01 17:04:25   Payload Unit Start Indicator: true
2021/04/01 17:04:25   Has Payload: true
2021/04/01 17:04:25   Has Adaptation Field: false
2021/04/01 17:04:25   Transport Error Indicator: false
2021/04/01 17:04:25   Transport Priority: false
2021/04/01 17:04:25   Transport Scrambling Control: 0
2021/04/01 17:04:25 PKT: 256
2021/04/01 17:04:25   Continuity Counter: 0
2021/04/01 17:04:25   Payload Unit Start Indicator: true
2021/04/01 17:04:25   Has Payload: true
2021/04/01 17:04:25   Has Adaptation Field: true
2021/04/01 17:04:25   Transport Error Indicator: false
2021/04/01 17:04:25   Transport Priority: false
2021/04/01 17:04:25   Transport Scrambling Control: 0
2021/04/01 17:04:25   Adaptation Field: &{AdaptationExtensionField:<nil> DiscontinuityIndicator:false ElementaryStreamPriorityIndicator:false HasAdaptationExtensionField:false HasOPCR:false HasPCR:true HasTransportPrivateData:false HasSplicingCountdown:false Length:7 IsOneByteStuffing:false StuffingLength:0 OPCR:<nil> PCR:0xc000192070 RandomAccessIndicator:true SpliceCountdown:0 TransportPrivateDataLength:0 TransportPrivateData:[]}

However when I try to use the demuxer on the same file, the PAT/PMT are not returned to me first as I would expect.

for {
    d, err := r.demux.NextData()
    if err != nil {
        panic(err)
    }

    if d.PAT != nil {
        log.Printf("action=pat")
    }

    if d.PMT != nil {
        log.Printf("action=pmt")
    }

    if d.PES == nil {
        continue
    }

    pid := d.FirstPacket.Header.PID
    log.Printf("action=pes pid=%v", pid)
}

I would expect to see action=pat and action=pmt printed as the first two lines. Instead I see:

...
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=257
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=257
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=257
2021/04/01 17:08:41 action=pat
2021/04/01 17:08:41 action=pes pid=256
2021/04/01 17:08:41 action=pes pid=257
2021/04/01 17:08:41 action=pmt
panic: astits: no more packets

Somehow the PAT and PMT are buffered until the entire file is done, and only returned at the end.

Is there a queue somewhere that's being read in the wrong order?

The same behavior can be seen with astits-probe data -d all -i <file.ts>, where the SDT, PAT and PMT are not printed out until the end.

...
2021/04/01 17:15:11 PES: 257
2021/04/01 17:15:11   Stream ID: 192
2021/04/01 17:15:11   Packet Length: 2824
2021/04/01 17:15:11   Optional Header: &{AdditionalCopyInfo:0 CRC:0 DataAlignmentIndicator:false DSMTrickMode:<nil> DTS:<nil> ESCR:<nil> ESRate:0 Extension2Data:[] Extension2Length:0 HasAdditionalCopyInfo:false HasCRC:false HasDSMTrickMode:false HasESCR:false HasESRate:false HasExtension:false HasExtension2:false HasOptionalFields:false HasPackHeaderField:false HasPrivateData:false HasProgramPacketSequenceCounter:false HasPSTDBuffer:false HeaderLength:5 IsCopyrighted:false IsOriginal:false MarkerBits:2 MPEG1OrMPEG2ID:0 OriginalStuffingLength:0 PacketSequenceCounter:0 PackField:0 Priority:false PrivateData:[] PSTDBufferScale:0 PSTDBufferSize:0 PTS:0xc0003bf450 PTSDTSIndicator:2 ScramblingControl:0}
2021/04/01 17:15:11 PAT: 0
2021/04/01 17:15:11   Transport Stream ID: 1
2021/04/01 17:15:11   Programs:
2021/04/01 17:15:11     &{ProgramMapID:4096 ProgramNumber:1}
2021/04/01 17:15:11 SDT: 17
2021/04/01 17:15:11 PES: 256
2021/04/01 17:15:11   Stream ID: 224
2021/04/01 17:15:11   Packet Length: 0
2021/04/01 17:15:11   Optional Header: &{AdditionalCopyInfo:0 CRC:0 DataAlignmentIndicator:false DSMTrickMode:<nil> DTS:0xc0003bf7c0 ESCR:<nil> ESRate:0 Extension2Data:[] Extension2Length:0 HasAdditionalCopyInfo:false HasCRC:false HasDSMTrickMode:false HasESCR:false HasESRate:false HasExtension:false HasExtension2:false HasOptionalFields:false HasPackHeaderField:false HasPrivateData:false HasProgramPacketSequenceCounter:false HasPSTDBuffer:false HeaderLength:10 IsCopyrighted:false IsOriginal:false MarkerBits:2 MPEG1OrMPEG2ID:0 OriginalStuffingLength:0 PacketSequenceCounter:0 PackField:0 Priority:false PrivateData:[] PSTDBufferScale:0 PSTDBufferSize:0 PTS:0xc0003bf7b0 PTSDTSIndicator:3 ScramblingControl:0}
2021/04/01 17:15:11 PES: 257
2021/04/01 17:15:11   Stream ID: 192
2021/04/01 17:15:11   Packet Length: 1544
2021/04/01 17:15:11   Optional Header: &{AdditionalCopyInfo:0 CRC:0 DataAlignmentIndicator:false DSMTrickMode:<nil> DTS:<nil> ESCR:<nil> ESRate:0 Extension2Data:[] Extension2Length:0 HasAdditionalCopyInfo:false HasCRC:false HasDSMTrickMode:false HasESCR:false HasESRate:false HasExtension:false HasExtension2:false HasOptionalFields:false HasPackHeaderField:false HasPrivateData:false HasProgramPacketSequenceCounter:false HasPSTDBuffer:false HeaderLength:5 IsCopyrighted:false IsOriginal:false MarkerBits:2 MPEG1OrMPEG2ID:0 OriginalStuffingLength:0 PacketSequenceCounter:0 PackField:0 Priority:false PrivateData:[] PSTDBufferScale:0 PSTDBufferSize:0 PTS:0xc0003bfa10 PTSDTSIndicator:2 ScramblingControl:0}
2021/04/01 17:15:11 PMT: 4096
2021/04/01 17:15:11   ProgramNumber: 1
2021/04/01 17:15:11   PCR PID: 256
2021/04/01 17:15:11   Elementary Streams:
2021/04/01 17:15:11     &{ElementaryPID:256 ElementaryStreamDescriptors:[] StreamType:H264 Video}
2021/04/01 17:15:11     &{ElementaryPID:257 ElementaryStreamDescriptors:[] StreamType:AAC Audio}
2021/04/01 17:15:11   Program Descriptors:
2021/04/01 17:15:11 astits: fetching data failed: astits: no more packets

So I think this bug is in (*packetPool).add(p *Packet), here:

go-astits/packet_pool.go

Lines 61 to 65 in 077af8e

// Check payload unit start indicator
if p.Header.PayloadUnitStartIndicator && len(mps) > 1 {
ps = mps[:len(mps)-1]
mps = []*Packet{p}
}

Basically we wait to flush out the PAT/PMT until the next packet shows up with a PayloadUnitStartIndicator set. Since my ts file only has a singular PAT/PMT at the front, this never triggers and its not until demuxer calls (*packetPool).dump() at the end until that data is flushed.

I guess one way to fix this would be to special case non-ES packets, and compare the size in the header with the accumulated data to decide when to flush out.

/cc @barbashov

Can you try to fix it and make a PR please?

Yes I will, I am trying to figure out what the best approach is first so any help would be appreciated.

I think maybe we can make a special case for when SectionSyntaxIndicator == true (PAT/PMT/CAT) and then compare if SectionLength is smaller than the amount of data accumulated inside the packetPool?

I think the same approach can also be taken for PES using the pes length (if present).

Does that make sense? It means packetPool would need to start parsing out the PSI and PES headers and cache them using the first packet.

Thing is, for SectionSyntaxIndicator doc says Set when a PES, PSI, or DVB-MIP packet begins immediately following the header.. Both PMT and PAT are PSI. Furthermore, SectionLength can't be trusted solely because a section could very well end right at the end of a payload and the next section start at the beginning of the next packet.

Unless I'm missing something, I don't really think there's a way to know when all data of PAT or PMT packets is available for parsing until the next data arrives.

Furthermore that shouldn't be a problem when using the lib : if you don't want to miss out on PES packets, you can buffer them until you get the PAT or PMT, can't you?

Furthermore that shouldn't be a problem when using the lib : if you don't want to miss out on PES packets, you can buffer them until you get the PAT or PMT, can't you?

In the current design that would mean buffering the entire file because the PAT/PMT aren't being flushed until a ErrNoMorePackets is encountered and dump() is called.

go-astits/demuxer.go

Lines 113 to 118 in 5caf397

// If the end of the stream has been reached, we dump the packet pool
if err == ErrNoMorePackets {
for {
// Dump packet pool
if ps = dmx.packetPool.dump(); len(ps) == 0 {
break

Unless I'm missing something, I don't really think there's a way to know when all data of PAT or PMT packets is available for parsing until the next data arrives.

Yea I guess this is tricky. The only alternative would be to try to parse them iteratively as new data arrives, to see if the parser succeeds or runs out of data.

That approach also seems to be used by gots:

https://github.com/Comcast/gots/blob/1e4417348cff9f2167a720e496f6c3d259eec700/psi/pmt.go#L405

https://github.com/Comcast/gots/blob/1e4417348cff9f2167a720e496f6c3d259eec700/psi/pmt.go#L64-L68

Unless I'm missing something, I don't really think there's a way to know when all data of PAT or PMT packets is available for parsing until the next data arrives.

Well, every PSI packet has section_number and last_section_number. From H.222 specification:

section_number – This 8-bit field gives the number of this section. The section_number of the first section in the Program Association Table shall be 0x00. It shall be incremented by 1 with each additional section in the program association table.
last_section_number – This 8-bit field specifies the number of the last section (that is, the section with the highest section_number) of the complete program association table.

So it is possible to know that you've got the whole PAT/PMT by comparing these two.

I thought about that a couple weeks ago.
To use this, one needs to partially parse PSI packets in packet pool. And have access to PAT (I mean the actual table, to determine PMT PIDs) from packet pool. Which is kinda dirty from architecture point of view. And I couldn't figure out the clean way, so haven't done anything there :)

Another idea of mine was to flush buffer on PID change, which is quite simple and clean to implement. But it doesn't work with some streams - some muxers tend to break PES sequence with other packets. This causes flush on PID change to return incomplete PES data.

Just in case, flush on PID change logic used to be like this: 69ec742#diff-bf6bc659050e8825df8f250e9e1db49f0d7e1cfcbe8f977f950046494dd90cb7

@barbashov this is really interesting ❤️

Even though it's the more complicated and weird way to solve this, your first proposition (partially parse PSI data in packet pool and store PAT information to retrieve PMT PIDs afterwards) seems like the best one.

In fact I think it makes sense to isolate the logic that determines whether all payloads of a data have been gathered and parseData must be called.

First I would share Demuxer.programMap with PacketPool so that it has the information about PMT PIDs.

Then I would partially parse packet payloads here to determine whether we can call parseData.

Does that sound like a good idea ?