Repository: pion/interceptor
Pull request #330 (2025-05-18 08:30:23)

Fix malformed FlexFEC-03
#### Description

Fix several issues with FlexFec-03 encoder, add simple decoder and round-trip tests

#### Reference issue
Fixes https://github.com/pion/interceptor/issues/318


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pion/interceptor/pull/330?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
Attention: Patch coverage is `74.19355%` with `88 lines` in your changes missing coverage. Please review.
> Project coverage is 75.24%. Comparing base [(`96a23bb`)](https://app.codecov.io/gh/pion/interceptor/commit/96a23bb5f5f80e084358d9392183e2ebb2076a9e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) to head [(`991d973`)](https://app.codecov.io/gh/pion/interceptor/commit/991d97302a2626086529a5efe2767c0e6f516f1e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/pion/interceptor/pull/330?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Patch % | Lines |
|---|---|---|
| [pkg/flexfec/flexfec\_decoder\_03.go](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&filepath=pkg%2Fflexfec%2Fflexfec_decoder_03.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL2ZsZXhmZWMvZmxleGZlY19kZWNvZGVyXzAzLmdv) | 72.90% | [55 Missing and 26 partials :warning: ](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) |
| [pkg/flexfec/flexfec\_encoder\_03.go](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&filepath=pkg%2Fflexfec%2Fflexfec_encoder_03.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL2ZsZXhmZWMvZmxleGZlY19lbmNvZGVyXzAzLmdv) | 81.48% | [3 Missing and 2 partials :warning: ](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) |
| [pkg/flexfec/flexfec\_coverage.go](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&filepath=pkg%2Fflexfec%2Fflexfec_coverage.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL2ZsZXhmZWMvZmxleGZlY19jb3ZlcmFnZS5nbw==) | 84.61% | [2 Missing :warning: ](https://app.codecov.io/gh/pion/interceptor/pull/330?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #330      +/-   ##
==========================================
+ Coverage   71.59%   75.24%   +3.64%     
==========================================
  Files          79       80       +1     
  Lines        4746     5077     +331     
==========================================
+ Hits         3398     3820     +422     
+ Misses       1207     1071     -136     
- Partials      141      186      +45     
```

| [Flag](https://app.codecov.io/gh/pion/interceptor/pull/330/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [go](https://app.codecov.io/gh/pion/interceptor/pull/330/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | `?` | |
| [wasm](https://app.codecov.io/gh/pion/interceptor/pull/330/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | `75.24% <74.19%> (+5.58%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pion/interceptor/pull/330?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
aalekseevx: @3DRX, I don’t want to make this PR even bigger and harder to review, so I created a separate issue regarding EncoderInterceptor with your suggestions and a couple of mine: https://github.com/pion/interceptor/issues/332. In this PR, I want to focus on fixing the encoder. I will make another one for the interceptor!
aalekseevx: @JoeTurki, do you want to take a look at the code too? I'm not rushing you in any way, just want to make sure we're on the same page.
JoeTurki: @aalekseevx 

The change looks good to me, great stuff, thank you so much.
The only nitpick is that I don't see a test for reallocation from variable-size frames. just to test the slice-growth fix under test. Because that part is easy to break in future changes.
3DRX: > @3DRX, I don’t want to make this PR even bigger and harder to review, so I created a separate issue regarding EncoderInterceptor with your suggestions and a couple of mine: #332. In this PR, I want to focus on fixing the encoder. I will make another one for the interceptor!

Sure, thanks!
aalekseevx: @JoeTurki, good catch! I have added the corresonding test

Patch:
diff --git a/pkg/flexfec/flexfec_coverage.go b/pkg/flexfec/flexfec_coverage.go
index e82feff..1ffeefb 100644
--- a/pkg/flexfec/flexfec_coverage.go
+++ b/pkg/flexfec/flexfec_coverage.go
@@ -119,29 +119,44 @@ func (p *ProtectionCoverage) GetCoveredBy(fecPacketIndex uint32) *util.MediaPack
 // ExtractMask1 returns the first section of the bitmask as defined by the FEC header.
 // https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1
 func (p *ProtectionCoverage) ExtractMask1(fecPacketIndex uint32) uint16 {
-	mask := p.packetMasks[fecPacketIndex]
+	return extractMask1(p.packetMasks[fecPacketIndex])
+}
+
+// ExtractMask2 returns the second section of the bitmask as defined by the FEC header.
+// https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1
+func (p *ProtectionCoverage) ExtractMask2(fecPacketIndex uint32) uint32 {
+	return extractMask2(p.packetMasks[fecPacketIndex])
+}
+
+// ExtractMask3 returns the third section of the bitmask as defined by the FEC header.
+// https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1
+func (p *ProtectionCoverage) ExtractMask3(fecPacketIndex uint32) uint64 {
+	return extractMask3(p.packetMasks[fecPacketIndex])
+}
+
+// ExtractMask3_03 returns the third section of the bitmask as defined by the FEC header.
+// https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03#section-4.2
+func (p *ProtectionCoverage) ExtractMask3_03(fecPacketIndex uint32) uint64 {
+	return extractMask3_03(p.packetMasks[fecPacketIndex])
+}
+
+func extractMask1(mask util.BitArray) uint16 {
 	// We get the first 16 bits (64 - 16 -> shift by 48) and we shift once more for K field
 	mask1 := mask.Lo >> 49
 
 	return uint16(mask1) //nolint:gosec // G115
 }
 
-// ExtractMask2 returns the second section of the bitmask as defined by the FEC header.
-// https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1
-func (p *ProtectionCoverage) ExtractMask2(fecPacketIndex uint32) uint32 {
-	mask := p.packetMasks[fecPacketIndex]
+func extractMask2(mask util.BitArray) uint32 {
 	// We remove the first 15 bits
 	mask2 := mask.Lo << 15
-	// We get the first 31 bits (64 - 31 -> shift by 33) and we shift once more for K field
-	mask2 >>= 34
+	// We get the first 31 bits (64 - 32 -> shift by 32) and we shift once more for K field
+	mask2 >>= 33
 
-	return uint32(mask2) //nolint:gosec // G115
+	return uint32(mask2) //nolint:gosec
 }
 
-// ExtractMask3 returns the third section of the bitmask as defined by the FEC header.
-// https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1
-func (p *ProtectionCoverage) ExtractMask3(fecPacketIndex uint32) uint64 {
-	mask := p.packetMasks[fecPacketIndex]
+func extractMask3(mask util.BitArray) uint64 {
 	// We remove the first 46 bits
 	maskLo := mask.Lo << 46
 	maskHi := mask.Hi >> 18
@@ -150,10 +165,7 @@ func (p *ProtectionCoverage) ExtractMask3(fecPacketIndex uint32) uint64 {
 	return mask3
 }
 
-// ExtractMask3_03 returns the third section of the bitmask as defined by the FEC header.
-// https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03#section-4.2
-func (p *ProtectionCoverage) ExtractMask3_03(fecPacketIndex uint32) uint64 {
-	mask := p.packetMasks[fecPacketIndex]
+func extractMask3_03(mask util.BitArray) uint64 {
 	// We remove the first 46 bits
 	maskLo := mask.Lo << 46
 	maskHi := mask.Hi >> 18
diff --git a/pkg/flexfec/flexfec_decoder_03.go b/pkg/flexfec/flexfec_decoder_03.go
new file mode 100644
index 0000000..56ccff0
--- /dev/null
+++ b/pkg/flexfec/flexfec_decoder_03.go
@@ -0,0 +1,445 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+// Package flexfec implements FlexFEC-03 to recover missing RTP packets due to packet loss.
+// https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03
+package flexfec
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"sort"
+
+	"github.com/pion/logging"
+	"github.com/pion/rtp"
+)
+
+// Static errors for the flexfec package.
+var (
+	errPacketTruncated                = errors.New("packet truncated")
+	errRetransmissionBitSet           = errors.New("packet with retransmission bit set not supported")
+	errInflexibleGeneratorMatrix      = errors.New("packet with inflexible generator matrix not supported")
+	errMultipleSSRCProtection         = errors.New("multiple ssrc protection not supported")
+	errLastOptionalMaskKBitSetToFalse = errors.New("k-bit of last optional mask is set to false")
+)
+
+// fecDecoder is a WIP implementation decoder used for testing purposes.
+type fecDecoder struct {
+	logger              logging.LeveledLogger
+	ssrc                uint32
+	protectedStreamSSRC uint32
+	maxMediaPackets     int
+	maxFECPackets       int
+	recoveredPackets    []rtp.Packet
+	receivedFECPackets  []fecPacketState
+}
+
+func newFECDecoder(ssrc uint32, protectedStreamSSRC uint32) *fecDecoder {
+	return &fecDecoder{
+		logger:              logging.NewDefaultLoggerFactory().NewLogger("fec_decoder"),
+		ssrc:                ssrc,
+		protectedStreamSSRC: protectedStreamSSRC,
+		maxMediaPackets:     100,
+		maxFECPackets:       100,
+		recoveredPackets:    make([]rtp.Packet, 0),
+		receivedFECPackets:  make([]fecPacketState, 0),
+	}
+}
+
+func (d *fecDecoder) DecodeFec(receivedPacket rtp.Packet) []rtp.Packet {
+	if len(d.recoveredPackets) == d.maxMediaPackets {
+		backRecoveredPacket := d.recoveredPackets[len(d.recoveredPackets)-1]
+		if backRecoveredPacket.SSRC == receivedPacket.SSRC {
+			seqDiffVal := seqDiff(receivedPacket.SequenceNumber, backRecoveredPacket.SequenceNumber)
+			if seqDiffVal > uint16(d.maxMediaPackets) { //nolint:gosec
+				d.logger.Info("big gap in media sequence numbers - resetting buffers")
+				d.recoveredPackets = nil
+				d.receivedFECPackets = nil
+			}
+		}
+	}
+
+	d.insertPacket(receivedPacket)
+
+	return d.attemptRecovery()
+}
+
+func (d *fecDecoder) insertPacket(receivedPkt rtp.Packet) {
+	// Discard old FEC packets such that the sequence numbers in
+	// `received_fec_packets_` span at most 1/2 of the sequence number space.
+	// This is important for keeping `received_fec_packets_` sorted, and may
+	// also reduce the possibility of incorrect decoding due to sequence number
+	// wrap-around.
+	if len(d.receivedFECPackets) > 0 && receivedPkt.SSRC == d.ssrc {
+		toRemove := 0
+		for _, fecPkt := range d.receivedFECPackets {
+			if abs(int(receivedPkt.SequenceNumber)-int(fecPkt.packet.SequenceNumber)) > 0x3fff {
+				toRemove++
+			} else {
+				// No need to keep iterating, since |received_fec_packets_| is sorted.
+				break
+			}
+		}
+	}
+
+	switch receivedPkt.SSRC {
+	case d.ssrc:
+		d.insertFECPacket(receivedPkt)
+	case d.protectedStreamSSRC:
+		d.insertMediaPacket(receivedPkt)
+	}
+
+	d.discardOldRecoveredPackets()
+}
+
+func (d *fecDecoder) insertMediaPacket(receivedPkt rtp.Packet) {
+	for _, recoveredPacket := range d.recoveredPackets {
+		if recoveredPacket.SequenceNumber == receivedPkt.SequenceNumber {
+			return
+		}
+	}
+
+	d.recoveredPackets = append(d.recoveredPackets, receivedPkt)
+	sort.Slice(d.recoveredPackets, func(i, j int) bool {
+		return isNewerSeq(d.recoveredPackets[i].SequenceNumber, d.recoveredPackets[j].SequenceNumber)
+	})
+	d.updateCoveringFecPackets(receivedPkt)
+}
+
+func (d *fecDecoder) updateCoveringFecPackets(receivedPkt rtp.Packet) {
+	for _, fecPkt := range d.receivedFECPackets {
+		for _, protectedPacket := range fecPkt.protectedPackets {
+			if protectedPacket.seq == receivedPkt.SequenceNumber {
+				protectedPacket.packet = &receivedPkt
+			}
+		}
+	}
+}
+
+func (d *fecDecoder) insertFECPacket(fecPkt rtp.Packet) { //nolint:cyclop
+	for _, existingFECPacket := range d.receivedFECPackets {
+		if existingFECPacket.packet.SequenceNumber == fecPkt.SequenceNumber {
+			return
+		}
+	}
+
+	fec, err := parseFlexFEC03Header(fecPkt.Payload)
+	if err != nil {
+		d.logger.Errorf("failed to parse flexfec03 header: %v", err)
+
+		return
+	}
+
+	if fec.protectedSSRC != d.protectedStreamSSRC {
+		d.logger.Errorf("fec is protecting unknown ssrc, expected %d, got %d", fec.protectedSSRC, d.protectedStreamSSRC)
+
+		return
+	}
+
+	protectedSeqs := decodeMask(uint64(fec.mask0), 15, fec.seqNumBase)
+	if fec.mask1 != 0 {
+		protectedSeqs = append(protectedSeqs, decodeMask(uint64(fec.mask1), 31, fec.seqNumBase+15)...)
+	}
+	if fec.mask2 != 0 {
+		protectedSeqs = append(protectedSeqs, decodeMask(fec.mask2, 63, fec.seqNumBase+46)...)
+	}
+
+	if len(protectedSeqs) == 0 {
+		d.logger.Warn("empty fec packet mask")
+
+		return
+	}
+
+	protectedPackets := make([]*protectedPacket, 0, len(protectedSeqs))
+	protectedSeqIt := 0
+	recoveredPacketIt := 0
+
+	for protectedSeqIt < len(protectedSeqs) && recoveredPacketIt < len(d.recoveredPackets) {
+		switch {
+		case isNewerSeq(protectedSeqs[protectedSeqIt], d.recoveredPackets[recoveredPacketIt].SequenceNumber):
+			protectedPackets = append(protectedPackets, &protectedPacket{
+				seq:    protectedSeqs[protectedSeqIt],
+				packet: nil,
+			})
+			protectedSeqIt++
+		case isNewerSeq(d.recoveredPackets[recoveredPacketIt].SequenceNumber, protectedSeqs[protectedSeqIt]):
+			recoveredPacketIt++
+		default:
+			protectedPackets = append(protectedPackets, &protectedPacket{
+				seq:    protectedSeqs[protectedSeqIt],
+				packet: &d.recoveredPackets[recoveredPacketIt],
+			})
+			protectedSeqIt++
+			recoveredPacketIt++
+		}
+	}
+
+	for protectedSeqIt < len(protectedSeqs) {
+		protectedPackets = append(protectedPackets, &protectedPacket{
+			seq:    protectedSeqs[protectedSeqIt],
+			packet: nil,
+		})
+		protectedSeqIt++
+	}
+	d.receivedFECPackets = append(d.receivedFECPackets, fecPacketState{
+		packet:           fecPkt,
+		flexFec:          fec,
+		protectedPackets: protectedPackets,
+	})
+
+	sort.Slice(d.receivedFECPackets, func(i, j int) bool {
+		return isNewerSeq(d.receivedFECPackets[i].packet.SequenceNumber, d.receivedFECPackets[j].packet.SequenceNumber)
+	})
+
+	if len(d.receivedFECPackets) > d.maxFECPackets {
+		d.receivedFECPackets = d.receivedFECPackets[1:]
+	}
+}
+
+func (d *fecDecoder) attemptRecovery() []rtp.Packet {
+	recoveredPackets := make([]rtp.Packet, 0)
+	for {
+		packetsRecovered := 0
+		for _, fecPkt := range d.receivedFECPackets {
+			packetsMissing := 0
+			for _, pkt := range fecPkt.protectedPackets {
+				if pkt.packet == nil {
+					packetsMissing++
+					if packetsMissing > 1 {
+						break
+					}
+				}
+			}
+
+			if packetsMissing != 1 {
+				continue
+			}
+
+			recovered, err := d.recoverPacket(&fecPkt) //nolint:gosec
+			if err != nil {
+				d.logger.Errorf("failed to recover packet: %v", err)
+			}
+
+			recoveredPackets = append(recoveredPackets, recovered)
+			d.recoveredPackets = append(d.recoveredPackets, recovered)
+			sort.Slice(d.recoveredPackets, func(i, j int) bool {
+				return isNewerSeq(d.recoveredPackets[i].SequenceNumber, d.recoveredPackets[j].SequenceNumber)
+			})
+
+			d.updateCoveringFecPackets(recovered)
+			d.discardOldRecoveredPackets()
+			packetsRecovered++
+		}
+
+		if packetsRecovered == 0 {
+			break
+		}
+	}
+
+	return recoveredPackets
+}
+
+func (d *fecDecoder) recoverPacket(fec *fecPacketState) (rtp.Packet, error) {
+	// https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03#section-6.3.2
+
+	// 2. For the repair packet in T, extract the FEC bit string as the
+	//    first 80 bits of the FEC header.
+	headerRecovery := make([]byte, 12)
+	copy(headerRecovery, fec.packet.Payload[:10])
+
+	var seqnum uint16
+	for _, protectedPacket := range fec.protectedPackets {
+		if protectedPacket.packet != nil {
+			// 1.   For each of the source packets that are successfully received in
+			//      T, compute the 80-bit string by concatenating the first 64 bits
+			//      of their RTP header and the unsigned network-ordered 16-bit
+			//      representation of their length in bytes minus 12.
+			receivedHeader, err := protectedPacket.packet.Header.Marshal()
+			if err != nil {
+				return rtp.Packet{}, fmt.Errorf("marshal received header: %w", err)
+			}
+			binary.BigEndian.PutUint16(receivedHeader[2:4], uint16(protectedPacket.packet.MarshalSize()-12)) //nolint:gosec
+			for i := 0; i < 8; i++ {
+				headerRecovery[i] ^= receivedHeader[i]
+			}
+		} else {
+			seqnum = protectedPacket.seq
+		}
+	}
+
+	// set version to 2
+	headerRecovery[0] |= 0x80
+	headerRecovery[0] &= 0xbf
+	payloadLength := binary.BigEndian.Uint16(headerRecovery[2:4])
+	binary.BigEndian.PutUint16(headerRecovery[2:4], seqnum)
+	binary.BigEndian.PutUint32(headerRecovery[8:12], d.protectedStreamSSRC)
+
+	payloadRecovery := make([]byte, payloadLength)
+	copy(payloadRecovery, fec.flexFec.payload)
+	for _, protectedPacket := range fec.protectedPackets {
+		if protectedPacket.packet != nil {
+			packet, err := protectedPacket.packet.Marshal()
+			if err != nil {
+				return rtp.Packet{}, fmt.Errorf("marshal protected packet: %w", err)
+			}
+			for i := 0; i < minInt(int(payloadLength), len(packet)-12); i++ {
+				payloadRecovery[i] ^= packet[12+i]
+			}
+		}
+	}
+
+	headerRecovery = append(headerRecovery, payloadRecovery...) //nolint:makezero
+
+	var packet rtp.Packet
+	err := packet.Unmarshal(headerRecovery)
+	if err != nil {
+		return rtp.Packet{}, fmt.Errorf("unmarshal recovered: %w", err)
+	}
+
+	return packet, nil
+}
+
+func (d *fecDecoder) discardOldRecoveredPackets() {
+	const limit = 192
+	if len(d.recoveredPackets) > limit {
+		d.recoveredPackets = d.recoveredPackets[len(d.recoveredPackets)-192:]
+	}
+}
+
+func decodeMask(mask uint64, bitCount uint16, seqNumBase uint16) []uint16 {
+	res := make([]uint16, 0)
+	for i := uint16(0); i < bitCount; i++ {
+		if (mask>>(bitCount-1-i))&1 == 1 {
+			res = append(res, seqNumBase+i)
+		}
+	}
+
+	return res
+}
+
+type fecPacketState struct {
+	packet           rtp.Packet
+	flexFec          flexFec
+	protectedPackets []*protectedPacket
+}
+
+type flexFec struct {
+	protectedSSRC uint32
+	seqNumBase    uint16
+	mask0         uint16
+	mask1         uint32
+	mask2         uint64
+	payload       []byte
+}
+
+type protectedPacket struct {
+	seq    uint16
+	packet *rtp.Packet
+}
+
+func parseFlexFEC03Header(data []byte) (flexFec, error) {
+	if len(data) < 20 {
+		return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data))
+	}
+
+	rBit := (data[0] & 0x80) != 0
+	if rBit {
+		return flexFec{}, errRetransmissionBitSet
+	}
+
+	fBit := (data[0] & 0x40) != 0
+	if fBit {
+		return flexFec{}, errInflexibleGeneratorMatrix
+	}
+
+	ssrcCount := data[8]
+	if ssrcCount != 1 {
+		return flexFec{}, fmt.Errorf("%w: count %d", errMultipleSSRCProtection, ssrcCount)
+	}
+
+	protectedSSRC := binary.BigEndian.Uint32(data[12:])
+	seqNumBase := binary.BigEndian.Uint16(data[16:])
+	rawPacketMask := data[18:]
+	var payload []byte
+
+	kBit0 := (rawPacketMask[0] & 0x80) != 0
+	maskPart0 := binary.BigEndian.Uint16(rawPacketMask[0:2]) & 0x7FFF
+	var maskPart1 uint32
+	var maskPart2 uint64
+
+	if kBit0 { //nolint:nestif
+		payload = rawPacketMask[2:]
+	} else {
+		if len(data) < 24 {
+			return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data))
+		}
+
+		kBit1 := (rawPacketMask[2] & 0x80) != 0
+		maskPart1 = binary.BigEndian.Uint32(rawPacketMask[2:]) & 0x7FFFFFFF
+
+		if kBit1 {
+			payload = rawPacketMask[6:]
+		} else {
+			if len(data) < 32 {
+				return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data))
+			}
+
+			kBit2 := (rawPacketMask[6] & 0x80) != 0
+			maskPart2 = binary.BigEndian.Uint64(rawPacketMask[6:]) & 0x7FFFFFFFFFFFFFFF
+
+			if kBit2 {
+				payload = rawPacketMask[14:]
+			} else {
+				return flexFec{}, errLastOptionalMaskKBitSetToFalse
+			}
+		}
+	}
+
+	return flexFec{
+		protectedSSRC: protectedSSRC,
+		seqNumBase:    seqNumBase,
+		mask0:         maskPart0,
+		mask1:         maskPart1,
+		mask2:         maskPart2,
+		payload:       payload,
+	}, nil
+}
+
+func seqDiff(a, b uint16) uint16 {
+	return minUInt16(a-b, b-a)
+}
+
+func minInt(a, b int) int {
+	if a < b {
+		return a
+	}
+
+	return b
+}
+
+func minUInt16(a, b uint16) uint16 {
+	if a < b {
+		return a
+	}
+
+	return b
+}
+
+func abs(x int) int {
+	if x >= 0 {
+		return x
+	}
+
+	return -x
+}
+
+func isNewerSeq(prevValue, value uint16) bool {
+	// half-way mark
+	breakpoint := uint16(0x8000)
+	if value-prevValue == breakpoint {
+		return value > prevValue
+	}
+
+	return value != prevValue && (value-prevValue) < breakpoint
+}
diff --git a/pkg/flexfec/flexfec_encoder_03.go b/pkg/flexfec/flexfec_encoder_03.go
index ef912d4..8adaab9 100644
--- a/pkg/flexfec/flexfec_encoder_03.go
+++ b/pkg/flexfec/flexfec_encoder_03.go
@@ -36,9 +36,21 @@ func NewFlexEncoder03(payloadType uint8, ssrc uint32) *FlexEncoder03 {
 }
 
 // EncodeFec returns a list of generated RTP packets with FEC payloads that protect the specified mediaPackets.
-// This method does not account for missing RTP packets in the mediaPackets array nor does it account for
-// them being passed out of order.
+// This method returns nil in case of missing RTP packets in the mediaPackets array or packets passed out of order.
 func (flex *FlexEncoder03) EncodeFec(mediaPackets []rtp.Packet, numFecPackets uint32) []rtp.Packet {
+	// Check if mediaPackets is empty
+	if len(mediaPackets) == 0 {
+		return nil
+	}
+
+	// Check if RTP packets are in order by comparing sequence numbers
+	for i := 1; i < len(mediaPackets); i++ {
+		if mediaPackets[i].SequenceNumber != mediaPackets[i-1].SequenceNumber+1 {
+			// Packets are not in order or there are missing packets
+			return nil
+		}
+	}
+
 	// Start by defining which FEC packets cover which media packets
 	if flex.coverage == nil {
 		flex.coverage = NewCoverage(mediaPackets, numFecPackets)
@@ -89,7 +101,7 @@ func (flex *FlexEncoder03) encodeFlexFecPacket(fecPacketIndex uint32, mediaBaseS
 	return packet
 }
 
-func (flex *FlexEncoder03) encodeFlexFecHeader(
+func (flex *FlexEncoder03) encodeFlexFecHeader( //nolint:cyclop
 	mediaPackets *util.MediaPacketIterator,
 	mask1 uint16,
 	optionalMask2 uint32,
@@ -122,7 +134,7 @@ func (flex *FlexEncoder03) encodeFlexFecHeader(
 
 	// Get header size - This depends on the size of the bitmask.
 	headerSize := BaseFec03HeaderSize
-	if optionalMask2 > 0 {
+	if optionalMask2 > 0 || optionalMask3 > 0 {
 		headerSize += 4
 	}
 	if optionalMask3 > 0 {
@@ -184,7 +196,7 @@ func (flex *FlexEncoder03) encodeFlexFecHeader(
 	// Write the bitmasks to the header
 	binary.BigEndian.PutUint16(flexFecHeader[18:20], mask1)
 
-	if optionalMask2 == 0 {
+	if optionalMask2 == 0 && optionalMask3 == 0 {
 		flexFecHeader[18] |= 0b10000000
 
 		return flexFecHeader
@@ -195,26 +207,39 @@ func (flex *FlexEncoder03) encodeFlexFecHeader(
 		flexFecHeader[20] |= 0b10000000
 	} else {
 		binary.BigEndian.PutUint64(flexFecHeader[24:32], optionalMask3)
+		flexFecHeader[24] |= 0b10000000
 	}
 
 	return flexFecHeader
 }
 
 func (flex *FlexEncoder03) encodeFlexFecRepairPayload(mediaPackets *util.MediaPacketIterator) []byte {
-	flexFecPayload := make([]byte, len(mediaPackets.First().Payload))
+	flexFecPayload := make([]byte, mediaPackets.First().MarshalSize()-BaseRTPHeaderSize)
+	tmpMediaPacketBuf := make([]byte, 0)
 
 	for mediaPackets.HasNext() {
-		mediaPacketPayload := mediaPackets.Next().Payload
+		mediaPacket := mediaPackets.Next()
+
+		if mediaPacket.MarshalSize() > len(tmpMediaPacketBuf) {
+			tmpMediaPacketBuf = make([]byte, mediaPacket.MarshalSize())
+		}
+
+		n, err := mediaPacket.MarshalTo(tmpMediaPacketBuf)
 
-		if len(flexFecPayload) < len(mediaPacketPayload) {
+		if n == 0 || err != nil {
+			return nil
+		}
+
+		if len(flexFecPayload) < mediaPacket.MarshalSize()-BaseRTPHeaderSize {
 			// Expected FEC packet payload is bigger that what we can currently store,
 			// we need to resize.
-			flexFecPayloadTmp := make([]byte, len(mediaPacketPayload))
+			flexFecPayloadTmp := make([]byte, mediaPacket.MarshalSize()-BaseRTPHeaderSize)
 			copy(flexFecPayloadTmp, flexFecPayload)
 			flexFecPayload = flexFecPayloadTmp
 		}
-		for byteIndex := 0; byteIndex < len(mediaPacketPayload); byteIndex++ {
-			flexFecPayload[byteIndex] ^= mediaPacketPayload[byteIndex]
+
+		for byteIndex := 0; byteIndex < mediaPacket.MarshalSize()-BaseRTPHeaderSize; byteIndex++ {
+			flexFecPayload[byteIndex] ^= tmpMediaPacketBuf[byteIndex+BaseRTPHeaderSize]
 		}
 	}
 
diff --git a/pkg/flexfec/util/media_packet_iterator.go b/pkg/flexfec/util/media_packet_iterator.go
index 16f93f6..a9cc5fd 100644
--- a/pkg/flexfec/util/media_packet_iterator.go
+++ b/pkg/flexfec/util/media_packet_iterator.go
@@ -40,7 +40,7 @@ func (m *MediaPacketIterator) Next() *rtp.Packet {
 	if m.nextIndex == len(m.coveredIndices) {
 		return nil
 	}
-	packet := m.mediaPackets[m.nextIndex]
+	packet := m.mediaPackets[m.coveredIndices[m.nextIndex]]
 	m.nextIndex++
 
 	return &packet
@@ -48,5 +48,9 @@ func (m *MediaPacketIterator) Next() *rtp.Packet {
 
 // First returns the first media packet to iterate through.
 func (m *MediaPacketIterator) First() *rtp.Packet {
-	return &m.mediaPackets[0]
+	if len(m.coveredIndices) == 0 {
+		return nil
+	}
+
+	return &m.mediaPackets[m.coveredIndices[0]]
 }


Test patch:
diff --git a/pkg/flexfec/flexfec_03_test.go b/pkg/flexfec/flexfec_03_test.go
new file mode 100644
index 0000000..3a46304
--- /dev/null
+++ b/pkg/flexfec/flexfec_03_test.go
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package flexfec
+
+import (
+	"testing"
+
+	"github.com/pion/rtp"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	payloadType         = uint8(49)
+	ssrc                = uint32(867589674)
+	protectedStreamSSRC = uint32(476325762)
+)
+
+func checkAnyPacketCanBeRecovered(t *testing.T, mediaPackets []rtp.Packet, fecPackets []rtp.Packet) {
+	t.Helper()
+
+	for lost := 0; lost < len(mediaPackets); lost++ {
+		decoder := newFECDecoder(ssrc, protectedStreamSSRC)
+		recoveredPackets := make([]rtp.Packet, 0)
+		// lose one packet
+		for _, mediaPacket := range mediaPackets[:lost] {
+			recoveredPackets = append(recoveredPackets, decoder.DecodeFec(mediaPacket)...)
+		}
+		for _, mediaPacket := range mediaPackets[lost+1:] {
+			recoveredPackets = append(recoveredPackets, decoder.DecodeFec(mediaPacket)...)
+		}
+		assert.Empty(t, recoveredPackets)
+
+		for _, fecPacket := range fecPackets {
+			recoveredPackets = append(recoveredPackets, decoder.DecodeFec(fecPacket)...)
+		}
+
+		require.Len(t, recoveredPackets, 1)
+		assert.Equal(t, mediaPackets[lost], recoveredPackets[0])
+	}
+}
+
+func generatePackets(t *testing.T, seqs []uint16) ([]rtp.Packet, []rtp.Packet) {
+	t.Helper()
+
+	mediaPackets := make([]rtp.Packet, 0)
+	for i, seq := range seqs {
+		payload := []byte{
+			// Payload
+			1, 2, 3, 4, 5, byte(i),
+		}
+		packet := rtp.Packet{
+			Header: rtp.Header{
+				Marker:      true,
+				Extension:   false,
+				Version:     2,
+				PayloadType: 96,
+
+				SequenceNumber: seq,
+				Timestamp:      3653407706,
+				SSRC:           protectedStreamSSRC,
+				CSRC:           []uint32{},
+			},
+			Payload: payload,
+		}
+
+		extension := []byte{0xAA, 0xAA}
+		err := packet.SetExtension(1, extension)
+		require.NoError(t, err)
+
+		mediaPackets = append(mediaPackets, packet)
+	}
+
+	encoder := NewFlexEncoder03(payloadType, ssrc)
+	fecPackets := encoder.EncodeFec(mediaPackets, 2)
+
+	return mediaPackets, fecPackets
+}
+
+func TestFlexFec03_SimpleRoundTrip(t *testing.T) {
+	tests := []struct {
+		name string
+		seqs []uint16
+	}{
+		{
+			name: "first",
+			seqs: []uint16{1, 2, 3, 4, 5},
+		},
+		{
+			name: "last",
+			seqs: []uint16{65531, 65532, 65533, 65534, 65535},
+		},
+		{
+			name: "wrap",
+			seqs: []uint16{65533, 65534, 65535, 0, 1},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			mediaPackets, fecPackets := generatePackets(t, test.seqs)
+			require.Len(t, mediaPackets, len(test.seqs))
+			checkAnyPacketCanBeRecovered(t, mediaPackets, fecPackets)
+		})
+	}
+}
+
+func TestFlexFec03_WholeRangeRoundTrip(t *testing.T) {
+	var seqs []uint16
+	const maxFlexFEC03MediaPackets = 109
+	for i := 0; i < maxFlexFEC03MediaPackets; i++ {
+		seqs = append(seqs, uint16(i)) //nolint:gosec
+	}
+
+	mediaPackets, fecPackets := generatePackets(t, seqs)
+	require.Len(t, mediaPackets, len(seqs))
+	checkAnyPacketCanBeRecovered(t, mediaPackets, fecPackets)
+}
diff --git a/pkg/flexfec/flexfec_coverage_test.go b/pkg/flexfec/flexfec_coverage_test.go
new file mode 100644
index 0000000..13721c5
--- /dev/null
+++ b/pkg/flexfec/flexfec_coverage_test.go
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package flexfec
+
+import (
+	"testing"
+
+	"github.com/pion/interceptor/pkg/flexfec/util"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMaskExtractors(t *testing.T) {
+	tests := []struct {
+		name     string
+		setBits  []uint32
+		mask1    uint16
+		mask2    uint32
+		mask3    uint64
+		mask3_03 uint64
+	}{
+		{
+			name:     "Empty mask",
+			setBits:  []uint32{},
+			mask1:    0,
+			mask2:    0,
+			mask3:    0,
+			mask3_03: 0,
+		},
+		{
+			name:     "Single bit in each mask",
+			setBits:  []uint32{5, 20, 50},
+			mask1:    0x200,             // bit 5
+			mask2:    0x2000000,         // bit 20
+			mask3:    0x800000000000000, // bit 50
+			mask3_03: 0x400000000000000, // bit 50
+		},
+		{
+			name:     "Multiple bits in each mask",
+			setBits:  []uint32{0, 7, 14, 15, 30, 45, 46, 80, 108, 109},
+			mask1:    0x4081,             // bits 0, 7, 14
+			mask2:    0x40008001,         // bits 15, 30, 45
+			mask3:    0x8000000020000003, // bits 46, 80, 108, 109
+			mask3_03: 0x4000000010000001, // bits 46, 80, 108
+		},
+		{
+			name:     "Boundary values",
+			setBits:  []uint32{0, 14, 15, 45, 46, 108, 109},
+			mask1:    0x4001,             // bits 0, 14
+			mask2:    0x40000001,         // bits 15, 45
+			mask3:    0x8000000000000003, // bits 46, 108, 109
+			mask3_03: 0x4000000000000001, // bits 46, 108
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mask := util.BitArray{}
+
+			for _, bit := range tt.setBits {
+				mask.SetBit(bit)
+			}
+
+			actualMask1 := extractMask1(mask)
+			actualMask2 := extractMask2(mask)
+			actualMask3 := extractMask3(mask)
+			actualMask3_03 := extractMask3_03(mask)
+
+			assert.Equal(t, tt.mask1, actualMask1, "Mask1 mismatch")
+			assert.Equal(t, tt.mask2, actualMask2, "Mask2 mismatch")
+			assert.Equal(t, tt.mask3, actualMask3, "Mask3 mismatch")
+			assert.Equal(t, tt.mask3_03, actualMask3_03, "Mask3_03 mismatch")
+		})
+	}
+}
diff --git a/pkg/flexfec/flexfec_encoder_03_test.go b/pkg/flexfec/flexfec_encoder_03_test.go
new file mode 100644
index 0000000..dc16054
--- /dev/null
+++ b/pkg/flexfec/flexfec_encoder_03_test.go
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package flexfec
+
+import (
+	"testing"
+
+	"github.com/pion/rtp"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFlexEncoder03_EncodeFec_EmptyMediaPackets(t *testing.T) {
+	encoder := NewFlexEncoder03(96, 1234)
+	var mediaPackets []rtp.Packet
+
+	result := encoder.EncodeFec(mediaPackets, 1)
+
+	assert.Nil(t, result, "EncodeFec should return nil when mediaPackets is empty")
+}
+
+func TestFlexEncoder03_EncodeFec_OutOfOrderPackets(t *testing.T) {
+	encoder := NewFlexEncoder03(96, 1234)
+	mediaPackets := []rtp.Packet{
+		{
+			Header: rtp.Header{
+				SequenceNumber: 2,
+				SSRC:           1234,
+			},
+		},
+		{
+			Header: rtp.Header{
+				SequenceNumber: 1, // Out of order (should be 3)
+				SSRC:           1234,
+			},
+		},
+	}
+
+	result := encoder.EncodeFec(mediaPackets, 1)
+
+	assert.Nil(t, result, "EncodeFec should return nil when packets are out of order")
+}
+
+func TestFlexEncoder03_EncodeFec_MissingPackets(t *testing.T) {
+	encoder := NewFlexEncoder03(96, 1234)
+	mediaPackets := []rtp.Packet{
+		{
+			Header: rtp.Header{
+				SequenceNumber: 1,
+				SSRC:           1234,
+			},
+		},
+		{
+			Header: rtp.Header{
+				SequenceNumber: 3, // Missing packet with sequence number 2
+				SSRC:           1234,
+			},
+		},
+	}
+
+	result := encoder.EncodeFec(mediaPackets, 1)
+	assert.Nil(t, result, "EncodeFec should return nil when there are missing packets")
+}
+
+func TestFlexEncoder03_EncodeFec_DifferentPayloadSizes(t *testing.T) {
+	encoder := NewFlexEncoder03(96, 1234)
+
+	smallPayload := []byte{1, 2, 3}
+	largePayload := []byte{1, 2, 3, 4, 5, 6, 7, 8}
+
+	mediaPackets := []rtp.Packet{
+		{
+			Header: rtp.Header{
+				SequenceNumber: 1,
+				SSRC:           1234,
+			},
+			Payload: smallPayload,
+		},
+		{
+			Header: rtp.Header{
+				SequenceNumber: 2,
+				SSRC:           1234,
+			},
+			Payload: largePayload,
+		},
+	}
+
+	fecPackets := encoder.EncodeFec(mediaPackets, 1)
+
+	assert.NotNil(t, fecPackets, "EncodeFec should return FEC packets")
+	assert.Equal(t, 1, len(fecPackets), "EncodeFec should return 1 FEC packet")
+
+	expectedPayloadSize := len(largePayload)
+	actualPayloadSize := len(fecPackets[0].Payload) - BaseFec03HeaderSize
+
+	assert.Equal(t, expectedPayloadSize, actualPayloadSize,
+		"FEC payload size should match the size of the largest media packet payload")
+}
diff --git a/pkg/flexfec/util/media_packet_iterator_test.go b/pkg/flexfec/util/media_packet_iterator_test.go
new file mode 100644
index 0000000..36f158e
--- /dev/null
+++ b/pkg/flexfec/util/media_packet_iterator_test.go
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package util_test
+
+import (
+	"testing"
+
+	"github.com/pion/interceptor/pkg/flexfec/util"
+	"github.com/pion/rtp"
+	"github.com/stretchr/testify/assert"
+)
+
+func createTestMediaPackets() []rtp.Packet {
+	return []rtp.Packet{
+		{Header: rtp.Header{SequenceNumber: 1, SSRC: 1}},
+		{Header: rtp.Header{SequenceNumber: 2, SSRC: 1}},
+		{Header: rtp.Header{SequenceNumber: 3, SSRC: 1}},
+		{Header: rtp.Header{SequenceNumber: 4, SSRC: 1}},
+		{Header: rtp.Header{SequenceNumber: 5, SSRC: 1}},
+	}
+}
+
+func TestNewMediaPacketIterator(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{1, 2, 4}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	assert.NotNil(t, iterator)
+}
+
+func TestMediaPacketIteratorReset(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{1, 2, 4}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	// Advance the iterator
+	_ = iterator.Next()
+	_ = iterator.Next()
+
+	// Reset the iterator
+	result := iterator.Reset()
+
+	// After reset, HasNext should be true again
+	assert.True(t, iterator.HasNext())
+
+	// Reset should return the iterator for chaining
+	assert.Equal(t, iterator, result, "Reset should return the iterator for chaining")
+}
+
+func TestMediaPacketIteratorHasNext(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{1, 2, 4}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	// Initially, HasNext should be true
+	assert.True(t, iterator.HasNext())
+
+	// Advance to the last element
+	_ = iterator.Next()
+	_ = iterator.Next()
+	assert.True(t, iterator.HasNext())
+
+	// Advance past the last element
+	_ = iterator.Next()
+	assert.False(t, iterator.HasNext())
+}
+
+func TestMediaPacketIteratorNext(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{1, 2, 4}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	// First call to Next should return the first covered packet
+	packet := iterator.Next()
+	assert.NotNil(t, packet)
+	assert.Equal(t, uint16(2), packet.SequenceNumber)
+
+	// Second call to Next should return the second covered packet
+	packet = iterator.Next()
+	assert.NotNil(t, packet)
+	assert.Equal(t, uint16(3), packet.SequenceNumber)
+
+	// Third call to Next should return the third covered packet
+	packet = iterator.Next()
+	assert.NotNil(t, packet)
+	assert.Equal(t, uint16(5), packet.SequenceNumber)
+
+	// Fourth call to Next should return nil
+	packet = iterator.Next()
+	assert.Nil(t, packet)
+}
+
+func TestMediaPacketIteratorFirst(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{1, 2, 4}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	// First should return the first covered packet
+	packet := iterator.First()
+	assert.NotNil(t, packet)
+	assert.Equal(t, uint16(2), packet.SequenceNumber)
+
+	// First should not advance the iterator, so Next should still return the first packet
+	nextPacket := iterator.Next()
+	assert.NotNil(t, nextPacket)
+	assert.Equal(t, uint16(2), nextPacket.SequenceNumber)
+
+	// Even after advancing the iterator, First should still return the first packet
+	_ = iterator.Next()
+	packet = iterator.First()
+	assert.NotNil(t, packet)
+	assert.Equal(t, uint16(2), packet.SequenceNumber)
+}
+
+func TestMediaPacketIteratorEmptyCoveredIndices(t *testing.T) {
+	mediaPackets := createTestMediaPackets()
+	coveredIndices := []uint32{}
+	iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices)
+
+	assert.False(t, iterator.HasNext())
+	assert.Nil(t, iterator.Next())
+}