Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require github.com/stretchr/testify v1.11.1

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pion/randutil v0.1.0
github.com/pion/rtp v1.10.5
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtp v1.10.5 h1:ip0HhO/wYZqQ4bKS+R99KnZh/GRCmIT0jDXikub7vlE=
github.com/pion/rtp v1.10.5/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
239 changes: 239 additions & 0 deletions h264reader/h264reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

// Package h264reader implements a H264 Annex-B Reader
package h264reader

import (
"bytes"
"errors"
"io"
)

// H264Reader reads data from stream and constructs h264 nal units.
type H264Reader struct {
stream io.Reader
nalBuffer []byte
countOfConsecutiveZeroBytes int
nalPrefixParsed bool
readBuffer []byte
tmpReadBuf []byte
includeSEI bool
}

var (
errNilReader = errors.New("stream is nil")
errDataIsNotH264Stream = errors.New("data is not a H264 bitstream")
)

// NewReader creates new H264Reader.
func NewReader(in io.Reader) (*H264Reader, error) {
if in == nil {
return nil, errNilReader
}

reader := &H264Reader{
stream: in,
nalBuffer: make([]byte, 0),
nalPrefixParsed: false,
readBuffer: make([]byte, 0),
tmpReadBuf: make([]byte, 4096),
includeSEI: false,
}

return reader, nil
}

// Option configures the behavior of H264Reader.
type Option func(*H264Reader) error

// NewReaderWithOptions creates new H264Reader with options.
// The default behavior is to skip SEI NAL units.
func NewReaderWithOptions(in io.Reader, options ...Option) (*H264Reader, error) {
reader, err := NewReader(in)
if err != nil {
return nil, err
}

for _, option := range options {
if err := option(reader); err != nil {
return nil, err
}
}

return reader, nil
}

// WithIncludeSEI controls whether SEI (Supplemental Enhancement Information) NAL units are returned.
// Default is false (SEI is skipped).
func WithIncludeSEI(include bool) Option {
return func(r *H264Reader) error {
r.includeSEI = include

return nil
}
}

// NAL H.264 Network Abstraction Layer.
type NAL struct {
PictureOrderCount uint32

// NAL header
ForbiddenZeroBit bool
RefIdc uint8
UnitType NalUnitType

Data []byte // header byte + rbsp
}

func (reader *H264Reader) read(numToRead int) (data []byte, e error) {
for len(reader.readBuffer) < numToRead {
n, err := reader.stream.Read(reader.tmpReadBuf)
if err != nil {
return nil, err
}
if n == 0 {
break
}
reader.readBuffer = append(reader.readBuffer, reader.tmpReadBuf[0:n]...)
}

numShouldRead := min(numToRead, len(reader.readBuffer))
data = reader.readBuffer[0:numShouldRead]
reader.readBuffer = reader.readBuffer[numShouldRead:]

return data, nil
}

func (reader *H264Reader) bitStreamStartsWithH264Prefix() (prefixLength int, e error) {
nalPrefix3Bytes := []byte{0, 0, 1}
nalPrefix4Bytes := []byte{0, 0, 0, 1}

prefixBuffer, e := reader.read(4)
if e != nil {
return prefixLength, e
}

n := len(prefixBuffer)

if n == 0 {
return 0, io.EOF
}

if n < 3 {
return 0, errDataIsNotH264Stream
}

nalPrefix3BytesFound := bytes.Equal(nalPrefix3Bytes, prefixBuffer[:3])
if n == 3 {
if nalPrefix3BytesFound {
return 0, io.EOF
}

return 0, errDataIsNotH264Stream
}

// n == 4
if nalPrefix3BytesFound {
reader.nalBuffer = append(reader.nalBuffer, prefixBuffer[3])

return 3, nil
}

nalPrefix4BytesFound := bytes.Equal(nalPrefix4Bytes, prefixBuffer)
if nalPrefix4BytesFound {
return 4, nil
}

return 0, errDataIsNotH264Stream
}

// NextNAL reads from stream and returns then next NAL,
// and an error if there is incomplete frame data.
// Returns all nil values when no more NALs are available.
func (reader *H264Reader) NextNAL() (*NAL, error) {
if !reader.nalPrefixParsed {
_, err := reader.bitStreamStartsWithH264Prefix()
if err != nil {
return nil, err
}

reader.nalPrefixParsed = true
}

for {
buffer, err := reader.read(1)
if err != nil {
break
}

n := len(buffer)

if n != 1 {
break
}
readByte := buffer[0]
nalFound := reader.processByte(readByte)
if nalFound {
nal := newNal(reader.nalBuffer)
nal.parseHeader()
if !reader.includeSEI && nal.UnitType == NalUnitTypeSEI {
reader.nalBuffer = nil

continue
}

break
}

reader.nalBuffer = append(reader.nalBuffer, readByte)
}

if len(reader.nalBuffer) == 0 {
return nil, io.EOF
}

nal := newNal(reader.nalBuffer)
reader.nalBuffer = nil
nal.parseHeader()

return nal, nil
}

func (reader *H264Reader) processByte(readByte byte) (nalFound bool) {
nalFound = false

switch readByte {
case 0:
reader.countOfConsecutiveZeroBytes++
case 1:
if reader.countOfConsecutiveZeroBytes >= 2 {
countOfConsecutiveZeroBytesInPrefix := 2
if reader.countOfConsecutiveZeroBytes > 2 {
countOfConsecutiveZeroBytesInPrefix = 3
}

if nalUnitLength := len(reader.nalBuffer) - countOfConsecutiveZeroBytesInPrefix; nalUnitLength > 0 {
reader.nalBuffer = reader.nalBuffer[0:nalUnitLength]
nalFound = true
}
}

reader.countOfConsecutiveZeroBytes = 0
default:
reader.countOfConsecutiveZeroBytes = 0
}

return nalFound
}

func newNal(data []byte) *NAL {
return &NAL{PictureOrderCount: 0, ForbiddenZeroBit: false, RefIdc: 0, UnitType: NalUnitTypeUnspecified, Data: data}
}

func (h *NAL) parseHeader() {
firstByte := h.Data[0]
h.ForbiddenZeroBit = (((firstByte & 0x80) >> 7) == 1) // 0x80 = 0b10000000
h.RefIdc = (firstByte & 0x60) >> 5 // 0x60 = 0b01100000
h.UnitType = NalUnitType((firstByte & 0x1F) >> 0) // 0x1F = 0b00011111
}
Loading
Loading