From 9795e9175aca577f310226dbcf6345bded6b6382 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 15 Nov 2022 23:01:26 +0100 Subject: [PATCH] add H265 decoder and encoder (#154) --- README.md | 5 +- examples/client-publish-codec-h265/main.go | 71 ++++++ examples/client-read-codec-h265/main.go | 77 +++++++ examples/client-read-codec-lpcm/main.go | 2 +- pkg/rtph264/decoder_test.go | 26 --- pkg/rtph265/decoder.go | 134 +++++++++++ pkg/rtph265/decoder_test.go | 161 ++++++++++++++ pkg/rtph265/encoder.go | 247 +++++++++++++++++++++ pkg/rtph265/encoder_test.go | 44 ++++ pkg/rtph265/rtph265.go | 6 + track_h265.go | 33 +++ track_test.go | 4 +- 12 files changed, 781 insertions(+), 29 deletions(-) create mode 100644 examples/client-publish-codec-h265/main.go create mode 100644 examples/client-read-codec-h265/main.go create mode 100644 pkg/rtph265/decoder.go create mode 100644 pkg/rtph265/decoder_test.go create mode 100644 pkg/rtph265/encoder.go create mode 100644 pkg/rtph265/encoder_test.go create mode 100644 pkg/rtph265/rtph265.go diff --git a/README.md b/README.md index 07f5e4eb..ec1b4dfc 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,10 @@ Features: * Compute and provide SSRC, RTP-Info to clients * Generate RTCP sender reports * Utilities - * Encode and decode codec-specific frames to/from RTP packets. The following codecs are supported: + * Encode and decode codec-specific frames into/from RTP packets. The following codecs are supported: * Video * H264 + * H265 * VP8 * VP9 * Audio @@ -73,6 +74,7 @@ Features: * [client-read-codec-h264](examples/client-read-codec-h264/main.go) * [client-read-codec-h264-convert-to-jpeg](examples/client-read-codec-h264-convert-to-jpeg/main.go) * [client-read-codec-h264-save-to-disk](examples/client-read-codec-h264-save-to-disk/main.go) +* [client-read-codec-h265](examples/client-read-codec-h265/main.go) * [client-read-codec-lpcm](examples/client-read-codec-lpcm/main.go) * [client-read-codec-mpeg4audio](examples/client-read-codec-mpeg4audio/main.go) * [client-read-codec-opus](examples/client-read-codec-opus/main.go) @@ -86,6 +88,7 @@ Features: * [client-read-republish](examples/client-read-republish/main.go) * [client-publish-codec-g722](examples/client-publish-codec-g722/main.go) * [client-publish-codec-h264](examples/client-publish-codec-h264/main.go) +* [client-publish-codec-h265](examples/client-publish-codec-h265/main.go) * [client-publish-codec-lpcm](examples/client-publish-codec-lpcm/main.go) * [client-publish-codec-mpeg4audio](examples/client-publish-codec-mpeg4audio/main.go) * [client-publish-codec-opus](examples/client-publish-codec-opus/main.go) diff --git a/examples/client-publish-codec-h265/main.go b/examples/client-publish-codec-h265/main.go new file mode 100644 index 00000000..f09e3a1a --- /dev/null +++ b/examples/client-publish-codec-h265/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "log" + "net" + + "github.com/aler9/gortsplib" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. generate RTP/H265 packets with GStreamer +// 2. connect to a RTSP server, announce an H265 track +// 3. route the packets from GStreamer to the server + +func main() { + // open a listener to receive RTP/H265 packets + pc, err := net.ListenPacket("udp", "localhost:9000") + if err != nil { + panic(err) + } + defer pc.Close() + + log.Println("Waiting for a RTP/H265 stream on UDP port 9000 - you can send one with GStreamer:\n" + + "gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080" + + " ! x265enc speed-preset=veryfast tune=zerolatency bitrate=600000" + + " ! rtph265pay ! udpsink host=127.0.0.1 port=9000") + + // wait for first packet + buf := make([]byte, 2048) + n, _, err := pc.ReadFrom(buf) + if err != nil { + panic(err) + } + log.Println("stream connected") + + // create an H265 track + track := &gortsplib.TrackH265{ + PayloadType: 96, + } + + // connect to the server and start publishing the track + c := gortsplib.Client{} + err = c.StartPublishing("rtsp://localhost:8554/mystream", + gortsplib.Tracks{track}) + if err != nil { + panic(err) + } + defer c.Close() + + var pkt rtp.Packet + for { + // parse RTP packet + err = pkt.Unmarshal(buf[:n]) + if err != nil { + panic(err) + } + + // route RTP packet to the server + err = c.WritePacketRTP(0, &pkt) + if err != nil { + panic(err) + } + + // read another RTP packet from source + n, _, err = pc.ReadFrom(buf) + if err != nil { + panic(err) + } + } +} diff --git a/examples/client-read-codec-h265/main.go b/examples/client-read-codec-h265/main.go new file mode 100644 index 00000000..c3891619 --- /dev/null +++ b/examples/client-read-codec-h265/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + + "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/pkg/url" +) + +// This example shows how to +// 1. connect to a RTSP server and read all tracks on a path +// 2. check if there's an H265 track +// 3. get access units of that track + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published tracks + tracks, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the H265 track + track := func() *gortsplib.TrackH265 { + for _, track := range tracks { + if track, ok := track.(*gortsplib.TrackH265); ok { + return track + } + } + return nil + }() + if track == nil { + panic("H265 track not found") + } + + // setup RTP/H265->H265 decoder + dec := track.CreateDecoder() + + // called when a RTP packet arrives + c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { + // convert RTP packets into NALUs + nalus, pts, err := dec.Decode(ctx.Packet) + if err != nil { + return + } + + fmt.Println("PTS", pts) + + for _, nalu := range nalus { + log.Printf("received NALU of size %d\n", len(nalu)) + } + } + + // setup and read the H265 track only + err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-codec-lpcm/main.go b/examples/client-read-codec-lpcm/main.go index 82b4bcdc..f0c10719 100644 --- a/examples/client-read-codec-lpcm/main.go +++ b/examples/client-read-codec-lpcm/main.go @@ -52,7 +52,7 @@ func main() { // called when a RTP packet arrives c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode an LPCM packet from the RTP packet + // decode LPCM samples from the RTP packet op, _, err := dec.Decode(ctx.Packet) if err != nil { return diff --git a/pkg/rtph264/decoder_test.go b/pkg/rtph264/decoder_test.go index 3edaad8b..93031bfa 100644 --- a/pkg/rtph264/decoder_test.go +++ b/pkg/rtph264/decoder_test.go @@ -57,32 +57,6 @@ var cases = []struct { }, }, }, - { - "negative timestamp", - [][]byte{ - mergeBytes( - []byte{0x05}, - bytes.Repeat([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, 8), - ), - }, - -20 * time.Millisecond, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - Timestamp: 2289524557, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x05}, - bytes.Repeat([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, 8), - ), - }, - }, - }, { "fragmented", [][]byte{ diff --git a/pkg/rtph265/decoder.go b/pkg/rtph265/decoder.go new file mode 100644 index 00000000..57058627 --- /dev/null +++ b/pkg/rtph265/decoder.go @@ -0,0 +1,134 @@ +package rtph265 + +import ( + "errors" + "fmt" + "time" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/pkg/rtptimedec" +) + +const ( + maxNALUSize = 3 * 1024 * 1024 +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// Decoder is a RTP/H265 decoder. +type Decoder struct { + // indicates that NALUs have an additional field that specifies the decoding order. + MaxDONDiff int + + timeDecoder *rtptimedec.Decoder + fragmentedSize int + fragments [][]byte +} + +// Init initializes the decoder. +func (d *Decoder) Init() { + d.timeDecoder = rtptimedec.New(rtpClockRate) +} + +// Decode decodes NALUs from a RTP/H265 packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) { + if d.MaxDONDiff != 0 { + return nil, 0, fmt.Errorf("MaxDONDiff != 0 is not supported (yet)") + } + + if len(pkt.Payload) < 2 { + d.fragments = d.fragments[:0] // discard pending fragmented packets + return nil, 0, fmt.Errorf("payload is too short") + } + + typ := (pkt.Payload[0] >> 1) & 0b111111 + var nalus [][]byte + + switch typ { + case 48: // aggregation unit + d.fragments = d.fragments[:0] // discard pending fragmented packets + + payload := pkt.Payload[2:] + + for len(payload) > 0 { + if len(payload) < 2 { + return nil, 0, fmt.Errorf("invalid aggregation unit (invalid size)") + } + + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 { + break + } + + if int(size) > len(payload) { + return nil, 0, fmt.Errorf("invalid aggregation unit (invalid size)") + } + + nalus = append(nalus, payload[:size]) + payload = payload[size:] + } + + case 49: // fragmentation unit + if len(pkt.Payload) < 3 { + d.fragments = d.fragments[:0] // discard pending fragmented packets + return nil, 0, fmt.Errorf("payload is too short") + } + + start := pkt.Payload[2] >> 7 + end := (pkt.Payload[2] >> 6) & 0x01 + + if start == 1 { + d.fragments = d.fragments[:0] // discard pending fragmented packets + + if end != 0 { + return nil, 0, fmt.Errorf("invalid fragmentation unit (can't contain both a start and end bit)") + } + + typ := pkt.Payload[2] & 0b111111 + head := uint16(pkt.Payload[0]&0b10000001)<<8 | uint16(typ)<<9 | uint16(pkt.Payload[1]) + d.fragmentedSize = len(pkt.Payload[1:]) + d.fragments = append(d.fragments, []byte{byte(head >> 8), byte(head)}, pkt.Payload[3:]) + return nil, 0, ErrMorePacketsNeeded + } + + if len(d.fragments) == 0 { + return nil, 0, fmt.Errorf("invalid fragmentation unit (non-starting)") + } + + d.fragmentedSize += len(pkt.Payload[3:]) + if d.fragmentedSize > maxNALUSize { + d.fragments = d.fragments[:0] + return nil, 0, fmt.Errorf("NALU size (%d) is too big (maximum is %d)", d.fragmentedSize, maxNALUSize) + } + + d.fragments = append(d.fragments, pkt.Payload[3:]) + + if end != 1 { + return nil, 0, ErrMorePacketsNeeded + } + + nalu := make([]byte, d.fragmentedSize) + pos := 0 + + for _, frag := range d.fragments { + pos += copy(nalu[pos:], frag) + } + + d.fragments = d.fragments[:0] + nalus = [][]byte{nalu} + + case 50: // PACI + d.fragments = d.fragments[:0] // discard pending fragmented packets + return nil, 0, fmt.Errorf("PACI packets are not supported (yet)") + + default: + d.fragments = d.fragments[:0] // discard pending fragmented packets + nalus = [][]byte{pkt.Payload} + } + + return nalus, d.timeDecoder.Decode(pkt.Timestamp), nil +} diff --git a/pkg/rtph265/decoder_test.go b/pkg/rtph265/decoder_test.go new file mode 100644 index 00000000..f890f3d4 --- /dev/null +++ b/pkg/rtph265/decoder_test.go @@ -0,0 +1,161 @@ +package rtph265 + +import ( + "bytes" + "testing" + "time" + + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func mergeBytes(vals ...[]byte) []byte { + size := 0 + for _, v := range vals { + size += len(v) + } + res := make([]byte, size) + + pos := 0 + for _, v := range vals { + n := copy(res[pos:], v) + pos += n + } + + return res +} + +var cases = []struct { + name string + nalus [][]byte + pts time.Duration + pkts []*rtp.Packet +}{ + { + "single", + [][]byte{{0x01, 0x02, 0x03, 0x04, 0x05}}, + 25 * time.Millisecond, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + Timestamp: 2289528607, + SSRC: 0x9dbb7812, + }, + Payload: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + }, + }, + }, + { + "aggregated", + [][]byte{ + {0x07, 0x07}, + {0x08, 0x08}, + {0x09, 0x09}, + }, + 0, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + Timestamp: 2289526357, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x60, 0x00, 0x00, 0x02, 0x07, 0x07, 0x00, 0x02, + 0x08, 0x08, 0x00, 0x02, 0x09, 0x09, + }, + }, + }, + }, + { + "fragmented", + [][]byte{ + bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 512), + }, + 55 * time.Millisecond, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + Timestamp: 2289531307, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x63, 0x02, 0x80, 0x03, 0x04}, + bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 363), + []byte{0x01, 0x02, 0x03}, + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + Timestamp: 2289531307, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x63, 0x02, 0x40, 0x04}, + bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 147), + ), + }, + }, + }, +} + +func TestDecode(t *testing.T) { + for _, ca := range cases { + t.Run(ca.name, func(t *testing.T) { + d := &Decoder{} + d.Init() + + // send an initial packet downstream + // in order to compute the right timestamp, + // that is relative to the initial packet + pkt := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + Timestamp: 2289526357, + SSRC: 0x9dbb7812, + }, + Payload: []byte{0x06, 0x00}, + } + _, _, err := d.Decode(&pkt) + require.NoError(t, err) + + var nalus [][]byte + + for _, pkt := range ca.pkts { + clone := pkt.Clone() + + addNALUs, pts, err := d.Decode(pkt) + if err == ErrMorePacketsNeeded { + continue + } + + require.NoError(t, err) + require.Equal(t, ca.pts, pts) + nalus = append(nalus, addNALUs...) + + // test input integrity + require.Equal(t, clone, pkt) + } + + require.Equal(t, ca.nalus, nalus) + }) + } +} diff --git a/pkg/rtph265/encoder.go b/pkg/rtph265/encoder.go new file mode 100644 index 00000000..e6f4ccf8 --- /dev/null +++ b/pkg/rtph265/encoder.go @@ -0,0 +1,247 @@ +package rtph265 + +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 +) + +func randUint32() uint32 { + var b [4]byte + rand.Read(b[:]) + return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) +} + +// Encoder is a RTP/H265 encoder. +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // initial timestamp of packets (optional). + // It defaults to a random value. + InitialTimestamp *uint32 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + // indicates that NALUs have an additional field that specifies the decoding order. + MaxDONDiff int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() { + if e.SSRC == nil { + v := randUint32() + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v := uint16(randUint32()) + e.InitialSequenceNumber = &v + } + if e.InitialTimestamp == nil { + v := randUint32() + e.InitialTimestamp = &v + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) + } + + e.sequenceNumber = *e.InitialSequenceNumber +} + +func (e *Encoder) encodeTimestamp(ts time.Duration) uint32 { + return *e.InitialTimestamp + uint32(ts.Seconds()*rtpClockRate) +} + +// Encode encodes NALUs into RTP/H265 packets. +func (e *Encoder) Encode(nalus [][]byte, pts time.Duration) ([]*rtp.Packet, error) { + if e.MaxDONDiff != 0 { + return nil, fmt.Errorf("MaxDONDiff != 0 is not supported (yet)") + } + + var rets []*rtp.Packet + var batch [][]byte + + // split NALUs into batches + for _, nalu := range nalus { + if e.lenAggregationUnit(batch, nalu) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, nalu) + } else { + // write batch + if batch != nil { + pkts, err := e.writeBatch(batch, pts, false) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + } + + // initialize new batch + batch = [][]byte{nalu} + } + } + + // write final batch + // marker is used to indicate when all NALUs with same PTS have been sent + pkts, err := e.writeBatch(batch, pts, true) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeBatch(nalus [][]byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) { + if len(nalus) == 1 { + // the NALU fits into a single RTP packet + if len(nalus[0]) < e.PayloadMaxSize { + return e.writeSingle(nalus[0], pts, marker) + } + + // split the NALU into multiple fragmentation packet + return e.writeFragmentationUnits(nalus[0], pts, marker) + } + + return e.writeAggregationUnit(nalus, pts, marker) +} + +func (e *Encoder) writeSingle(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) { + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: e.encodeTimestamp(pts), + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: nalu, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} + +func (e *Encoder) writeFragmentationUnits(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) { + n := (len(nalu) - 2) / (e.PayloadMaxSize - 3) + lastPacketSize := (len(nalu) - 2) % (e.PayloadMaxSize - 3) + if lastPacketSize > 0 { + n++ + } + + ret := make([]*rtp.Packet, n) + encPTS := e.encodeTimestamp(pts) + + head := nalu[:2] + nalu = nalu[2:] + + for i := range ret { + start := uint8(0) + if i == 0 { + start = 1 + } + end := uint8(0) + le := e.PayloadMaxSize - 3 + if i == (n - 1) { + end = 1 + le = lastPacketSize + } + + data := make([]byte, 3+le) + data[0] = head[0]&0b10000001 | 49<<1 + data[1] = head[1] + data[2] = (start << 7) | (end << 6) | (head[0]>>1)&0b111111 + copy(data[3:], nalu[:le]) + nalu = nalu[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: encPTS, + SSRC: *e.SSRC, + Marker: (i == (n-1) && marker), + }, + Payload: data, + } + + e.sequenceNumber++ + } + + return ret, nil +} + +func (e *Encoder) lenAggregationUnit(nalus [][]byte, addNALU []byte) int { + ret := 2 // header + + for _, nalu := range nalus { + ret += 2 // size + ret += len(nalu) // nalu + } + + if addNALU != nil { + ret += 2 // size + ret += len(addNALU) // nalu + } + + return ret +} + +func (e *Encoder) writeAggregationUnit(nalus [][]byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) { + payload := make([]byte, e.lenAggregationUnit(nalus, nil)) + + // header + h := uint16(48) << 9 + payload[0] = byte(h >> 8) + payload[1] = byte(h) + pos := 2 + + for _, nalu := range nalus { + // size + naluLen := len(nalu) + payload[pos] = uint8(naluLen >> 8) + payload[pos+1] = uint8(naluLen) + pos += 2 + + // nalu + copy(payload[pos:], nalu) + pos += naluLen + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: e.encodeTimestamp(pts), + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/pkg/rtph265/encoder_test.go b/pkg/rtph265/encoder_test.go new file mode 100644 index 00000000..273159c5 --- /dev/null +++ b/pkg/rtph265/encoder_test.go @@ -0,0 +1,44 @@ +package rtph265 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncode(t *testing.T) { + for _, ca := range cases { + t.Run(ca.name, func(t *testing.T) { + e := &Encoder{ + PayloadType: 96, + SSRC: func() *uint32 { + v := uint32(0x9dbb7812) + return &v + }(), + InitialSequenceNumber: func() *uint16 { + v := uint16(0x44ed) + return &v + }(), + InitialTimestamp: func() *uint32 { + v := uint32(0x88776655) + return &v + }(), + } + e.Init() + + pkts, err := e.Encode(ca.nalus, ca.pts) + require.NoError(t, err) + require.Equal(t, ca.pkts, pkts) + }) + } +} + +func TestEncodeRandomInitialState(t *testing.T) { + e := &Encoder{ + PayloadType: 96, + } + e.Init() + require.NotEqual(t, nil, e.SSRC) + require.NotEqual(t, nil, e.InitialSequenceNumber) + require.NotEqual(t, nil, e.InitialTimestamp) +} diff --git a/pkg/rtph265/rtph265.go b/pkg/rtph265/rtph265.go new file mode 100644 index 00000000..6df37da8 --- /dev/null +++ b/pkg/rtph265/rtph265.go @@ -0,0 +1,6 @@ +// Package rtph265 contains a RTP/H265 decoder and encoder. +package rtph265 + +const ( + rtpClockRate = 90000 // H265 always uses 90khz +) diff --git a/track_h265.go b/track_h265.go index 684170f6..03419d3a 100644 --- a/track_h265.go +++ b/track_h265.go @@ -8,6 +8,8 @@ import ( "sync" psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/rtph265" ) // TrackH265 is a H265 track. @@ -16,6 +18,7 @@ type TrackH265 struct { VPS []byte SPS []byte PPS []byte + MaxDONDiff int trackBase mutex sync.RWMutex @@ -82,6 +85,13 @@ func (t *TrackH265) fillParamsFromMediaDescription(md *psdp.MediaDescription) er if err != nil { return fmt.Errorf("invalid sprop-pps (%v)", v) } + + case "sprop-max-don-diff": + tmp, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid sprop-max-don-diff (%v)", v) + } + t.MaxDONDiff = int(tmp) } } @@ -112,6 +122,9 @@ func (t *TrackH265) MediaDescription() *psdp.MediaDescription { if t.PPS != nil { tmp = append(tmp, "sprop-pps="+base64.StdEncoding.EncodeToString(t.PPS)) } + if t.MaxDONDiff != 0 { + tmp = append(tmp, "sprop-max-don-diff="+strconv.FormatInt(int64(t.MaxDONDiff), 10)) + } if tmp != nil { fmtp += " " + strings.Join(tmp, "; ") } @@ -145,10 +158,30 @@ func (t *TrackH265) clone() Track { VPS: t.VPS, SPS: t.SPS, PPS: t.PPS, + MaxDONDiff: t.MaxDONDiff, trackBase: t.trackBase, } } +// CreateDecoder creates a decoder able to decode the content of the track. +func (t *TrackH265) CreateDecoder() *rtph265.Decoder { + d := &rtph265.Decoder{ + MaxDONDiff: t.MaxDONDiff, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the track. +func (t *TrackH265) CreateEncoder() *rtph265.Encoder { + e := &rtph265.Encoder{ + PayloadType: t.PayloadType, + MaxDONDiff: t.MaxDONDiff, + } + e.Init() + return e +} + // SafeVPS returns the track VPS. func (t *TrackH265) SafeVPS() []byte { t.mutex.RLock() diff --git a/track_test.go b/track_test.go index 7e260a76..096ad525 100644 --- a/track_test.go +++ b/track_test.go @@ -441,7 +441,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { { Key: "fmtp", Value: "96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwB4mZgJ; " + - "sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; sprop-pps=RAHBcrRiQA==", + "sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; " + + "sprop-pps=RAHBcrRiQA==; sprop-max-don-diff=2", }, }, }, @@ -463,6 +464,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { PPS: []byte{ 0x44, 0x1, 0xc1, 0x72, 0xb4, 0x62, 0x40, }, + MaxDONDiff: 2, }, }, {