Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
265 lines
5.8 KiB
Go
265 lines
5.8 KiB
Go
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package h265reader implements a H265/HEVC Annex-B Reader
|
|
package h265reader
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
)
|
|
|
|
// H265Reader reads data from stream and constructs h265 nal units.
|
|
type H265Reader struct {
|
|
stream io.Reader
|
|
nalBuffer []byte
|
|
countOfConsecutiveZeroBytes int
|
|
nalPrefixParsed bool
|
|
readBuffer []byte
|
|
tmpReadBuf []byte
|
|
includeSEI bool
|
|
}
|
|
|
|
var (
|
|
errNilReader = errors.New("stream is nil")
|
|
errDataIsNotH265Stream = errors.New("data is not a H265/HEVC bitstream")
|
|
)
|
|
|
|
func (reader *H265Reader) shouldSkipNAL(naluType NalUnitType) bool {
|
|
return !reader.includeSEI && (naluType == NalUnitTypePrefixSei || naluType == NalUnitTypeSuffixSei)
|
|
}
|
|
|
|
// NewReader creates new H265Reader.
|
|
func NewReader(in io.Reader) (*H265Reader, error) {
|
|
if in == nil {
|
|
return nil, errNilReader
|
|
}
|
|
|
|
reader := &H265Reader{
|
|
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 H265Reader.
|
|
type Option func(*H265Reader) error
|
|
|
|
// NewReaderWithOptions creates new H265Reader with options.
|
|
// The default behavior is to skip SEI NAL units.
|
|
func NewReaderWithOptions(in io.Reader, options ...Option) (*H265Reader, 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 *H265Reader) error {
|
|
r.includeSEI = include
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NAL H.265/HEVC Network Abstraction Layer.
|
|
type NAL struct {
|
|
PictureOrderCount uint32
|
|
|
|
/* NAL Unit header https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4
|
|
+---------------+---------------+
|
|
|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
|F| Type | LayerId | TID |
|
|
+-------------+-----------------+
|
|
*/
|
|
ForbiddenZeroBit bool
|
|
NalUnitType NalUnitType
|
|
LayerID uint8
|
|
TemporalIDPlus1 uint8
|
|
|
|
Data []byte // header bytes + rbsp
|
|
}
|
|
|
|
func (reader *H265Reader) 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 *H265Reader) bitStreamStartsWithH265Prefix() (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, errDataIsNotH265Stream
|
|
}
|
|
|
|
nalPrefix3BytesFound := bytes.Equal(nalPrefix3Bytes, prefixBuffer[:3])
|
|
if n == 3 {
|
|
if nalPrefix3BytesFound {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
return 0, errDataIsNotH265Stream
|
|
}
|
|
|
|
// 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, errDataIsNotH265Stream
|
|
}
|
|
|
|
// 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 *H265Reader) NextNAL() (*NAL, error) {
|
|
if !reader.nalPrefixParsed {
|
|
_, err := reader.bitStreamStartsWithH265Prefix()
|
|
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 {
|
|
naluType := NalUnitType((reader.nalBuffer[0] & 0x7E) >> 1)
|
|
if reader.shouldSkipNAL(naluType) {
|
|
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 *H265Reader) 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,
|
|
NalUnitType: NalUnitTypeTrailN,
|
|
LayerID: 0,
|
|
TemporalIDPlus1: 0,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (h *NAL) parseHeader() {
|
|
if len(h.Data) < 2 {
|
|
return
|
|
}
|
|
|
|
// H.265 NAL header is 2 bytes
|
|
firstByte := h.Data[0]
|
|
secondByte := h.Data[1]
|
|
|
|
h.ForbiddenZeroBit = (firstByte & 0x80) != 0
|
|
h.NalUnitType = NalUnitType((firstByte & 0x7E) >> 1)
|
|
h.LayerID = ((firstByte & 0x01) << 5) | ((secondByte & 0xF8) >> 3)
|
|
h.TemporalIDPlus1 = secondByte & 0x07
|
|
}
|