NEW PROTOCOL: Support viewing stream over RTSP. v7.0.47 (#4333)

## Introduce

This PR adds support for viewing streams via the RTSP protocol. Note
that it only supports viewing streams, not publishing streams via RTSP.

Currently, only publishing via RTMP is supported, which is then
converted to RTSP. Further work is needed to support publishing RTC/SRT
streams and converting them to RTSP.

## Usage

Build and run SRS with RTSP support:

```
cd srs/trunk && ./configure --rtsp=on && make -j16
./objs/srs -c conf/rtsp.conf
```

Push stream via RTMP by FFmpeg:

```
ffmpeg -re -i doc/source.flv -c copy -f flv rtmp://localhost/live/livestream
```

View the stream via RTSP protocol, try UDP first, then use TCP:

```
ffplay -i rtsp://localhost:8554/live/livestream
```

Or specify the transport protocol with TCP:

```
ffplay -rtsp_transport tcp -i rtsp://localhost:8554/live/livestream
```

## Unit Test

Run utest for RTSP:

```
./configure --utest=on & make utest -j16
./objs/srs_utest
```

## Regression Test

You need to start SRS for regression testing.

```
./objs/srs -c conf/regression-test-for-clion.conf
```

Then run regression tests for RTSP.

```
cd srs/trunk/3rdparty/srs-bench
go test ./srs -mod=vendor -v -count=1 -run=TestRtmpPublish_RtspPlay
```

## Blackbox Test

For blackbox testing, SRS will be started by utest, so there is no need
to start SRS manually.

```
cd srs/trunk/3rdparty/srs-bench
go test ./blackbox -mod=vendor -v -count=1 -run=TestFast_RtmpPublish_RtspPlay_Basic
```

## UDP Transport

As UDP requires port allocation, this PR doesn't support delivering
media stream via UDP transport, so it will fail if you try to use UDP as
transport:

```
ffplay -rtsp_transport udp -i rtsp://localhost:8554/live/livestream

[rtsp @ 0x7fbc99a14880] method SETUP failed: 461 Unsupported Transport
rtsp://localhost:8554/live/livestream: Protocol not supported

[2025-07-05 21:30:52.738][WARN][14916][7d7gf623][35] RTSP: setup failed: code=2057
(RtspTransportNotSupported) : UDP transport not supported, only TCP/interleaved mode is supported
```

There are no plans to support UDP transport for RTSP. In the real world,
UDP is rarely used; the vast majority of RTSP traffic uses TCP.

## Play Before Publish

RTSP supports audio with AAC and OPUS codecs, which is significantly
different from RTMP or WebRTC.

RTSP uses commands to exchange SDP and specify the audio track to play,
unlike WHEP or HTTP-FLV, which use the query string of the URL. RTSP
depends on the player’s behavior, making it very difficult to use and
describe.

Considering the feature that allows playing the stream before publishing
it, it requires generating some default parameters in the SDP. For OPUS,
the sample rate is 48 kHz with 2 channels, while AAC is more complex,
especially regarding the sample rate, which may be 44.1 kHz, 32 kHz, or
48 kHz.

Therefore, for RTSP, we cannot support play-then-publish. Instead, there
must already be a stream when playing it, so that the audio codec is
determined.

## Opus Codec

No Opus codec support for RTSP, because for RTC2RTSP, it always converts
RTC to RTMP frames, then converts them to RTSP packets. Therefore, the
audio codec is always AAC after converting RTC to RTMP.

This means the bridge architecture needs some changes. We need a new
bridge that binds to the target protocol. For example, RTC2RTMP converts
the audio codec, but RTC2RTSP keeps the original audio codec.

Furthermore, the RTC2RTMP bridge should also support bypassing the Opus
codec if we use enhanced-RTMP, which supports the Opus audio codec. I
think it should be configurable to either transcode or bypass the audio
codec. However, this is not relevant to RTSP.

## AI Contributor

Below commits are contributed by AI:

* [AI: Remove support for media transport via
UDP.](755686229f)
* [AI: Add crutial logs for each RTSP
stage.](9c8cbe7bde)
* [AI: Support AAC doec for
RTSP.](7d7cc12bae)
* [AI: Add option --rtsp for
RTSP.](f67414d9ee)
* [AI: Extract SrsRtpVideoBuilder for RTC and
RTSP.](562e76b904)

---------

Co-authored-by: Jacob Su <suzp1984@gmail.com>
Co-authored-by: winlin <winlinvip@gmail.com>
This commit is contained in:
Haibo Chen(陈海博) 2025-07-11 20:18:40 +08:00 committed by GitHub
parent 6208b6fe61
commit 5dc292ce64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
238 changed files with 32809 additions and 293 deletions

View File

@ -0,0 +1,119 @@
// The MIT License (MIT)
//
// # Copyright (c) 2025 Winlin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package blackbox
import (
"context"
"fmt"
"math/rand"
"os"
"path"
"sync"
"testing"
"time"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
)
func TestFast_RtmpPublish_RtspPlay_Basic(t *testing.T) {
// This case is run in parallel.
t.Parallel()
// Setup the max timeout for this case.
ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond)
defer cancel()
// Check a set of errors.
var r0, r1, r2, r3, r4, r5 error
defer func(ctx context.Context) {
if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil {
t.Errorf("Fail for err %+v", err)
} else {
logger.Tf(ctx, "test done with err %+v", err)
}
}(ctx)
var wg sync.WaitGroup
defer wg.Wait()
// Start SRS server and wait for it to be ready.
svr := NewSRSServer(func(v *srsServer) {
v.envs = []string{
"SRS_RTSP_SERVER_ENABLED=on",
"SRS_VHOST_RTSP_ENABLED=on",
"SRS_VHOST_RTSP_RTMP_TO_RTSP=on",
}
})
wg.Add(1)
go func() {
defer wg.Done()
r0 = svr.Run(ctx, cancel)
}()
// Start FFmpeg to publish stream.
streamID := fmt.Sprintf("stream-%v-%v", os.Getpid(), rand.Int())
streamURL := fmt.Sprintf("rtmp://localhost:%v/live/%v", svr.RTMPPort(), streamID)
ffmpeg := NewFFmpeg(func(v *ffmpegClient) {
v.args = []string{
"-stream_loop", "-1", "-re", "-i", *srsPublishAvatar, "-c", "copy", "-f", "flv", streamURL,
}
})
wg.Add(1)
go func() {
defer wg.Done()
<-svr.ReadyCtx().Done()
r1 = ffmpeg.Run(ctx, cancel)
}()
// Start FFprobe to detect and verify stream.
duration := time.Duration(*srsFFprobeDuration) * time.Millisecond
ffprobe := NewFFprobe(func(v *ffprobeClient) {
v.dvrFile = path.Join(svr.WorkDir(), "objs", fmt.Sprintf("srs-ffprobe-%v.mp4", streamID))
v.streamURL = fmt.Sprintf("rtsp://localhost:%v/live/%v", svr.RTSPPort(), streamID)
v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond
})
wg.Add(1)
go func() {
defer wg.Done()
<-svr.ReadyCtx().Done()
r2 = ffprobe.Run(ctx, cancel)
}()
// Fast quit for probe done.
select {
case <-ctx.Done():
case <-ffprobe.ProbeDoneCtx().Done():
defer cancel()
str, m := ffprobe.Result()
if len(m.Streams) != 2 {
r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str)
}
if ts := 90; m.Format.ProbeScore < ts {
r4 = errors.Errorf("low score=%v < %v, %v, %v", m.Format.ProbeScore, ts, m.String(), str)
}
if dv := m.Duration(); dv < duration {
r5 = errors.Errorf("short duration=%v < %v, %v, %v", dv, duration, m.String(), str)
}
}
}

View File

@ -422,6 +422,8 @@ type SRSServer interface {
APIPort() int
// SRTPort is the SRT UDP port.
SRTPort() int
// RTSPPort is the RTSP port.
RTSPPort() int
}
// srsServer is a SRS server instance.
@ -450,6 +452,8 @@ type srsServer struct {
httpListen int
// SRT UDP server listen port.
srtListen int
// RTSP server listen port.
rtspListen int
// The envs from user.
envs []string
@ -476,6 +480,7 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer {
v.apiListen = allocator.Allocate()
v.httpListen = allocator.Allocate()
v.srtListen = allocator.Allocate()
v.rtspListen = allocator.Allocate()
// Do cleanup.
v.process.onDispose = func(ctx context.Context, bs *backendService) error {
@ -483,7 +488,7 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer {
allocator.Free(v.apiListen)
allocator.Free(v.httpListen)
allocator.Free(v.srtListen)
allocator.Free(v.rtspListen)
if _, err := os.Stat(v.workDir); err == nil {
os.RemoveAll(v.workDir)
}
@ -520,6 +525,10 @@ func (v *srsServer) SRTPort() int {
return v.srtListen
}
func (v *srsServer) RTSPPort() int {
return v.rtspListen
}
func (v *srsServer) WorkDir() string {
return v.workDir
}
@ -575,6 +584,8 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error {
fmt.Sprintf("SRS_HTTP_SERVER_LISTEN=%v", v.httpListen),
// Setup the SRT server listen port.
fmt.Sprintf("SRS_SRT_SERVER_LISTEN=%v", v.srtListen),
// Setup the RTSP server listen port.
fmt.Sprintf("SRS_RTSP_SERVER_LISTEN=%v", v.rtspListen),
}...)
// Rewrite envs by case.
if v.envs != nil {

View File

@ -3,6 +3,7 @@ module github.com/ossrs/srs-bench
go 1.23.0
require (
github.com/bluenviron/gortsplib/v4 v4.13.1
github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83
github.com/google/gopacket v1.1.19
github.com/haivision/srtgo v0.0.0-20230627061225-a70d53fcd618
@ -21,6 +22,7 @@ require (
)
require (
github.com/bluenviron/mediacommon/v2 v2.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0-rc.1 // indirect

View File

@ -1,3 +1,7 @@
github.com/bluenviron/gortsplib/v4 v4.13.1 h1:FpkfzLTWgeC4C3ytfNZ9ezes+MA4ZSp8rAvxJP5RnLY=
github.com/bluenviron/gortsplib/v4 v4.13.1/go.mod h1:nJVGKKG8KEkt7KKuckZIXQ1FHevSbvdV7y5UcpLmORw=
github.com/bluenviron/mediacommon/v2 v2.1.0 h1:NtlRCaAo7gnCcO+EHHeFJxSIt+v8uNbvJqlH1Gk2PZM=
github.com/bluenviron/mediacommon/v2 v2.1.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k=
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/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=

View File

@ -30,11 +30,13 @@ import (
"testing"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/ossrs/go-oryx-lib/avc"
"github.com/ossrs/go-oryx-lib/flv"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/ossrs/go-oryx-lib/rtmp"
"github.com/pion/interceptor"
"github.com/pion/rtp"
"github.com/pkg/errors"
)
@ -190,6 +192,82 @@ func TestRtmpPublish_RtcPlay_AVC(t *testing.T) {
}
}
func TestRtmpPublish_RtspPlay(t *testing.T) {
ctx := logger.WithContext(context.Background())
ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond)
var r0, r1 error
err := func() (err error) {
streamSuffix := fmt.Sprintf("rtmp-regression-%v-%v", os.Getpid(), rand.Int())
rtmpUrl := fmt.Sprintf("rtmp://%v/live/%v", *srsServer, streamSuffix)
// TODO: 8554
rtspUrl := fmt.Sprintf("rtsp://%v:8554/live/%v", *srsServer, streamSuffix)
// Publisher connect to a RTMP stream.
publisher := NewRTMPPublisher()
defer publisher.Close()
if err := publisher.Publish(ctx, rtmpUrl); err != nil {
return err
}
player := NewRTSPPlayer()
defer player.Close()
// Run publisher and player
var wg sync.WaitGroup
defer wg.Wait()
publisherReady, publisherReadyCancel := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(30 * time.Millisecond)
publisherReadyCancel()
}()
wg.Add(1)
go func() {
defer wg.Done()
<-publisherReady.Done()
if err := player.Play(ctx, rtspUrl); err != nil {
r1 = err
cancel()
return
}
var nnPackets int
player.onRTPPacket = func(media *description.Media, format uint8, packet *rtp.Packet) error {
logger.Tf(ctx, "RTSP: recv rtp packet #%v, payload type=%v, size=%vB",
nnPackets, packet.PayloadType, len(packet.Payload))
if nnPackets += 1; nnPackets > 50 {
cancel()
}
// TODO: Further validate the RTP packets.
return nil
}
}()
wg.Add(1)
go func() {
defer wg.Done()
publisher.onSendPacket = func(m *rtmp.Message) error {
time.Sleep(1 * time.Millisecond)
return nil
}
if r0 = publisher.Ingest(ctx, *srsPublishAvatar); r0 != nil {
cancel()
}
}()
return nil
}()
if err := filterTestError(ctx.Err(), err, r0, r1); err != nil {
t.Errorf("err %+v", err)
}
}
func TestRtmpPublish_MultipleSequences(t *testing.T) {
ctx := logger.WithContext(context.Background())
ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond)
@ -235,7 +313,7 @@ func TestRtmpPublish_MultipleSequences(t *testing.T) {
}
// Ingore the duplicated sps/pps.
if isAvccrEquals(previousAvccr, avccr) {
if IsAvccrEquals(previousAvccr, avccr) {
return nil
}
previousAvccr = avccr

View File

@ -44,6 +44,10 @@ import (
"sync"
"time"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/ossrs/go-oryx-lib/amf0"
"github.com/ossrs/go-oryx-lib/avc"
"github.com/ossrs/go-oryx-lib/flv"
@ -2220,7 +2224,90 @@ func (v *FLVPlayer) consume(ctx context.Context) (err error) {
}
}
func isAvccrEquals(a, b *avc.AVCDecoderConfigurationRecord) bool {
// RTSPPlayer
type RTSPPlayer struct {
rtspUrl string
client *gortsplib.Client
ctx context.Context
cancel context.CancelFunc
onRTPPacket func(media *description.Media, format uint8, packet *rtp.Packet) error
}
func NewRTSPPlayer() *RTSPPlayer {
return &RTSPPlayer{}
}
func (v *RTSPPlayer) Close() error {
if v.cancel != nil {
v.cancel()
}
if v.client != nil {
v.client.Close()
}
return nil
}
func (v *RTSPPlayer) Play(ctx context.Context, rtspUrl string) error {
v.rtspUrl = rtspUrl
v.ctx, v.cancel = context.WithCancel(ctx)
transport := gortsplib.Transport(gortsplib.TransportTCP)
v.client = &gortsplib.Client{
Transport: &transport,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
u, err := base.ParseURL(rtspUrl)
if err != nil {
return errors.Wrapf(err, "parse url %v", rtspUrl)
}
err = v.client.Start(u.Scheme, u.Host)
if err != nil {
return errors.Wrapf(err, "connect rtsp %v", rtspUrl)
}
desc, _, err := v.client.Describe(u)
if err != nil {
return errors.Wrapf(err, "describe rtsp %v", rtspUrl)
}
err = v.client.SetupAll(desc.BaseURL, desc.Medias)
if err != nil {
return errors.Wrapf(err, "setup rtsp %v", rtspUrl)
}
v.client.OnPacketRTPAny(func(media *description.Media, forma format.Format, packet *rtp.Packet) {
if v.onRTPPacket != nil {
if err := v.onRTPPacket(media, uint8(forma.ClockRate()), packet); err != nil {
logger.Wf(ctx, "rtsp: on rtp error %+v", err)
}
}
})
_, err = v.client.Play(nil)
if err != nil {
return errors.Wrapf(err, "play rtsp %v", rtspUrl)
}
go func() {
err := v.client.Wait()
if err != nil {
logger.Wf(ctx, "rtsp: client error %+v", err)
}
if v.cancel != nil {
v.cancel()
}
}()
return nil
}
func IsAvccrEquals(a, b *avc.AVCDecoderConfigurationRecord) bool {
if a == nil || b == nil {
return false
}

View File

@ -0,0 +1 @@
.git

View File

@ -0,0 +1 @@
/coverage*.txt

View File

@ -0,0 +1,73 @@
linters:
enable:
- asciicheck
- bidichk
- bodyclose
#- contextcheck
- copyloopvar
- dupl
- errorlint
- gochecknoinits
- gocritic
- gofmt
- gofumpt
- lll
- misspell
- nilerr
- prealloc
- predeclared
- revive
- usestdlibvars
- unconvert
#- usetesting
- tparallel
- wastedassign
- whitespace
issues:
exclude-use-default: false
linters-settings:
errcheck:
exclude-functions:
- io.Copy
- (io.Closer).Close
- (io.Writer).Write
- (hash.Hash).Write
- (net.Conn).Close
- (net.Conn).SetReadDeadline
- (net.Conn).SetWriteDeadline
- (*net.TCPConn).SetKeepAlive
- (*net.TCPConn).SetKeepAlivePeriod
- (*net.TCPConn).SetNoDelay
- (net.Listener).Close
- (net.PacketConn).Close
- (net.PacketConn).SetReadDeadline
- (net.PacketConn).SetWriteDeadline
- (net/http.ResponseWriter).Write
- (*net/http.Server).Serve
- (*net/http.Server).ServeTLS
- (*net/http.Server).Shutdown
- os.Chdir
- os.Mkdir
- os.MkdirAll
- os.Remove
- os.RemoveAll
- os.Setenv
- os.Unsetenv
- (*os.File).WriteString
- (*os.File).Close
- (github.com/datarhei/gosrt.Conn).Close
- (github.com/datarhei/gosrt.Conn).SetReadDeadline
- (github.com/datarhei/gosrt.Conn).SetWriteDeadline
- (*github.com/bluenviron/gortsplib/v4.Client).Close
- (*github.com/bluenviron/gortsplib/v4.Server).Close
- (*github.com/bluenviron/gortsplib/v4.ServerSession).Close
- (*github.com/bluenviron/gortsplib/v4.ServerStream).Close
- (*github.com/bluenviron/gortsplib/v4.ServerConn).Close
govet:
enable-all: true
disable:
- fieldalignment
- reflectvaluecompare

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 aler9
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
BASE_IMAGE = golang:1.24-alpine3.20
LINT_IMAGE = golangci/golangci-lint:v1.64.5
.PHONY: $(shell ls)
help:
@echo "usage: make [action]"
@echo ""
@echo "available actions:"
@echo ""
@echo " mod-tidy run go mod tidy"
@echo " format format source files"
@echo " test run tests"
@echo " test-32 run tests on a 32-bit system"
@echo " test-e2e run end-to-end tests"
@echo " lint run linter"
@echo " bench run benchmarks"
@echo ""
blank :=
define NL
$(blank)
endef
include scripts/*.mk

View File

@ -0,0 +1,177 @@
# gortsplib
[![Test](https://github.com/bluenviron/gortsplib/actions/workflows/test.yml/badge.svg)](https://github.com/bluenviron/gortsplib/actions/workflows/test.yml)
[![Lint](https://github.com/bluenviron/gortsplib/actions/workflows/lint.yml/badge.svg)](https://github.com/bluenviron/gortsplib/actions/workflows/lint.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/bluenviron/gortsplib)](https://goreportcard.com/report/github.com/bluenviron/gortsplib)
[![CodeCov](https://codecov.io/gh/bluenviron/gortsplib/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/gortsplib/tree/main)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/bluenviron/gortsplib/v4)](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4#pkg-index)
RTSP 1.0 client and server library for the Go programming language, written for [MediaMTX](https://github.com/bluenviron/mediamtx).
Go &ge; 1.21 is required.
Features:
* Client
* Query servers about available media streams
* Read media streams from a server ("play")
* Read streams with the UDP, UDP-multicast or TCP transport protocol
* Read TLS-encrypted streams (TCP only)
* Switch transport protocol automatically
* Read selected media streams
* Pause or seek without disconnecting from the server
* Write to ONVIF back channels
* Get PTS (relative) timestamp of incoming packets
* Get NTP (absolute) timestamp of incoming packets
* Write media streams to a server ("record")
* Write streams with the UDP or TCP transport protocol
* Write TLS-encrypted streams (TCP only)
* Switch transport protocol automatically
* Pause without disconnecting from the server
* Server
* Handle requests from clients
* Validate client credentials
* Read media streams from clients ("record")
* Read streams with the UDP or TCP transport protocol
* Read TLS-encrypted streams (TCP only)
* Get PTS (relative) timestamp of incoming packets
* Get NTP (absolute) timestamp of incoming packets
* Serve media streams to clients ("play")
* Write streams with the UDP, UDP-multicast or TCP transport protocol
* Write TLS-encrypted streams (TCP only)
* Compute and provide SSRC, RTP-Info to clients
* Utilities
* Parse RTSP elements
* Encode/decode RTP packets into/from codec-specific frames
## Table of contents
* [Examples](#examples)
* [API Documentation](#api-documentation)
* [RTP Payload Formats](#rtp-payload-formats)
* [Specifications](#specifications)
* [Related projects](#related-projects)
## Examples
* [client-query](examples/client-query/main.go)
* [client-play](examples/client-play/main.go)
* [client-play-timestamp](examples/client-play-timestamp/main.go)
* [client-play-options](examples/client-play-options/main.go)
* [client-play-pause](examples/client-play-pause/main.go)
* [client-play-to-record](examples/client-play-to-record/main.go)
* [client-play-backchannel](examples/client-play-backchannel/main.go)
* [client-play-format-av1](examples/client-play-format-av1/main.go)
* [client-play-format-av1-to-jpeg](examples/client-play-format-av1-to-jpeg/main.go)
* [client-play-format-g711](examples/client-play-format-g711/main.go)
* [client-play-format-h264](examples/client-play-format-h264/main.go)
* [client-play-format-h264-to-jpeg](examples/client-play-format-h264-to-jpeg/main.go)
* [client-play-format-h264-to-disk](examples/client-play-format-h264-to-disk/main.go)
* [client-play-format-h264-mpeg4audio-to-disk](examples/client-play-format-h264-mpeg4audio-to-disk/main.go)
* [client-play-format-h265](examples/client-play-format-h265/main.go)
* [client-play-format-h265-to-jpeg](examples/client-play-format-h265-to-jpeg/main.go)
* [client-play-format-h265-to-disk](examples/client-play-format-h265-to-disk/main.go)
* [client-play-format-lpcm](examples/client-play-format-lpcm/main.go)
* [client-play-format-mjpeg](examples/client-play-format-mjpeg/main.go)
* [client-play-format-mpeg4audio](examples/client-play-format-mpeg4audio/main.go)
* [client-play-format-mpeg4audio-to-disk](examples/client-play-format-mpeg4audio-to-disk/main.go)
* [client-play-format-opus](examples/client-play-format-opus/main.go)
* [client-play-format-opus-to-disk](examples/client-play-format-opus-to-disk/main.go)
* [client-play-format-vp8](examples/client-play-format-vp8/main.go)
* [client-play-format-vp9](examples/client-play-format-vp9/main.go)
* [client-record-options](examples/client-record-options/main.go)
* [client-record-pause](examples/client-record-pause/main.go)
* [client-record-format-av1](examples/client-record-format-av1/main.go)
* [client-record-format-g711](examples/client-record-format-g711/main.go)
* [client-record-format-h264](examples/client-record-format-h264/main.go)
* [client-record-format-h264-from-disk](examples/client-record-format-h264-from-disk/main.go)
* [client-record-format-h265](examples/client-record-format-h265/main.go)
* [client-record-format-lpcm](examples/client-record-format-lpcm/main.go)
* [client-record-format-mjpeg](examples/client-record-format-mjpeg/main.go)
* [client-record-format-mpeg4audio](examples/client-record-format-mpeg4audio/main.go)
* [client-record-format-opus](examples/client-record-format-opus/main.go)
* [client-record-format-vp8](examples/client-record-format-vp8/main.go)
* [client-record-format-vp9](examples/client-record-format-vp9/main.go)
* [server](examples/server/main.go)
* [server-tls](examples/server-tls/main.go)
* [server-auth](examples/server-auth/main.go)
* [server-h264-to-disk](examples/server-h264-to-disk/main.go)
* [server-h264-from-disk](examples/server-h264-from-disk/main.go)
* [proxy](examples/proxy/main.go)
## API Documentation
[Click to open the API Documentation](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4#pkg-index)
## RTP Payload Formats
In RTSP, media streams are transmitted by using RTP packets, which are encoded in a specific, codec-dependent, format. This library supports formats for the following codecs:
### Video
|codec|documentation|encoder and decoder available|
|------|-------------|-----------------------------|
|AV1|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AV1)|:heavy_check_mark:|
|VP9|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#VP9)|:heavy_check_mark:|
|VP8|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#VP8)|:heavy_check_mark:|
|H265|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#H265)|:heavy_check_mark:|
|H264|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#H264)|:heavy_check_mark:|
|MPEG-4 Video (H263, Xvid)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4Video)|:heavy_check_mark:|
|MPEG-1/2 Video|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG1Video)|:heavy_check_mark:|
|M-JPEG|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MJPEG)|:heavy_check_mark:|
### Audio
|codec|documentation|encoder and decoder available|
|------|-------------|-----------------------------|
|Opus|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Opus)|:heavy_check_mark:|
|Vorbis|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Vorbis)||
|MPEG-4 Audio (AAC)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4Audio)|:heavy_check_mark:|
|MPEG-1/2 Audio (MP3)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG1Audio)|:heavy_check_mark:|
|AC-3|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AC3)|:heavy_check_mark:|
|Speex|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Speex)||
|G726|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G726)||
|G722|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G722)|:heavy_check_mark:|
|G711 (PCMA, PCMU)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G711)|:heavy_check_mark:|
|LPCM|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#LPCM)|:heavy_check_mark:|
### Other
|codec|documentation|encoder and decoder available|
|------|-------------|-----------------------------|
|MPEG-TS|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEGTS)||
## Specifications
|name|area|
|----|----|
|[RFC2326, RTSP 1.0](https://datatracker.ietf.org/doc/html/rfc2326)|protocol|
|[RFC7826, RTSP 2.0](https://datatracker.ietf.org/doc/html/rfc7826)|protocol|
|[RFC8866, SDP: Session Description Protocol](https://datatracker.ietf.org/doc/html/rfc8866)|SDP|
|[RTP Payload Format For AV1 (v1.0)](https://aomediacodec.github.io/av1-rtp-spec/)|payload formats / AV1|
|[RTP Payload Format for VP9 Video](https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16)|payload formats / VP9|
|[RFC7741, RTP Payload Format for VP8 Video](https://datatracker.ietf.org/doc/html/rfc7741)|payload formats / VP8|
|[RFC7798, RTP Payload Format for High Efficiency Video Coding (HEVC)](https://datatracker.ietf.org/doc/html/rfc7798)|payload formats / H265|
|[RFC6184, RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)|payload formats / H264|
|[RFC3640, RTP Payload Format for Transport of MPEG-4 Elementary Streams](https://datatracker.ietf.org/doc/html/rfc3640)|payload formats / MPEG-4 audio, MPEG-4 video|
|[RFC2250, RTP Payload Format for MPEG1/MPEG2 Video](https://datatracker.ietf.org/doc/html/rfc2250)|payload formats / MPEG-1 video, MPEG-2 audio, MPEG-TS|
|[RFC2435, RTP Payload Format for JPEG-compressed Video](https://datatracker.ietf.org/doc/html/rfc2435)|payload formats / M-JPEG|
|[RFC7587, RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)|payload formats / Opus|
|[Multiopus in libwebrtc](https://webrtc-review.googlesource.com/c/src/+/129768)|payload formats / Opus|
|[RFC5215, RTP Payload Format for Vorbis Encoded Audio](https://datatracker.ietf.org/doc/html/rfc5215)|payload formats / Vorbis|
|[RFC4184, RTP Payload Format for AC-3 Audio](https://datatracker.ietf.org/doc/html/rfc4184)|payload formats / AC-3|
|[RFC6416, RTP Payload Format for MPEG-4 Audio/Visual Streams](https://datatracker.ietf.org/doc/html/rfc6416)|payload formats / MPEG-4 audio|
|[RFC5574, RTP Payload Format for the Speex Codec](https://datatracker.ietf.org/doc/html/rfc5574)|payload formats / Speex|
|[RFC3551, RTP Profile for Audio and Video Conferences with Minimal Control](https://datatracker.ietf.org/doc/html/rfc3551)|payload formats / G726, G722, G711, LPCM|
|[RFC3190, RTP Payload Format for 12-bit DAT Audio and 20- and 24-bit Linear Sampled Audio](https://datatracker.ietf.org/doc/html/rfc3190)|payload formats / LPCM|
|[Codec specifications](https://github.com/bluenviron/mediacommon#specifications)|codecs|
|[Golang project layout](https://github.com/golang-standards/project-layout)|project layout|
## Related projects
* [MediaMTX](https://github.com/bluenviron/mediamtx)
* [gohlslib](https://github.com/bluenviron/gohlslib)
* [mediacommon](https://github.com/bluenviron/mediacommon)
* [pion/sdp (SDP library used internally)](https://github.com/pion/sdp)
* [pion/rtp (RTP library used internally)](https://github.com/pion/rtp)
* [pion/rtcp (RTCP library used internally)](https://github.com/pion/rtcp)

View File

@ -0,0 +1,58 @@
package gortsplib
import (
"github.com/bluenviron/gortsplib/v4/pkg/ringbuffer"
)
// this is an asynchronous queue processor
// that allows to detach the routine that is reading a stream
// from the routine that is writing a stream.
type asyncProcessor struct {
bufferSize int
running bool
buffer *ringbuffer.RingBuffer
stopError error
chStopped chan struct{}
}
func (w *asyncProcessor) initialize() {
w.buffer, _ = ringbuffer.New(uint64(w.bufferSize))
}
func (w *asyncProcessor) close() {
if w.running {
w.buffer.Close()
<-w.chStopped
}
}
func (w *asyncProcessor) start() {
w.running = true
w.chStopped = make(chan struct{})
go w.run()
}
func (w *asyncProcessor) run() {
w.stopError = w.runInner()
close(w.chStopped)
}
func (w *asyncProcessor) runInner() error {
for {
tmp, ok := w.buffer.Pull()
if !ok {
return nil
}
err := tmp.(func() error)()
if err != nil {
return err
}
}
}
func (w *asyncProcessor) push(cb func() error) bool {
return w.buffer.Push(cb)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,159 @@
package gortsplib
import (
"sync/atomic"
"time"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver"
"github.com/bluenviron/gortsplib/v4/pkg/rtcpsender"
"github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector"
"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
)
type clientFormat struct {
cm *clientMedia
format format.Format
onPacketRTP OnPacketRTPFunc
udpReorderer *rtpreorderer.Reorderer // play
tcpLossDetector *rtplossdetector.LossDetector // play
rtcpReceiver *rtcpreceiver.RTCPReceiver // play
rtcpSender *rtcpsender.RTCPSender // record or back channel
writePacketRTPInQueue func([]byte) error
rtpPacketsReceived *uint64
rtpPacketsSent *uint64
rtpPacketsLost *uint64
}
func (cf *clientFormat) initialize() {
cf.rtpPacketsReceived = new(uint64)
cf.rtpPacketsSent = new(uint64)
cf.rtpPacketsLost = new(uint64)
}
func (cf *clientFormat) start() {
if cf.cm.udpRTPListener != nil {
cf.writePacketRTPInQueue = cf.writePacketRTPInQueueUDP
} else {
cf.writePacketRTPInQueue = cf.writePacketRTPInQueueTCP
}
if cf.cm.c.state == clientStateRecord || cf.cm.media.IsBackChannel {
cf.rtcpSender = &rtcpsender.RTCPSender{
ClockRate: cf.format.ClockRate(),
Period: cf.cm.c.senderReportPeriod,
TimeNow: cf.cm.c.timeNow,
WritePacketRTCP: func(pkt rtcp.Packet) {
if !cf.cm.c.DisableRTCPSenderReports {
cf.cm.c.WritePacketRTCP(cf.cm.media, pkt) //nolint:errcheck
}
},
}
cf.rtcpSender.Initialize()
} else {
if cf.cm.udpRTPListener != nil {
cf.udpReorderer = &rtpreorderer.Reorderer{}
cf.udpReorderer.Initialize()
} else {
cf.tcpLossDetector = &rtplossdetector.LossDetector{}
}
cf.rtcpReceiver = &rtcpreceiver.RTCPReceiver{
ClockRate: cf.format.ClockRate(),
Period: cf.cm.c.receiverReportPeriod,
TimeNow: cf.cm.c.timeNow,
WritePacketRTCP: func(pkt rtcp.Packet) {
if cf.cm.udpRTPListener != nil {
cf.cm.c.WritePacketRTCP(cf.cm.media, pkt) //nolint:errcheck
}
},
}
err := cf.rtcpReceiver.Initialize()
if err != nil {
panic(err)
}
}
}
func (cf *clientFormat) stop() {
if cf.rtcpReceiver != nil {
cf.rtcpReceiver.Close()
cf.rtcpReceiver = nil
}
if cf.rtcpSender != nil {
cf.rtcpSender.Close()
}
}
func (cf *clientFormat) readPacketRTPUDP(pkt *rtp.Packet) {
packets, lost := cf.udpReorderer.Process(pkt)
if lost != 0 {
cf.handlePacketsLost(uint64(lost))
// do not return
}
now := cf.cm.c.timeNow()
for _, pkt := range packets {
cf.handlePacketRTP(pkt, now)
}
}
func (cf *clientFormat) readPacketRTPTCP(pkt *rtp.Packet) {
lost := cf.tcpLossDetector.Process(pkt)
if lost != 0 {
cf.handlePacketsLost(uint64(lost))
// do not return
}
now := cf.cm.c.timeNow()
cf.handlePacketRTP(pkt, now)
}
func (cf *clientFormat) handlePacketRTP(pkt *rtp.Packet, now time.Time) {
err := cf.rtcpReceiver.ProcessPacket(pkt, now, cf.format.PTSEqualsDTS(pkt))
if err != nil {
cf.cm.onPacketRTPDecodeError(err)
return
}
atomic.AddUint64(cf.rtpPacketsReceived, 1)
cf.onPacketRTP(pkt)
}
func (cf *clientFormat) handlePacketsLost(lost uint64) {
atomic.AddUint64(cf.rtpPacketsLost, lost)
cf.cm.c.OnPacketsLost(lost)
}
func (cf *clientFormat) writePacketRTPInQueueUDP(payload []byte) error {
err := cf.cm.udpRTPListener.write(payload)
if err != nil {
return err
}
atomic.AddUint64(cf.cm.bytesSent, uint64(len(payload)))
atomic.AddUint64(cf.rtpPacketsSent, 1)
return nil
}
func (cf *clientFormat) writePacketRTPInQueueTCP(payload []byte) error {
cf.cm.c.tcpFrame.Channel = cf.cm.tcpChannel
cf.cm.c.tcpFrame.Payload = payload
cf.cm.c.nconn.SetWriteDeadline(time.Now().Add(cf.cm.c.WriteTimeout))
err := cf.cm.c.conn.WriteInterleavedFrame(cf.cm.c.tcpFrame, cf.cm.c.tcpBuffer)
if err != nil {
return err
}
atomic.AddUint64(cf.cm.bytesSent, uint64(len(payload)))
atomic.AddUint64(cf.rtpPacketsSent, 1)
return nil
}

View File

@ -0,0 +1,361 @@
package gortsplib
import (
"net"
"sync/atomic"
"time"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
)
type clientMedia struct {
c *Client
media *description.Media
onPacketRTCP OnPacketRTCPFunc
formats map[uint8]*clientFormat
tcpChannel int
udpRTPListener *clientUDPListener
udpRTCPListener *clientUDPListener
writePacketRTCPInQueue func([]byte) error
bytesReceived *uint64
bytesSent *uint64
rtpPacketsInError *uint64
rtcpPacketsReceived *uint64
rtcpPacketsSent *uint64
rtcpPacketsInError *uint64
}
func (cm *clientMedia) initialize() {
cm.onPacketRTCP = func(rtcp.Packet) {}
cm.bytesReceived = new(uint64)
cm.bytesSent = new(uint64)
cm.rtpPacketsInError = new(uint64)
cm.rtcpPacketsReceived = new(uint64)
cm.rtcpPacketsSent = new(uint64)
cm.rtcpPacketsInError = new(uint64)
cm.formats = make(map[uint8]*clientFormat)
for _, forma := range cm.media.Formats {
f := &clientFormat{
cm: cm,
format: forma,
onPacketRTP: func(*rtp.Packet) {},
}
f.initialize()
cm.formats[forma.PayloadType()] = f
}
}
func (cm *clientMedia) close() {
if cm.udpRTPListener != nil {
cm.udpRTPListener.close()
cm.udpRTCPListener.close()
}
}
func (cm *clientMedia) createUDPListeners(
multicastEnable bool,
multicastSourceIP net.IP,
rtpAddress string,
rtcpAddress string,
) error {
if rtpAddress != ":0" {
l1 := &clientUDPListener{
c: cm.c,
multicastEnable: multicastEnable,
multicastSourceIP: multicastSourceIP,
address: rtpAddress,
}
err := l1.initialize()
if err != nil {
return err
}
l2 := &clientUDPListener{
c: cm.c,
multicastEnable: multicastEnable,
multicastSourceIP: multicastSourceIP,
address: rtcpAddress,
}
err = l2.initialize()
if err != nil {
l1.close()
return err
}
cm.udpRTPListener, cm.udpRTCPListener = l1, l2
return nil
}
var err error
cm.udpRTPListener, cm.udpRTCPListener, err = createUDPListenerPair(cm.c)
return err
}
func (cm *clientMedia) start() {
if cm.udpRTPListener != nil {
cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueUDP
if cm.c.state == clientStateRecord || cm.media.IsBackChannel {
cm.udpRTPListener.readFunc = cm.readPacketRTPUDPRecord
cm.udpRTCPListener.readFunc = cm.readPacketRTCPUDPRecord
} else {
cm.udpRTPListener.readFunc = cm.readPacketRTPUDPPlay
cm.udpRTCPListener.readFunc = cm.readPacketRTCPUDPPlay
}
} else {
cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueTCP
if cm.c.tcpCallbackByChannel == nil {
cm.c.tcpCallbackByChannel = make(map[int]readFunc)
}
if cm.c.state == clientStateRecord || cm.media.IsBackChannel {
cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readPacketRTPTCPRecord
cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readPacketRTCPTCPRecord
} else {
cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readPacketRTPTCPPlay
cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readPacketRTCPTCPPlay
}
}
for _, ct := range cm.formats {
ct.start()
}
if cm.udpRTPListener != nil {
cm.udpRTPListener.start()
cm.udpRTCPListener.start()
}
}
func (cm *clientMedia) stop() {
if cm.udpRTPListener != nil {
cm.udpRTPListener.stop()
cm.udpRTCPListener.stop()
}
for _, ct := range cm.formats {
ct.stop()
}
}
func (cm *clientMedia) findFormatWithSSRC(ssrc uint32) *clientFormat {
for _, format := range cm.formats {
stats := format.rtcpReceiver.Stats()
if stats != nil && stats.RemoteSSRC == ssrc {
return format
}
}
return nil
}
func (cm *clientMedia) writePacketRTCPInQueueUDP(payload []byte) error {
err := cm.udpRTCPListener.write(payload)
if err != nil {
return err
}
atomic.AddUint64(cm.bytesSent, uint64(len(payload)))
atomic.AddUint64(cm.rtcpPacketsSent, 1)
return nil
}
func (cm *clientMedia) writePacketRTCPInQueueTCP(payload []byte) error {
cm.c.tcpFrame.Channel = cm.tcpChannel + 1
cm.c.tcpFrame.Payload = payload
cm.c.nconn.SetWriteDeadline(time.Now().Add(cm.c.WriteTimeout))
err := cm.c.conn.WriteInterleavedFrame(cm.c.tcpFrame, cm.c.tcpBuffer)
if err != nil {
return err
}
atomic.AddUint64(cm.bytesSent, uint64(len(payload)))
atomic.AddUint64(cm.rtcpPacketsSent, 1)
return nil
}
func (cm *clientMedia) readPacketRTPTCPPlay(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
now := cm.c.timeNow()
atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix())
pkt := &rtp.Packet{}
err := pkt.Unmarshal(payload)
if err != nil {
cm.onPacketRTPDecodeError(err)
return false
}
forma, ok := cm.formats[pkt.PayloadType]
if !ok {
cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType})
return false
}
forma.readPacketRTPTCP(pkt)
return true
}
func (cm *clientMedia) readPacketRTCPTCPPlay(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
now := cm.c.timeNow()
atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix())
if len(payload) > udpMaxPayloadSize {
cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize})
return false
}
packets, err := rtcp.Unmarshal(payload)
if err != nil {
cm.onPacketRTCPDecodeError(err)
return false
}
atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets)))
for _, pkt := range packets {
if sr, ok := pkt.(*rtcp.SenderReport); ok {
format := cm.findFormatWithSSRC(sr.SSRC)
if format != nil {
format.rtcpReceiver.ProcessSenderReport(sr, now)
}
}
cm.onPacketRTCP(pkt)
}
return true
}
func (cm *clientMedia) readPacketRTPTCPRecord(_ []byte) bool {
return false
}
func (cm *clientMedia) readPacketRTCPTCPRecord(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
if len(payload) > udpMaxPayloadSize {
cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize})
return false
}
packets, err := rtcp.Unmarshal(payload)
if err != nil {
cm.onPacketRTCPDecodeError(err)
return false
}
atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets)))
for _, pkt := range packets {
cm.onPacketRTCP(pkt)
}
return true
}
func (cm *clientMedia) readPacketRTPUDPPlay(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
if len(payload) == (udpMaxPayloadSize + 1) {
cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketTooBigUDP{})
return false
}
pkt := &rtp.Packet{}
err := pkt.Unmarshal(payload)
if err != nil {
cm.onPacketRTPDecodeError(err)
return false
}
forma, ok := cm.formats[pkt.PayloadType]
if !ok {
cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType})
return false
}
forma.readPacketRTPUDP(pkt)
return true
}
func (cm *clientMedia) readPacketRTCPUDPPlay(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
if len(payload) == (udpMaxPayloadSize + 1) {
cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBigUDP{})
return false
}
packets, err := rtcp.Unmarshal(payload)
if err != nil {
cm.onPacketRTCPDecodeError(err)
return false
}
now := cm.c.timeNow()
atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets)))
for _, pkt := range packets {
if sr, ok := pkt.(*rtcp.SenderReport); ok {
format := cm.findFormatWithSSRC(sr.SSRC)
if format != nil {
format.rtcpReceiver.ProcessSenderReport(sr, now)
}
}
cm.onPacketRTCP(pkt)
}
return true
}
func (cm *clientMedia) readPacketRTPUDPRecord(_ []byte) bool {
return false
}
func (cm *clientMedia) readPacketRTCPUDPRecord(payload []byte) bool {
atomic.AddUint64(cm.bytesReceived, uint64(len(payload)))
if len(payload) == (udpMaxPayloadSize + 1) {
cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBigUDP{})
return false
}
packets, err := rtcp.Unmarshal(payload)
if err != nil {
cm.onPacketRTCPDecodeError(err)
return false
}
atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets)))
for _, pkt := range packets {
cm.onPacketRTCP(pkt)
}
return true
}
func (cm *clientMedia) onPacketRTPDecodeError(err error) {
atomic.AddUint64(cm.rtpPacketsInError, 1)
cm.c.OnDecodeError(err)
}
func (cm *clientMedia) onPacketRTCPDecodeError(err error) {
atomic.AddUint64(cm.rtcpPacketsInError, 1)
cm.c.OnDecodeError(err)
}

View File

@ -0,0 +1,79 @@
package gortsplib
import (
"sync"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
)
type clientReader struct {
c *Client
mutex sync.Mutex
allowInterleavedFrames bool
chResponse chan *base.Response
chRequest chan *base.Request
chError chan error
}
func (r *clientReader) start() {
r.chResponse = make(chan *base.Response)
r.chRequest = make(chan *base.Request)
r.chError = make(chan error)
go r.run()
}
func (r *clientReader) setAllowInterleavedFrames(v bool) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.allowInterleavedFrames = v
}
func (r *clientReader) wait() {
for {
select {
case <-r.chError:
return
case <-r.chResponse:
case <-r.chRequest:
}
}
}
func (r *clientReader) run() {
r.chError <- r.runInner()
}
func (r *clientReader) runInner() error {
for {
what, err := r.c.conn.Read()
if err != nil {
return err
}
switch what := what.(type) {
case *base.Response:
r.chResponse <- what
case *base.Request:
r.chRequest <- what
case *base.InterleavedFrame:
r.mutex.Lock()
if !r.allowInterleavedFrames {
r.mutex.Unlock()
return liberrors.ErrClientUnexpectedFrame{}
}
if cb, ok := r.c.tcpCallbackByChannel[what.Channel]; ok {
cb(what.Payload)
}
r.mutex.Unlock()
}
}
}

View File

@ -0,0 +1,7 @@
package gortsplib
// ClientStats are client statistics
type ClientStats struct {
Conn StatsConn
Session StatsSession
}

View File

@ -0,0 +1,188 @@
package gortsplib
import (
"crypto/rand"
"math/big"
"net"
"strconv"
"sync/atomic"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/multicast"
)
func int64Ptr(v int64) *int64 {
return &v
}
func randInRange(maxVal int) (int, error) {
b := big.NewInt(int64(maxVal + 1))
n, err := rand.Int(rand.Reader, b)
if err != nil {
return 0, err
}
return int(n.Int64()), nil
}
func createUDPListenerPair(c *Client) (*clientUDPListener, *clientUDPListener, error) {
// choose two consecutive ports in range 65535-10000
// RTP port must be even and RTCP port odd
for {
v, err := randInRange((65535 - 10000) / 2)
if err != nil {
return nil, nil, err
}
rtpPort := v*2 + 10000
rtcpPort := rtpPort + 1
rtpListener := &clientUDPListener{
c: c,
multicastEnable: false,
multicastSourceIP: nil,
address: net.JoinHostPort("", strconv.FormatInt(int64(rtpPort), 10)),
}
err = rtpListener.initialize()
if err != nil {
continue
}
rtcpListener := &clientUDPListener{
c: c,
multicastEnable: false,
multicastSourceIP: nil,
address: net.JoinHostPort("", strconv.FormatInt(int64(rtcpPort), 10)),
}
err = rtcpListener.initialize()
if err != nil {
rtpListener.close()
continue
}
return rtpListener, rtcpListener, nil
}
}
type packetConn interface {
net.PacketConn
SetReadBuffer(int) error
}
type clientUDPListener struct {
c *Client
multicastEnable bool
multicastSourceIP net.IP
address string
pc packetConn
readFunc readFunc
readIP net.IP
readPort int
writeAddr *net.UDPAddr
running bool
lastPacketTime *int64
done chan struct{}
}
func (u *clientUDPListener) initialize() error {
if u.multicastEnable {
intf, err := multicast.InterfaceForSource(u.multicastSourceIP)
if err != nil {
return err
}
u.pc, err = multicast.NewSingleConn(intf, u.address, u.c.ListenPacket)
if err != nil {
return err
}
} else {
tmp, err := u.c.ListenPacket(restrictNetwork("udp", u.address))
if err != nil {
return err
}
u.pc = tmp.(*net.UDPConn)
}
err := u.pc.SetReadBuffer(udpKernelReadBufferSize)
if err != nil {
u.pc.Close()
return err
}
u.lastPacketTime = int64Ptr(0)
return nil
}
func (u *clientUDPListener) close() {
if u.running {
u.stop()
}
u.pc.Close()
}
func (u *clientUDPListener) port() int {
return u.pc.LocalAddr().(*net.UDPAddr).Port
}
func (u *clientUDPListener) start() {
u.running = true
u.pc.SetReadDeadline(time.Time{})
u.done = make(chan struct{})
go u.run()
}
func (u *clientUDPListener) stop() {
u.pc.SetReadDeadline(time.Now())
<-u.done
u.running = false
}
func (u *clientUDPListener) run() {
defer close(u.done)
var buf []byte
createNewBuffer := func() {
buf = make([]byte, udpMaxPayloadSize+1)
}
createNewBuffer()
for {
n, addr, err := u.pc.ReadFrom(buf)
if err != nil {
return
}
uaddr := addr.(*net.UDPAddr)
if !u.readIP.Equal(uaddr.IP) {
continue
}
// in case of anyPortEnable, store the port of the first packet we receive.
// this reduces security issues
if u.c.AnyPortEnable && u.readPort == 0 {
u.readPort = uaddr.Port
} else if u.readPort != uaddr.Port {
continue
}
now := u.c.timeNow()
atomic.StoreInt64(u.lastPacketTime, now.Unix())
if u.readFunc(buf[:n]) {
createNewBuffer()
}
}
}
func (u *clientUDPListener) write(payload []byte) error {
// no mutex is needed here since Write() has an internal lock.
// https://github.com/golang/go/issues/27203#issuecomment-534386117
u.pc.SetWriteDeadline(time.Now().Add(u.c.WriteTimeout))
_, err := u.pc.WriteTo(payload, u.writeAddr)
return err
}

View File

@ -0,0 +1,9 @@
package gortsplib
const (
// same size as GStreamer's rtspsrc
udpKernelReadBufferSize = 0x80000
// 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header)
udpMaxPayloadSize = 1472
)

View File

@ -0,0 +1,11 @@
package gortsplib
import (
"time"
)
func emptyTimer() *time.Timer {
t := time.NewTimer(0)
<-t.C
return t
}

View File

@ -0,0 +1,2 @@
// Package auth contains utilities to perform authentication.
package auth

View File

@ -0,0 +1,17 @@
package auth
import (
"crypto/rand"
"encoding/hex"
)
// GenerateNonce generates a nonce that can be used in Validate().
func GenerateNonce() (string, error) {
byts := make([]byte, 16)
_, err := rand.Read(byts)
if err != nil {
return "", err
}
return hex.EncodeToString(byts), nil
}

View File

@ -0,0 +1,89 @@
package auth
import (
"fmt"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)
// NewSender allocates a Sender.
//
// Deprecated: replaced by Sender.Initialize().
func NewSender(wwwAuth base.HeaderValue, user string, pass string) (*Sender, error) {
s := &Sender{
WWWAuth: wwwAuth,
User: user,
Pass: pass,
}
err := s.Initialize()
return s, err
}
// Sender allows to send credentials.
// It requires a WWW-Authenticate header (provided by the server)
// and a set of credentials.
type Sender struct {
WWWAuth base.HeaderValue
User string
Pass string
authHeader *headers.Authenticate
}
// Initialize initializes a Sender.
func (se *Sender) Initialize() error {
for _, v := range se.WWWAuth {
var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v})
if err != nil {
continue // ignore unrecognized headers
}
if se.authHeader == nil ||
(auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256) ||
(se.authHeader.Method == headers.AuthMethodBasic) {
se.authHeader = &auth
}
}
if se.authHeader == nil {
return fmt.Errorf("no authentication methods available")
}
return nil
}
// AddAuthorization adds the Authorization header to a Request.
func (se *Sender) AddAuthorization(req *base.Request) {
urStr := req.URL.CloneWithoutCredentials().String()
h := headers.Authorization{
Method: se.authHeader.Method,
}
h.Username = se.User
if se.authHeader.Method == headers.AuthMethodBasic {
h.BasicPass = se.Pass
} else { // digest
h.Realm = se.authHeader.Realm
h.Nonce = se.authHeader.Nonce
h.URI = urStr
h.Algorithm = se.authHeader.Algorithm
if se.authHeader.Algorithm == nil || *se.authHeader.Algorithm == headers.AuthAlgorithmMD5 {
h.Response = md5Hex(md5Hex(se.User+":"+se.authHeader.Realm+":"+se.Pass) + ":" +
se.authHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
} else { // sha256
h.Response = sha256Hex(sha256Hex(se.User+":"+se.authHeader.Realm+":"+se.Pass) + ":" +
se.authHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
}
}
if req.Header == nil {
req.Header = make(base.Header)
}
req.Header["Authorization"] = h.Marshal()
}

View File

@ -0,0 +1,33 @@
package auth
import (
"github.com/bluenviron/gortsplib/v4/pkg/base"
)
// ValidateMethod is a validation method.
//
// Deprecated: replaced by VerifyMethod
type ValidateMethod = VerifyMethod
// validation methods.
//
// Deprecated.
const (
ValidateMethodBasic = VerifyMethodBasic
ValidateMethodDigestMD5 = VerifyMethodDigestMD5
ValidateMethodSHA256 = VerifyMethodDigestSHA256
)
// Validate validates a request sent by a client.
//
// Deprecated: replaced by Verify.
func Validate(
req *base.Request,
user string,
pass string,
methods []ValidateMethod,
realm string,
nonce string,
) error {
return Verify(req, user, pass, methods, realm, nonce)
}

View File

@ -0,0 +1,135 @@
package auth
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)
var reControlAttribute = regexp.MustCompile("^(.+/)trackID=[0-9]+$")
func md5Hex(in string) string {
h := md5.New()
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
func sha256Hex(in string) string {
h := sha256.New()
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
func contains(list []VerifyMethod, item VerifyMethod) bool {
for _, i := range list {
if i == item {
return true
}
}
return false
}
func urlMatches(expected string, received string, isSetup bool) bool {
if received == expected {
return true
}
// in SETUP requests, VLC uses the base URL of the stream
// instead of the URL of the track.
// Strip the control attribute to obtain the URL of the stream.
if isSetup {
if m := reControlAttribute.FindStringSubmatch(expected); m != nil && received == m[1] {
return true
}
}
return false
}
// VerifyMethod is a validation method.
type VerifyMethod int
// validation methods.
const (
VerifyMethodBasic VerifyMethod = iota
VerifyMethodDigestMD5
VerifyMethodDigestSHA256
)
// Verify verifies a request sent by a client.
func Verify(
req *base.Request,
user string,
pass string,
methods []VerifyMethod,
realm string,
nonce string,
) error {
if methods == nil {
// disable VerifyMethodDigestSHA256 unless explicitly set
// since it prevents FFmpeg from authenticating
methods = []VerifyMethod{VerifyMethodBasic, VerifyMethodDigestMD5}
}
var auth headers.Authorization
err := auth.Unmarshal(req.Header["Authorization"])
if err != nil {
return err
}
switch {
case auth.Method == headers.AuthMethodDigest &&
(contains(methods, VerifyMethodDigestMD5) &&
(auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5) ||
contains(methods, VerifyMethodDigestSHA256) &&
auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256):
if auth.Nonce != nonce {
return fmt.Errorf("wrong nonce")
}
if auth.Realm != realm {
return fmt.Errorf("wrong realm")
}
if auth.Username != user {
return fmt.Errorf("authentication failed")
}
if !urlMatches(req.URL.String(), auth.URI, req.Method == base.Setup) {
return fmt.Errorf("wrong URL")
}
var response string
if auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5 {
response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+auth.URI))
} else { // sha256
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
}
if auth.Response != response {
return fmt.Errorf("authentication failed")
}
case auth.Method == headers.AuthMethodBasic && contains(methods, VerifyMethodBasic):
if auth.Username != user {
return fmt.Errorf("authentication failed")
}
if auth.BasicPass != pass {
return fmt.Errorf("authentication failed")
}
default:
return fmt.Errorf("no supported authentication methods found")
}
return nil
}

View File

@ -0,0 +1,51 @@
package auth
import (
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
func GenerateWWWAuthenticate(methods []ValidateMethod, realm string, nonce string) base.HeaderValue {
if methods == nil {
// disable VerifyMethodDigestSHA256 unless explicitly set
// since it prevents FFmpeg from authenticating
methods = []VerifyMethod{VerifyMethodBasic, VerifyMethodDigestMD5}
}
var ret base.HeaderValue
for _, m := range methods {
var a base.HeaderValue
switch m {
case ValidateMethodBasic:
a = headers.Authenticate{
Method: headers.AuthMethodBasic,
Realm: realm,
}.Marshal()
case ValidateMethodDigestMD5:
aa := headers.AuthAlgorithmMD5
a = headers.Authenticate{
Method: headers.AuthMethodDigest,
Realm: realm,
Nonce: nonce,
Algorithm: &aa,
}.Marshal()
default: // sha256
aa := headers.AuthAlgorithmSHA256
a = headers.Authenticate{
Method: headers.AuthMethodDigest,
Realm: realm,
Nonce: nonce,
Algorithm: &aa,
}.Marshal()
}
ret = append(ret, a...)
}
return ret
}

View File

@ -0,0 +1,54 @@
package base
import (
"bufio"
"fmt"
"io"
"strconv"
)
const (
rtspMaxContentLength = 128 * 1024
)
type body []byte
func (b *body) unmarshal(header Header, rb *bufio.Reader) error {
cls, ok := header["Content-Length"]
if !ok || len(cls) != 1 {
*b = nil
return nil
}
cl, err := strconv.ParseUint(cls[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid Content-Length")
}
if cl > rtspMaxContentLength {
return fmt.Errorf("Content-Length exceeds %d (it's %d)",
rtspMaxContentLength, cl)
}
*b = make([]byte, cl)
n, err := io.ReadFull(rb, *b)
if err != nil && n != len(*b) {
return err
}
return nil
}
func (b body) marshalSize() int {
return len(b)
}
func (b body) marshalTo(buf []byte) int {
return copy(buf, b)
}
func (b body) marshal() []byte {
buf := make([]byte, b.marshalSize())
b.marshalTo(buf)
return buf
}

View File

@ -0,0 +1,148 @@
package base
import (
"bufio"
"fmt"
"net/http"
"sort"
"strings"
)
const (
headerMaxEntryCount = 255
headerMaxKeyLength = 512
headerMaxValueLength = 2048
)
func headerKeyNormalize(in string) string {
switch strings.ToLower(in) {
case "rtp-info":
return "RTP-Info"
case "www-authenticate":
return "WWW-Authenticate"
case "cseq":
return "CSeq"
}
return http.CanonicalHeaderKey(in)
}
// HeaderValue is an header value.
type HeaderValue []string
// Header is a RTSP reader, present in both Requests and Responses.
type Header map[string]HeaderValue
func (h *Header) unmarshal(br *bufio.Reader) error {
*h = make(Header)
count := 0
for {
byt, err := br.ReadByte()
if err != nil {
return err
}
if byt == '\r' {
err = readByteEqual(br, '\n')
if err != nil {
return err
}
break
}
if count >= headerMaxEntryCount {
return fmt.Errorf("headers count exceeds %d", headerMaxEntryCount)
}
key := string([]byte{byt})
byts, err := readBytesLimited(br, ':', headerMaxKeyLength-1)
if err != nil {
return fmt.Errorf("value is missing")
}
key += string(byts[:len(byts)-1])
key = headerKeyNormalize(key)
// https://tools.ietf.org/html/rfc2616
// The field value MAY be preceded by any amount of spaces
for {
byt, err = br.ReadByte()
if err != nil {
return err
}
if byt != ' ' {
break
}
}
br.UnreadByte() //nolint:errcheck
byts, err = readBytesLimited(br, '\r', headerMaxValueLength)
if err != nil {
return err
}
val := string(byts[:len(byts)-1])
err = readByteEqual(br, '\n')
if err != nil {
return err
}
(*h)[key] = append((*h)[key], val)
count++
}
return nil
}
func (h Header) marshalSize() int {
// sort headers by key
// in order to obtain deterministic results
keys := make([]string, len(h))
for key := range h {
keys = append(keys, key)
}
sort.Strings(keys)
n := 0
for _, key := range keys {
for _, val := range h[key] {
n += len([]byte(key + ": " + val + "\r\n"))
}
}
n += 2
return n
}
func (h Header) marshalTo(buf []byte) int {
// sort headers by key
// in order to obtain deterministic results
keys := make([]string, len(h))
for key := range h {
keys = append(keys, key)
}
sort.Strings(keys)
pos := 0
for _, key := range keys {
for _, val := range h[key] {
pos += copy(buf[pos:], []byte(key+": "+val+"\r\n"))
}
}
pos += copy(buf[pos:], []byte("\r\n"))
return pos
}
func (h Header) marshal() []byte {
buf := make([]byte, h.marshalSize())
h.marshalTo(buf)
return buf
}

View File

@ -0,0 +1,72 @@
package base
import (
"bufio"
"fmt"
"io"
)
const (
// InterleavedFrameMagicByte is the first byte of an interleaved frame.
InterleavedFrameMagicByte = 0x24
)
// InterleavedFrame is an interleaved frame, and allows to transfer binary data
// within RTSP/TCP connections. It is used to send and receive RTP and RTCP packets with TCP.
type InterleavedFrame struct {
// channel ID
Channel int
// payload
Payload []byte
}
// Unmarshal decodes an interleaved frame.
func (f *InterleavedFrame) Unmarshal(br *bufio.Reader) error {
var header [4]byte
_, err := io.ReadFull(br, header[:])
if err != nil {
return err
}
if header[0] != InterleavedFrameMagicByte {
return fmt.Errorf("invalid magic byte (0x%.2x)", header[0])
}
// it's useless to check payloadLen since it's limited to 65535
payloadLen := int(uint16(header[2])<<8 | uint16(header[3]))
f.Channel = int(header[1])
f.Payload = make([]byte, payloadLen)
_, err = io.ReadFull(br, f.Payload)
return err
}
// MarshalSize returns the size of an InterleavedFrame.
func (f InterleavedFrame) MarshalSize() int {
return 4 + len(f.Payload)
}
// MarshalTo writes an InterleavedFrame.
func (f InterleavedFrame) MarshalTo(buf []byte) (int, error) {
pos := 0
pos += copy(buf[pos:], []byte{0x24, byte(f.Channel)})
payloadLen := len(f.Payload)
buf[pos] = byte(payloadLen >> 8)
buf[pos+1] = byte(payloadLen)
pos += 2
pos += copy(buf[pos:], f.Payload)
return pos, nil
}
// Marshal writes an InterleavedFrame.
func (f InterleavedFrame) Marshal() ([]byte, error) {
buf := make([]byte, f.MarshalSize())
_, err := f.MarshalTo(buf)
return buf, err
}

View File

@ -0,0 +1,17 @@
package base
import (
"strings"
)
// PathSplitQuery splits a path from a query.
//
// Deprecated: not useful anymore.
func PathSplitQuery(pathAndQuery string) (string, string) {
i := strings.Index(pathAndQuery, "?")
if i >= 0 {
return pathAndQuery[:i], pathAndQuery[i+1:]
}
return pathAndQuery, ""
}

View File

@ -0,0 +1,173 @@
// Package base contains the primitives of the RTSP protocol.
package base
import (
"bufio"
"fmt"
"strconv"
)
const (
rtspProtocol10 = "RTSP/1.0"
requestMaxMethodLength = 64
requestMaxURLLength = 2048
requestMaxProtocolLength = 64
)
// Method is the method of a RTSP request.
type Method string
// methods.
const (
Announce Method = "ANNOUNCE"
Describe Method = "DESCRIBE"
GetParameter Method = "GET_PARAMETER"
Options Method = "OPTIONS"
Pause Method = "PAUSE"
Play Method = "PLAY"
Record Method = "RECORD"
Setup Method = "SETUP"
SetParameter Method = "SET_PARAMETER"
Teardown Method = "TEARDOWN"
)
// Request is a RTSP request.
type Request struct {
// request method
Method Method
// request url
URL *URL
// map of header values
Header Header
// optional body
Body []byte
}
// Unmarshal reads a request.
func (req *Request) Unmarshal(br *bufio.Reader) error {
byts, err := readBytesLimited(br, ' ', requestMaxMethodLength)
if err != nil {
return err
}
req.Method = Method(byts[:len(byts)-1])
if req.Method == "" {
return fmt.Errorf("empty method")
}
byts, err = readBytesLimited(br, ' ', requestMaxURLLength)
if err != nil {
return err
}
rawURL := string(byts[:len(byts)-1])
if rawURL != "*" {
var ur *URL
ur, err = ParseURL(rawURL)
if err != nil {
return fmt.Errorf("invalid URL (%v)", rawURL)
}
req.URL = ur
} else {
req.URL = nil
}
byts, err = readBytesLimited(br, '\r', requestMaxProtocolLength)
if err != nil {
return err
}
proto := byts[:len(byts)-1]
if string(proto) != rtspProtocol10 {
return fmt.Errorf("expected '%s', got %v", rtspProtocol10, proto)
}
err = readByteEqual(br, '\n')
if err != nil {
return err
}
err = req.Header.unmarshal(br)
if err != nil {
return err
}
err = (*body)(&req.Body).unmarshal(req.Header, br)
if err != nil {
return err
}
return nil
}
// MarshalSize returns the size of a Request.
func (req Request) MarshalSize() int {
n := len(req.Method) + 1
if req.URL != nil {
n += len(req.URL.CloneWithoutCredentials().String())
} else {
n++
}
n += 1 + len(rtspProtocol10) + 2
if len(req.Body) != 0 {
req.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(req.Body)), 10)}
}
n += req.Header.marshalSize()
n += body(req.Body).marshalSize()
return n
}
// MarshalTo writes a Request.
func (req Request) MarshalTo(buf []byte) (int, error) {
pos := 0
pos += copy(buf[pos:], []byte(req.Method))
buf[pos] = ' '
pos++
if req.URL != nil {
pos += copy(buf[pos:], []byte(req.URL.CloneWithoutCredentials().String()))
} else {
pos += copy(buf[pos:], []byte("*"))
}
buf[pos] = ' '
pos++
pos += copy(buf[pos:], rtspProtocol10)
buf[pos] = '\r'
pos++
buf[pos] = '\n'
pos++
if len(req.Body) != 0 {
req.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(req.Body)), 10)}
}
pos += req.Header.marshalTo(buf[pos:])
pos += body(req.Body).marshalTo(buf[pos:])
return pos, nil
}
// Marshal writes a Request.
func (req Request) Marshal() ([]byte, error) {
buf := make([]byte, req.MarshalSize())
_, err := req.MarshalTo(buf)
return buf, err
}
// String implements fmt.Stringer.
func (req Request) String() string {
buf, _ := req.Marshal()
return string(buf)
}

View File

@ -0,0 +1,254 @@
package base
import (
"bufio"
"fmt"
"strconv"
)
// StatusCode is the status code of a RTSP response.
type StatusCode int
// status codes.
const (
StatusContinue StatusCode = 100
StatusOK StatusCode = 200
StatusMovedPermanently StatusCode = 301
StatusFound StatusCode = 302
StatusSeeOther StatusCode = 303
StatusNotModified StatusCode = 304
StatusUseProxy StatusCode = 305
StatusBadRequest StatusCode = 400
StatusUnauthorized StatusCode = 401
StatusPaymentRequired StatusCode = 402
StatusForbidden StatusCode = 403
StatusNotFound StatusCode = 404
StatusMethodNotAllowed StatusCode = 405
StatusNotAcceptable StatusCode = 406
StatusProxyAuthRequired StatusCode = 407
StatusRequestTimeout StatusCode = 408
StatusGone StatusCode = 410
StatusPreconditionFailed StatusCode = 412
StatusRequestEntityTooLarge StatusCode = 413
StatusRequestURITooLong StatusCode = 414
StatusUnsupportedMediaType StatusCode = 415
StatusParameterNotUnderstood StatusCode = 451
StatusNotEnoughBandwidth StatusCode = 453
StatusSessionNotFound StatusCode = 454
StatusMethodNotValidInThisState StatusCode = 455
StatusHeaderFieldNotValidForResource StatusCode = 456
StatusInvalidRange StatusCode = 457
StatusParameterIsReadOnly StatusCode = 458
StatusAggregateOperationNotAllowed StatusCode = 459
StatusOnlyAggregateOperationAllowed StatusCode = 460
StatusUnsupportedTransport StatusCode = 461
StatusDestinationUnreachable StatusCode = 462
StatusDestinationProhibited StatusCode = 463
StatusDataTransportNotReadyYet StatusCode = 464
StatusNotificationReasonUnknown StatusCode = 465
StatusKeyManagementError StatusCode = 466
StatusConnectionAuthorizationRequired StatusCode = 470
StatusConnectionCredentialsNotAccepted StatusCode = 471
StatusFailureToEstablishSecureConnection StatusCode = 472
StatusInternalServerError StatusCode = 500
StatusNotImplemented StatusCode = 501
StatusBadGateway StatusCode = 502
StatusServiceUnavailable StatusCode = 503
StatusGatewayTimeout StatusCode = 504
StatusRTSPVersionNotSupported StatusCode = 505
StatusOptionNotSupported StatusCode = 551
StatusProxyUnavailable StatusCode = 553
)
// StatusMessages contains the status messages associated with each status code.
var StatusMessages = statusMessages
var statusMessages = map[StatusCode]string{
StatusContinue: "Continue",
StatusOK: "OK",
StatusMovedPermanently: "Moved Permanently",
StatusFound: "Found",
StatusSeeOther: "See Other",
StatusNotModified: "Not Modified",
StatusUseProxy: "Use Proxy",
StatusBadRequest: "Bad Request",
StatusUnauthorized: "Unauthorized",
StatusPaymentRequired: "Payment Required",
StatusForbidden: "Forbidden",
StatusNotFound: "Not Found",
StatusMethodNotAllowed: "Method Not Allowed",
StatusNotAcceptable: "Not Acceptable",
StatusProxyAuthRequired: "Proxy Auth Required",
StatusRequestTimeout: "Request Timeout",
StatusGone: "Gone",
StatusPreconditionFailed: "Precondition Failed",
StatusRequestEntityTooLarge: "Request Entity Too Large",
StatusRequestURITooLong: "Request URI Too Long",
StatusUnsupportedMediaType: "Unsupported Media Type",
StatusParameterNotUnderstood: "Parameter Not Understood",
StatusNotEnoughBandwidth: "Not Enough Bandwidth",
StatusSessionNotFound: "Session Not Found",
StatusMethodNotValidInThisState: "Method Not Valid In This State",
StatusHeaderFieldNotValidForResource: "Header Field Not Valid for Resource",
StatusInvalidRange: "Invalid Range",
StatusParameterIsReadOnly: "Parameter Is Read-Only",
StatusAggregateOperationNotAllowed: "Aggregate Operation Not Allowed",
StatusOnlyAggregateOperationAllowed: "Only Aggregate Operation Allowed",
StatusUnsupportedTransport: "Unsupported Transport",
StatusDestinationUnreachable: "Destination Unreachable",
StatusDestinationProhibited: "Destination Prohibited",
StatusDataTransportNotReadyYet: "Data Transport Not Ready Yet",
StatusNotificationReasonUnknown: "Notification Reason Unknown",
StatusKeyManagementError: "Key Management Error",
StatusConnectionAuthorizationRequired: "Connection Authorization Required",
StatusConnectionCredentialsNotAccepted: "Connection Credentials Not Accepted",
StatusFailureToEstablishSecureConnection: "Failure to Establish Secure Connection",
StatusInternalServerError: "Internal Server Error",
StatusNotImplemented: "Not Implemented",
StatusBadGateway: "Bad Gateway",
StatusServiceUnavailable: "Service Unavailable",
StatusGatewayTimeout: "Gateway Timeout",
StatusRTSPVersionNotSupported: "RTSP Version Not Supported",
StatusOptionNotSupported: "Option Not Supported",
StatusProxyUnavailable: "Proxy Unavailable",
}
// Response is a RTSP response.
type Response struct {
// numeric status code
StatusCode StatusCode
// status message
StatusMessage string
// map of header values
Header Header
// optional body
Body []byte
}
// Unmarshal reads a response.
func (res *Response) Unmarshal(br *bufio.Reader) error {
byts, err := readBytesLimited(br, ' ', 255)
if err != nil {
return err
}
proto := byts[:len(byts)-1]
if string(proto) != rtspProtocol10 {
return fmt.Errorf("expected '%s', got %v", rtspProtocol10, proto)
}
byts, err = readBytesLimited(br, ' ', 4)
if err != nil {
return err
}
statusCodeStr := string(byts[:len(byts)-1])
tmp, err := strconv.ParseUint(statusCodeStr, 10, 31)
if err != nil {
return fmt.Errorf("unable to parse status code")
}
res.StatusCode = StatusCode(tmp)
byts, err = readBytesLimited(br, '\r', 255)
if err != nil {
return err
}
res.StatusMessage = string(byts[:len(byts)-1])
if len(res.StatusMessage) == 0 {
return fmt.Errorf("empty status message")
}
err = readByteEqual(br, '\n')
if err != nil {
return err
}
err = res.Header.unmarshal(br)
if err != nil {
return err
}
err = (*body)(&res.Body).unmarshal(res.Header, br)
if err != nil {
return err
}
return nil
}
// MarshalSize returns the size of a Response.
func (res Response) MarshalSize() int {
n := 0
if res.StatusMessage == "" {
if status, ok := statusMessages[res.StatusCode]; ok {
res.StatusMessage = status
}
}
n += len(rtspProtocol10) + 1 + len(strconv.FormatInt(int64(res.StatusCode), 10)) + 1 + len(res.StatusMessage) + 2
if len(res.Body) != 0 {
res.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(res.Body)), 10)}
}
n += res.Header.marshalSize()
n += body(res.Body).marshalSize()
return n
}
// MarshalTo writes a Response.
func (res Response) MarshalTo(buf []byte) (int, error) {
if res.StatusMessage == "" {
if status, ok := statusMessages[res.StatusCode]; ok {
res.StatusMessage = status
}
}
pos := 0
pos += copy(buf[pos:], []byte(rtspProtocol10))
buf[pos] = ' '
pos++
pos += copy(buf[pos:], []byte(strconv.FormatInt(int64(res.StatusCode), 10)))
buf[pos] = ' '
pos++
pos += copy(buf[pos:], []byte(res.StatusMessage))
buf[pos] = '\r'
pos++
buf[pos] = '\n'
pos++
if len(res.Body) != 0 {
res.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(res.Body)), 10)}
}
pos += res.Header.marshalTo(buf[pos:])
pos += body(res.Body).marshalTo(buf[pos:])
return pos, nil
}
// Marshal writes a Response.
func (res Response) Marshal() ([]byte, error) {
buf := make([]byte, res.MarshalSize())
_, err := res.MarshalTo(buf)
return buf, err
}
// String implements fmt.Stringer.
func (res Response) String() string {
buf, _ := res.Marshal()
return string(buf)
}

View File

@ -0,0 +1,107 @@
package base
import (
"fmt"
"net/url"
"regexp"
"strings"
)
// URL is a RTSP URL.
// This is basically an HTTP URL with some additional functions to handle
// control attributes.
type URL url.URL
var escapeRegexp = regexp.MustCompile(`^(.+?)://(.*?)@(.*?)/(.*?)$`)
// ParseURL parses a RTSP URL.
func ParseURL(s string) (*URL, error) {
// https://github.com/golang/go/issues/30611
m := escapeRegexp.FindStringSubmatch(s)
if m != nil {
m[3] = strings.ReplaceAll(m[3], "%25", "%")
m[3] = strings.ReplaceAll(m[3], "%", "%25")
s = m[1] + "://" + m[2] + "@" + m[3] + "/" + m[4]
}
u, err := url.Parse(s)
if err != nil {
return nil, err
}
if u.Scheme != "rtsp" && u.Scheme != "rtsps" {
return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme)
}
if u.Opaque != "" {
return nil, fmt.Errorf("URLs with opaque data are not supported")
}
if u.Fragment != "" {
return nil, fmt.Errorf("URLs with fragments are not supported")
}
return (*URL)(u), nil
}
// String implements fmt.Stringer.
func (u *URL) String() string {
return (*url.URL)(u).String()
}
// Clone clones a URL.
func (u *URL) Clone() *URL {
return (*URL)(&url.URL{
Scheme: u.Scheme,
User: u.User,
Host: u.Host,
Path: u.Path,
RawPath: u.RawPath,
ForceQuery: u.ForceQuery,
RawQuery: u.RawQuery,
})
}
// CloneWithoutCredentials clones a URL without its credentials.
func (u *URL) CloneWithoutCredentials() *URL {
return (*URL)(&url.URL{
Scheme: u.Scheme,
Host: u.Host,
Path: u.Path,
RawPath: u.RawPath,
ForceQuery: u.ForceQuery,
RawQuery: u.RawQuery,
})
}
// RTSPPathAndQuery returns the path and query of a RTSP URL.
//
// Deprecated: not useful anymore.
func (u *URL) RTSPPathAndQuery() (string, bool) {
var pathAndQuery string
if u.RawPath != "" {
pathAndQuery = u.RawPath
} else {
pathAndQuery = u.Path
}
if u.RawQuery != "" {
pathAndQuery += "?" + u.RawQuery
}
return pathAndQuery, true
}
// Hostname returns u.Host, stripping any valid port number if present.
//
// If the result is enclosed in square brackets, as literal IPv6 addresses are,
// the square brackets are removed from the result.
func (u *URL) Hostname() string {
return (*url.URL)(u).Hostname()
}
// Port returns the port part of u.Host, without the leading colon.
//
// If u.Host doesn't contain a valid numeric port, Port returns an empty string.
func (u *URL) Port() string {
return (*url.URL)(u).Port()
}

View File

@ -0,0 +1,34 @@
package base
import (
"bufio"
"fmt"
)
func readByteEqual(rb *bufio.Reader, cmp byte) error {
byt, err := rb.ReadByte()
if err != nil {
return err
}
if byt != cmp {
return fmt.Errorf("expected '%c', got '%c'", cmp, byt)
}
return nil
}
func readBytesLimited(rb *bufio.Reader, delim byte, n int) ([]byte, error) {
for i := 1; i <= n; i++ {
byts, err := rb.Peek(i)
if err != nil {
return nil, err
}
if byts[len(byts)-1] == delim {
rb.Discard(len(byts)) //nolint:errcheck
return byts, nil
}
}
return nil, fmt.Errorf("buffer length exceeds %d", n)
}

View File

@ -0,0 +1,54 @@
// Package bytecounter contains a io.ReadWriter wrapper that allows to count read and written bytes.
package bytecounter
import (
"io"
"sync/atomic"
)
// ByteCounter is a io.ReadWriter wrapper that allows to count read and written bytes.
type ByteCounter struct {
rw io.ReadWriter
received *uint64
sent *uint64
}
// New allocates a ByteCounter.
func New(rw io.ReadWriter, received *uint64, sent *uint64) *ByteCounter {
if received == nil {
received = new(uint64)
}
if sent == nil {
sent = new(uint64)
}
return &ByteCounter{
rw: rw,
received: received,
sent: sent,
}
}
// Read implements io.ReadWriter.
func (bc *ByteCounter) Read(p []byte) (int, error) {
n, err := bc.rw.Read(p)
atomic.AddUint64(bc.received, uint64(n))
return n, err
}
// Write implements io.ReadWriter.
func (bc *ByteCounter) Write(p []byte) (int, error) {
n, err := bc.rw.Write(p)
atomic.AddUint64(bc.sent, uint64(n))
return n, err
}
// BytesReceived returns the number of bytes received.
func (bc *ByteCounter) BytesReceived() uint64 {
return atomic.LoadUint64(bc.received)
}
// BytesSent returns the number of bytes sent.
func (bc *ByteCounter) BytesSent() uint64 {
return atomic.LoadUint64(bc.sent)
}

View File

@ -0,0 +1,105 @@
// Package conn contains a RTSP connection implementation.
package conn
import (
"bufio"
"io"
"github.com/bluenviron/gortsplib/v4/pkg/base"
)
const (
readBufferSize = 4096
)
// Conn is a RTSP connection.
type Conn struct {
w io.Writer
br *bufio.Reader
// reuse interleaved frames. they should never be passed to secondary routines
fr base.InterleavedFrame
}
// NewConn allocates a Conn.
func NewConn(rw io.ReadWriter) *Conn {
return &Conn{
w: rw,
br: bufio.NewReaderSize(rw, readBufferSize),
}
}
// Read reads a Request, a Response or an Interleaved frame.
func (c *Conn) Read() (interface{}, error) {
for {
byts, err := c.br.Peek(2)
if err != nil {
return nil, err
}
if byts[0] == base.InterleavedFrameMagicByte {
return c.ReadInterleavedFrame()
}
if byts[0] == 'R' && byts[1] == 'T' {
return c.ReadResponse()
}
if (byts[0] == 'A' && byts[1] == 'N') ||
(byts[0] == 'D' && byts[1] == 'E') ||
(byts[0] == 'G' && byts[1] == 'E') ||
(byts[0] == 'O' && byts[1] == 'P') ||
(byts[0] == 'P' && byts[1] == 'A') ||
(byts[0] == 'P' && byts[1] == 'L') ||
(byts[0] == 'R' && byts[1] == 'E') ||
(byts[0] == 'S' && byts[1] == 'E') ||
(byts[0] == 'T' && byts[1] == 'E') {
return c.ReadRequest()
}
if _, err := c.br.Discard(1); err != nil {
return nil, err
}
}
}
// ReadRequest reads a Request.
func (c *Conn) ReadRequest() (*base.Request, error) {
var req base.Request
err := req.Unmarshal(c.br)
return &req, err
}
// ReadResponse reads a Response.
func (c *Conn) ReadResponse() (*base.Response, error) {
var res base.Response
err := res.Unmarshal(c.br)
return &res, err
}
// ReadInterleavedFrame reads a InterleavedFrame.
func (c *Conn) ReadInterleavedFrame() (*base.InterleavedFrame, error) {
err := c.fr.Unmarshal(c.br)
return &c.fr, err
}
// WriteRequest writes a request.
func (c *Conn) WriteRequest(req *base.Request) error {
buf, _ := req.Marshal()
_, err := c.w.Write(buf)
return err
}
// WriteResponse writes a response.
func (c *Conn) WriteResponse(res *base.Response) error {
buf, _ := res.Marshal()
_, err := c.w.Write(buf)
return err
}
// WriteInterleavedFrame writes an interleaved frame.
func (c *Conn) WriteInterleavedFrame(fr *base.InterleavedFrame, buf []byte) error {
n, _ := fr.MarshalTo(buf)
_, err := c.w.Write(buf[:n])
return err
}

View File

@ -0,0 +1,218 @@
// Package description contains objects to describe streams.
package description
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
psdp "github.com/pion/sdp/v3"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/format"
)
func getAttribute(attributes []psdp.Attribute, key string) string {
for _, attr := range attributes {
if attr.Key == key {
return attr.Value
}
}
return ""
}
func isBackChannel(attributes []psdp.Attribute) bool {
for _, attr := range attributes {
if attr.Key == "sendonly" {
return true
}
}
return false
}
func sortedKeys(fmtp map[string]string) []string {
keys := make([]string, len(fmtp))
i := 0
for key := range fmtp {
keys[i] = key
i++
}
sort.Strings(keys)
return keys
}
func isAlphaNumeric(v string) bool {
for _, r := range v {
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
return false
}
}
return true
}
// MediaType is the type of a media stream.
type MediaType string
// media types.
const (
MediaTypeVideo MediaType = "video"
MediaTypeAudio MediaType = "audio"
MediaTypeApplication MediaType = "application"
)
// Media is a media stream.
// It contains one or more formats.
type Media struct {
// Media type.
Type MediaType
// Media ID (optional).
ID string
// Whether this media is a back channel.
IsBackChannel bool
// Control attribute.
Control string
// Formats contained into the media.
Formats []format.Format
}
// Unmarshal decodes the media from the SDP format.
func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
m.Type = MediaType(md.MediaName.Media)
m.ID = getAttribute(md.Attributes, "mid")
if m.ID != "" && !isAlphaNumeric(m.ID) {
return fmt.Errorf("invalid mid: %v", m.ID)
}
m.IsBackChannel = isBackChannel(md.Attributes)
m.Control = getAttribute(md.Attributes, "control")
m.Formats = nil
for _, payloadType := range md.MediaName.Formats {
format, err := format.Unmarshal(md, payloadType)
if err != nil {
return err
}
m.Formats = append(m.Formats, format)
}
if m.Formats == nil {
return fmt.Errorf("no formats found")
}
return nil
}
// Marshal encodes the media in SDP format.
func (m Media) Marshal() *psdp.MediaDescription {
md := &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: string(m.Type),
Protos: []string{"RTP", "AVP"},
},
}
if m.ID != "" {
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "mid",
Value: m.ID,
})
}
if m.IsBackChannel {
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "sendonly",
})
}
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "control",
Value: m.Control,
})
for _, forma := range m.Formats {
typ := strconv.FormatUint(uint64(forma.PayloadType()), 10)
md.MediaName.Formats = append(md.MediaName.Formats, typ)
rtpmap := forma.RTPMap()
if rtpmap != "" {
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "rtpmap",
Value: typ + " " + rtpmap,
})
}
fmtp := forma.FMTP()
if len(fmtp) != 0 {
tmp := make([]string, len(fmtp))
for i, key := range sortedKeys(fmtp) {
tmp[i] = key + "=" + fmtp[key]
}
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "fmtp",
Value: typ + " " + strings.Join(tmp, "; "),
})
}
}
return md
}
// URL returns the absolute URL of the media.
func (m Media) URL(contentBase *base.URL) (*base.URL, error) {
if contentBase == nil {
return nil, fmt.Errorf("Content-Base header not provided")
}
// no control attribute, use base URL
if m.Control == "" {
return contentBase, nil
}
// control attribute contains an absolute path
if strings.HasPrefix(m.Control, "rtsp://") ||
strings.HasPrefix(m.Control, "rtsps://") {
ur, err := base.ParseURL(m.Control)
if err != nil {
return nil, err
}
// copy host and credentials
ur.Host = contentBase.Host
ur.User = contentBase.User
return ur, nil
}
// control attribute contains a relative control attribute
// insert the control attribute at the end of the URL
// if there's a query, insert it after the query
// otherwise insert it after the path
strURL := contentBase.String()
if m.Control[0] != '?' && m.Control[0] != '/' && !strings.HasSuffix(strURL, "/") {
strURL += "/"
}
ur, _ := base.ParseURL(strURL + m.Control)
return ur, nil
}
// FindFormat finds a certain format among all the formats in the media.
func (m Media) FindFormat(forma interface{}) bool {
for _, formak := range m.Formats {
if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() {
reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak))
return true
}
}
return false
}

View File

@ -0,0 +1,164 @@
package description
import (
"fmt"
"strings"
psdp "github.com/pion/sdp/v3"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/sdp"
)
func atLeastOneHasMID(medias []*Media) bool {
for _, media := range medias {
if media.ID != "" {
return true
}
}
return false
}
func atLeastOneDoesntHaveMID(medias []*Media) bool {
for _, media := range medias {
if media.ID == "" {
return true
}
}
return false
}
func hasMediaWithID(medias []*Media, id string) bool {
for _, media := range medias {
if media.ID == id {
return true
}
}
return false
}
// SessionFECGroup is a FEC group.
type SessionFECGroup []string
// Session is the description of a RTSP stream.
type Session struct {
// Base URL of the stream (read only).
BaseURL *base.URL
// Title of the stream (optional).
Title string
// FEC groups (RFC5109).
FECGroups []SessionFECGroup
// Media streams.
Medias []*Media
}
// FindFormat finds a certain format among all the formats in all the medias of the stream.
// If the format is found, it is inserted into forma, and its media is returned.
func (d *Session) FindFormat(forma interface{}) *Media {
for _, media := range d.Medias {
ok := media.FindFormat(forma)
if ok {
return media
}
}
return nil
}
// Unmarshal decodes the description from SDP.
func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
d.Title = string(ssd.SessionName)
if d.Title == " " {
d.Title = ""
}
d.Medias = make([]*Media, len(ssd.MediaDescriptions))
for i, md := range ssd.MediaDescriptions {
var m Media
err := m.Unmarshal(md)
if err != nil {
return fmt.Errorf("media %d is invalid: %w", i+1, err)
}
if m.ID != "" && hasMediaWithID(d.Medias[:i], m.ID) {
return fmt.Errorf("duplicate media IDs")
}
d.Medias[i] = &m
}
if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) {
return fmt.Errorf("media IDs sent partially")
}
for _, attr := range ssd.Attributes {
if attr.Key == "group" && strings.HasPrefix(attr.Value, "FEC ") {
group := SessionFECGroup(strings.Split(attr.Value[len("FEC "):], " "))
for _, id := range group {
if !hasMediaWithID(d.Medias, id) {
return fmt.Errorf("FEC group points to an invalid media ID: %v", id)
}
}
d.FECGroups = append(d.FECGroups, group)
}
}
return nil
}
// Marshal encodes the description in SDP.
func (d Session) Marshal(multicast bool) ([]byte, error) {
var sessionName psdp.SessionName
if d.Title != "" {
sessionName = psdp.SessionName(d.Title)
} else {
// RFC 4566: If a session has no meaningful name, the
// value "s= " SHOULD be used (i.e., a single space as the session name).
sessionName = psdp.SessionName(" ")
}
var address string
if multicast {
address = "224.1.0.0"
} else {
address = "0.0.0.0"
}
sout := &sdp.SessionDescription{
SessionName: sessionName,
Origin: psdp.Origin{
Username: "-",
NetworkType: "IN",
AddressType: "IP4",
UnicastAddress: "127.0.0.1",
},
// required by Darwin Streaming Server
ConnectionInformation: &psdp.ConnectionInformation{
NetworkType: "IN",
AddressType: "IP4",
Address: &psdp.Address{Address: address},
},
TimeDescriptions: []psdp.TimeDescription{
{Timing: psdp.Timing{StartTime: 0, StopTime: 0}},
},
MediaDescriptions: make([]*psdp.MediaDescription, len(d.Medias)),
}
for i, media := range d.Medias {
sout.MediaDescriptions[i] = media.Marshal()
}
for _, group := range d.FECGroups {
sout.Attributes = append(sout.Attributes, psdp.Attribute{
Key: "group",
Value: "FEC " + strings.Join(group, " "),
})
}
return sout.Marshal()
}

View File

@ -0,0 +1,101 @@
package format
import (
"strconv"
"strings"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3"
)
// AC3 is the RTP format for the AC-3 codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc4184
type AC3 struct {
PayloadTyp uint8
SampleRate int
ChannelCount int
}
func (f *AC3) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
tmp := strings.SplitN(ctx.clock, "/", 2)
tmp1, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil {
return err
}
f.SampleRate = int(tmp1)
if len(tmp) >= 2 {
tmp1, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return err
}
f.ChannelCount = int(tmp1)
} else {
// RFC4184: If the "channels" parameter
// is omitted, a default maximum value of 6 is implied.
f.ChannelCount = 6
}
return nil
}
// Codec implements Format.
func (f *AC3) Codec() string {
return "AC-3"
}
// ClockRate implements Format.
func (f *AC3) ClockRate() int {
return f.SampleRate
}
// PayloadType implements Format.
func (f *AC3) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *AC3) RTPMap() string {
return "AC3/" + strconv.FormatInt(int64(f.SampleRate), 10) +
"/" + strconv.FormatInt(int64(f.ChannelCount), 10)
}
// FMTP implements Format.
func (f *AC3) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *AC3) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *AC3) CreateDecoder() (*rtpac3.Decoder, error) {
d := &rtpac3.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *AC3) CreateEncoder() (*rtpac3.Encoder, error) {
e := &rtpac3.Encoder{
PayloadType: f.PayloadTyp,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,124 @@
package format //nolint:dupl
import (
"fmt"
"strconv"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
)
// AV1 is the RTP format for the AV1 codec.
// Specification: https://aomediacodec.github.io/av1-rtp-spec/
type AV1 struct {
PayloadTyp uint8
LevelIdx *int
Profile *int
Tier *int
}
func (f *AV1) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
for key, val := range ctx.fmtp {
switch key {
case "level-idx":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid level-idx: %v", val)
}
v2 := int(n)
f.LevelIdx = &v2
case "profile":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid profile: %v", val)
}
v2 := int(n)
f.Profile = &v2
case "tier":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid tier: %v", val)
}
v2 := int(n)
f.Tier = &v2
}
}
return nil
}
// Codec implements Format.
func (f *AV1) Codec() string {
return "AV1"
}
// ClockRate implements Format.
func (f *AV1) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *AV1) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *AV1) RTPMap() string {
return "AV1/90000"
}
// FMTP implements Format.
func (f *AV1) FMTP() map[string]string {
fmtp := make(map[string]string)
if f.LevelIdx != nil {
fmtp["level-idx"] = strconv.FormatInt(int64(*f.LevelIdx), 10)
}
if f.Profile != nil {
fmtp["profile"] = strconv.FormatInt(int64(*f.Profile), 10)
}
if f.Tier != nil {
fmtp["tier"] = strconv.FormatInt(int64(*f.Tier), 10)
}
return fmtp
}
// PTSEqualsDTS implements Format.
func (f *AV1) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *AV1) CreateDecoder() (*rtpav1.Decoder, error) {
d := &rtpav1.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *AV1) CreateEncoder() (*rtpav1.Encoder, error) {
e := &rtpav1.Encoder{
PayloadType: f.PayloadTyp,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,234 @@
// Package format contains RTP format definitions, decoders and encoders.
package format
import (
"regexp"
"strconv"
"strings"
"github.com/pion/rtp"
psdp "github.com/pion/sdp/v3"
)
var (
smartPayloadTypeRegexp = regexp.MustCompile("^smart/[0-9]/[0-9]+$")
smartRtpmapRegexp = regexp.MustCompile("^([0-9]+) (.+)/[0-9]+$")
)
func replaceSmartPayloadType(payloadType string, attributes []psdp.Attribute) string {
re1 := smartPayloadTypeRegexp.FindStringSubmatch(payloadType)
if re1 != nil {
for _, attr := range attributes {
if attr.Key == "rtpmap" {
re2 := smartRtpmapRegexp.FindStringSubmatch(attr.Value)
if re2 != nil {
return re2[1]
}
}
}
}
return payloadType
}
func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string {
for _, attr := range attributes {
if attr.Key == key {
v := strings.TrimSpace(attr.Value)
if parts := strings.SplitN(v, " ", 2); len(parts) == 2 {
if tmp, err := strconv.ParseUint(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType {
return parts[1]
}
}
}
}
return ""
}
func getCodecAndClock(rtpMap string) (string, string) {
parts2 := strings.SplitN(rtpMap, "/", 2)
if len(parts2) != 2 {
return "", ""
}
return strings.ToLower(parts2[0]), parts2[1]
}
func decodeFMTP(enc string) map[string]string {
if enc == "" {
return nil
}
ret := make(map[string]string)
for _, kv := range strings.Split(enc, ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
continue
}
ret[strings.ToLower(tmp[0])] = tmp[1]
}
return ret
}
type unmarshalContext struct {
mediaType string
payloadType uint8
clock string
codec string
rtpMap string
fmtp map[string]string
}
// Format is a media format.
// It defines the payload type of RTP packets and how to encode/decode them.
type Format interface {
unmarshal(ctx *unmarshalContext) error
// Codec returns the codec name.
Codec() string
// ClockRate returns the clock rate.
ClockRate() int
// PayloadType returns the payload type.
PayloadType() uint8
// RTPMap returns the rtpmap attribute.
RTPMap() string
// FMTP returns the fmtp attribute.
FMTP() map[string]string
// PTSEqualsDTS checks whether PTS is equal to DTS in RTP packets.
PTSEqualsDTS(*rtp.Packet) bool
}
// Unmarshal decodes a format from a media description.
func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error) {
mediaType := md.MediaName.Media
payloadTypeStr = replaceSmartPayloadType(payloadTypeStr, md.Attributes)
tmp, err := strconv.ParseUint(payloadTypeStr, 10, 8)
if err != nil {
return nil, err
}
payloadType := uint8(tmp)
rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap")
fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadType, "fmtp"))
codec, clock := getCodecAndClock(rtpMap)
format := func() Format {
switch {
/*
* dynamic payload types
**/
// video
case codec == "av1" && clock == "90000" && payloadType >= 96 && payloadType <= 127:
return &AV1{}
case codec == "vp9" && clock == "90000" && payloadType >= 96 && payloadType <= 127:
return &VP9{}
case codec == "vp8" && clock == "90000" && payloadType >= 96 && payloadType <= 127:
return &VP8{}
case codec == "h265" && clock == "90000" && payloadType >= 96 && payloadType <= 127:
return &H265{}
case codec == "h264" && clock == "90000" && ((payloadType >= 96 && payloadType <= 127) || payloadType == 35):
return &H264{}
case codec == "mp4v-es" && clock == "90000" && payloadType >= 96 && payloadType <= 127:
return &MPEG4Video{}
// audio
case codec == "opus", codec == "multiopus" && payloadType >= 96 && payloadType <= 127:
return &Opus{}
case codec == "vorbis" && payloadType >= 96 && payloadType <= 127:
return &Vorbis{}
case (codec == "mpeg4-generic" || codec == "mp4a-latm") && payloadType >= 96 && payloadType <= 127:
return &MPEG4Audio{}
case codec == "ac3" && payloadType >= 96 && payloadType <= 127:
return &AC3{}
case codec == "speex" && payloadType >= 96 && payloadType <= 127:
return &Speex{}
case (codec == "g726-16" ||
codec == "g726-24" ||
codec == "g726-32" ||
codec == "g726-40" ||
codec == "aal2-g726-16" ||
codec == "aal2-g726-24" ||
codec == "aal2-g726-32" ||
codec == "aal2-g726-40") && clock == "8000" && payloadType >= 96 && payloadType <= 127:
return &G726{}
case codec == "pcma", codec == "pcmu" && payloadType >= 96 && payloadType <= 127:
return &G711{}
case codec == "l8", codec == "l16", codec == "l24" && payloadType >= 96 && payloadType <= 127:
return &LPCM{}
/*
* static payload types
**/
// video
case payloadType == 32:
return &MPEG1Video{}
case payloadType == 26:
return &MJPEG{}
case payloadType == 33:
return &MPEGTS{}
// audio
case payloadType == 14:
return &MPEG1Audio{}
case payloadType == 9:
return &G722{}
case payloadType == 0, payloadType == 8:
return &G711{}
case payloadType == 10, payloadType == 11:
return &LPCM{}
}
return &Generic{}
}()
err = format.unmarshal(&unmarshalContext{
mediaType: mediaType,
payloadType: payloadType,
clock: clock,
codec: codec,
rtpMap: rtpMap,
fmtp: fmtp,
})
if err != nil {
return nil, err
}
return format, nil
}

View File

@ -0,0 +1,134 @@
package format
import (
"strconv"
"strings"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
)
// G711 is the RTP format for the G711 codec, encoded with mu-law or A-law.
// Specification: https://datatracker.ietf.org/doc/html/rfc3551
type G711 struct {
PayloadTyp uint8
MULaw bool
SampleRate int
ChannelCount int
}
func (f *G711) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
if ctx.payloadType == 0 {
f.MULaw = true
f.SampleRate = 8000
f.ChannelCount = 1
return nil
}
if ctx.payloadType == 8 {
f.MULaw = false
f.SampleRate = 8000
f.ChannelCount = 1
return nil
}
f.MULaw = (ctx.codec == "pcmu")
tmp := strings.SplitN(ctx.clock, "/", 2)
tmp1, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil {
return err
}
f.SampleRate = int(tmp1)
if len(tmp) >= 2 {
tmp1, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return err
}
f.ChannelCount = int(tmp1)
} else {
f.ChannelCount = 1
}
return nil
}
// Codec implements Format.
func (f *G711) Codec() string {
return "G711"
}
// ClockRate implements Format.
func (f *G711) ClockRate() int {
return f.SampleRate
}
// PayloadType implements Format.
func (f *G711) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *G711) RTPMap() string {
ret := ""
if f.MULaw {
ret += "PCMU"
} else {
ret += "PCMA"
}
ret += "/" + strconv.FormatInt(int64(f.SampleRate), 10)
if f.ChannelCount != 1 {
ret += "/" + strconv.FormatInt(int64(f.ChannelCount), 10)
}
return ret
}
// FMTP implements Format.
func (f *G711) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *G711) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *G711) CreateDecoder() (*rtplpcm.Decoder, error) {
d := &rtplpcm.Decoder{
BitDepth: 8,
ChannelCount: f.ChannelCount,
}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *G711) CreateEncoder() (*rtplpcm.Encoder, error) {
e := &rtplpcm.Encoder{
PayloadType: f.PayloadType(),
BitDepth: 8,
ChannelCount: f.ChannelCount,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,76 @@
package format
import (
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio"
)
// G722 is the RTP format for the G722 codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc3551
type G722 struct {
// in Go, empty structs share the same pointer,
// therefore they cannot be used as map keys
// or in equality operations. Prevent this.
unused int //nolint:unused
}
func (f *G722) unmarshal(_ *unmarshalContext) error {
return nil
}
// Codec implements Format.
func (f *G722) Codec() string {
return "G722"
}
// ClockRate implements Format.
func (f *G722) ClockRate() int {
return 8000
}
// PayloadType implements Format.
func (f *G722) PayloadType() uint8 {
return 9
}
// RTPMap implements Format.
func (f *G722) RTPMap() string {
return "G722/8000"
}
// FMTP implements Format.
func (f *G722) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *G722) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *G722) CreateDecoder() (*rtpsimpleaudio.Decoder, error) {
d := &rtpsimpleaudio.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *G722) CreateEncoder() (*rtpsimpleaudio.Encoder, error) {
e := &rtpsimpleaudio.Encoder{
PayloadType: 9,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,73 @@
package format
import (
"strconv"
"strings"
"github.com/pion/rtp"
)
// G726 is the RTP format for the G726 codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc3551
type G726 struct {
PayloadTyp uint8
BitRate int
BigEndian bool
}
func (f *G726) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
if strings.HasPrefix(ctx.codec, "aal2-") {
f.BigEndian = true
}
switch {
case strings.HasSuffix(ctx.codec, "-16"):
f.BitRate = 16
case strings.HasSuffix(ctx.codec, "-24"):
f.BitRate = 24
case strings.HasSuffix(ctx.codec, "-32"):
f.BitRate = 32
default:
f.BitRate = 40
}
return nil
}
// Codec implements Format.
func (f *G726) Codec() string {
return "G726"
}
// ClockRate implements Format.
func (f *G726) ClockRate() int {
return 8000
}
// PayloadType implements Format.
func (f *G726) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *G726) RTPMap() string {
codec := ""
if f.BigEndian {
codec += "AAL2-"
}
return codec + "G726-" + strconv.FormatInt(int64(f.BitRate), 10) + "/8000"
}
// FMTP implements Format.
func (f *G726) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *G726) PTSEqualsDTS(*rtp.Packet) bool {
return true
}

View File

@ -0,0 +1,111 @@
package format
import (
"fmt"
"strconv"
"strings"
"github.com/pion/rtp"
)
func findClockRate(payloadType uint8, rtpMap string, isApplication bool) (int, error) {
// get clock rate from payload type
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType {
case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18:
return 8000, nil
case 6:
return 16000, nil
case 10, 11:
return 44100, nil
case 14, 25, 26, 28, 31, 32, 33, 34:
return 90000, nil
case 16:
return 11025, nil
case 17:
return 22050, nil
}
// get clock rate from rtpmap
// https://tools.ietf.org/html/rfc4566
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
if rtpMap != "" {
if tmp := strings.Split(rtpMap, "/"); len(tmp) >= 2 {
v, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return 0, err
}
return int(v), nil
}
}
// application format without clock rate.
// do not throw an error, but return zero, that disables RTCP sender and receiver reports.
if isApplication || rtpMap != "" {
return 0, nil
}
return 0, fmt.Errorf("clock rate not found")
}
// Generic is a generic RTP format.
type Generic struct {
PayloadTyp uint8
RTPMa string
FMT map[string]string
// clock rate of the format. Filled when calling Init().
ClockRat int
}
// Init computes the clock rate of the format. It is mandatory to call it.
func (f *Generic) Init() error {
var err error
f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, true)
return err
}
func (f *Generic) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
f.RTPMa = ctx.rtpMap
f.FMT = ctx.fmtp
var err error
f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, ctx.mediaType == "application")
return err
}
// Codec implements Format.
func (f *Generic) Codec() string {
return "Generic"
}
// ClockRate implements Format.
func (f *Generic) ClockRate() int {
return f.ClockRat
}
// PayloadType implements Format.
func (f *Generic) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *Generic) RTPMap() string {
return f.RTPMa
}
// FMTP implements Format.
func (f *Generic) FMTP() map[string]string {
return f.FMT
}
// PTSEqualsDTS implements Format.
func (f *Generic) PTSEqualsDTS(*rtp.Packet) bool {
return true
}

View File

@ -0,0 +1,227 @@
package format
import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"strings"
"sync"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
)
// H264 is the RTP format for the H264 codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc6184
type H264 struct {
PayloadTyp uint8
SPS []byte
PPS []byte
PacketizationMode int
mutex sync.RWMutex
}
func (f *H264) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
for key, val := range ctx.fmtp {
switch key {
case "sprop-parameter-sets":
tmp := strings.Split(val, ",")
if len(tmp) >= 2 {
sps, err := base64.StdEncoding.DecodeString(tmp[0])
if err != nil {
return fmt.Errorf("invalid sprop-parameter-sets (%v)", val)
}
// some cameras ship parameters with Annex-B prefix
sps = bytes.TrimPrefix(sps, []byte{0, 0, 0, 1})
pps, err := base64.StdEncoding.DecodeString(tmp[1])
if err != nil {
return fmt.Errorf("invalid sprop-parameter-sets (%v)", val)
}
// some cameras ship parameters with Annex-B prefix
pps = bytes.TrimPrefix(pps, []byte{0, 0, 0, 1})
var spsp h264.SPS
err = spsp.Unmarshal(sps)
if err != nil {
continue
}
f.SPS = sps
f.PPS = pps
}
case "packetization-mode":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid packetization-mode (%v)", val)
}
f.PacketizationMode = int(tmp)
}
}
return nil
}
// Codec implements Format.
func (f *H264) Codec() string {
return "H264"
}
// ClockRate implements Format.
func (f *H264) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *H264) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *H264) RTPMap() string {
return "H264/90000"
}
// FMTP implements Format.
func (f *H264) FMTP() map[string]string {
f.mutex.RLock()
defer f.mutex.RUnlock()
fmtp := make(map[string]string)
if f.PacketizationMode != 0 {
fmtp["packetization-mode"] = strconv.FormatInt(int64(f.PacketizationMode), 10)
}
var tmp []string
if f.SPS != nil {
tmp = append(tmp, base64.StdEncoding.EncodeToString(f.SPS))
}
if f.PPS != nil {
tmp = append(tmp, base64.StdEncoding.EncodeToString(f.PPS))
}
if tmp != nil {
fmtp["sprop-parameter-sets"] = strings.Join(tmp, ",")
}
if len(f.SPS) >= 4 {
fmtp["profile-level-id"] = strings.ToUpper(hex.EncodeToString(f.SPS[1:4]))
}
return fmtp
}
// PTSEqualsDTS implements Format.
func (f *H264) PTSEqualsDTS(pkt *rtp.Packet) bool {
if len(pkt.Payload) == 0 {
return false
}
typ := h264.NALUType(pkt.Payload[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS:
return true
case 24: // STAP-A
payload := pkt.Payload[1:]
for {
if len(payload) < 2 {
return false
}
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 || int(size) > len(payload) {
return false
}
var nalu []byte
nalu, payload = payload[:size], payload[size:]
typ = h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS:
return true
}
if len(payload) == 0 {
break
}
}
case 28: // FU-A
if len(pkt.Payload) < 2 {
return false
}
start := pkt.Payload[1] >> 7
if start != 1 {
return false
}
typ := h264.NALUType(pkt.Payload[1] & 0x1F)
switch typ {
case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS:
return true
}
}
return false
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *H264) CreateDecoder() (*rtph264.Decoder, error) {
d := &rtph264.Decoder{
PacketizationMode: f.PacketizationMode,
}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *H264) CreateEncoder() (*rtph264.Encoder, error) {
e := &rtph264.Encoder{
PayloadType: f.PayloadTyp,
PacketizationMode: f.PacketizationMode,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}
// SafeSetParams sets the codec parameters.
func (f *H264) SafeSetParams(sps []byte, pps []byte) {
f.mutex.Lock()
defer f.mutex.Unlock()
f.SPS = sps
f.PPS = pps
}
// SafeParams returns the codec parameters.
func (f *H264) SafeParams() ([]byte, []byte) {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.SPS, f.PPS
}

View File

@ -0,0 +1,240 @@
package format
import (
"bytes"
"encoding/base64"
"fmt"
"strconv"
"sync"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
)
// H265 is the RTP format for the H265 codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc7798
type H265 struct {
PayloadTyp uint8
VPS []byte
SPS []byte
PPS []byte
MaxDONDiff int
mutex sync.RWMutex
}
func (f *H265) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
for key, val := range ctx.fmtp {
switch key {
case "sprop-vps":
var err error
f.VPS, err = base64.StdEncoding.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid sprop-vps (%v)", ctx.fmtp)
}
// some cameras ship parameters with Annex-B prefix
f.VPS = bytes.TrimPrefix(f.VPS, []byte{0, 0, 0, 1})
case "sprop-sps":
var err error
f.SPS, err = base64.StdEncoding.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid sprop-sps (%v)", ctx.fmtp)
}
// some cameras ship parameters with Annex-B prefix
f.SPS = bytes.TrimPrefix(f.SPS, []byte{0, 0, 0, 1})
var spsp h265.SPS
err = spsp.Unmarshal(f.SPS)
if err != nil {
return fmt.Errorf("invalid SPS: %w", err)
}
case "sprop-pps":
var err error
f.PPS, err = base64.StdEncoding.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid sprop-pps (%v)", ctx.fmtp)
}
// some cameras ship parameters with Annex-B prefix
f.PPS = bytes.TrimPrefix(f.PPS, []byte{0, 0, 0, 1})
var ppsp h265.PPS
err = ppsp.Unmarshal(f.PPS)
if err != nil {
return fmt.Errorf("invalid PPS: %w", err)
}
case "sprop-max-don-diff":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid sprop-max-don-diff (%v)", ctx.fmtp)
}
f.MaxDONDiff = int(tmp)
}
}
return nil
}
// Codec implements Format.
func (f *H265) Codec() string {
return "H265"
}
// ClockRate implements Format.
func (f *H265) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *H265) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *H265) RTPMap() string {
return "H265/90000"
}
// FMTP implements Format.
func (f *H265) FMTP() map[string]string {
f.mutex.RLock()
defer f.mutex.RUnlock()
fmtp := make(map[string]string)
if f.VPS != nil {
fmtp["sprop-vps"] = base64.StdEncoding.EncodeToString(f.VPS)
}
if f.SPS != nil {
fmtp["sprop-sps"] = base64.StdEncoding.EncodeToString(f.SPS)
}
if f.PPS != nil {
fmtp["sprop-pps"] = base64.StdEncoding.EncodeToString(f.PPS)
}
if f.MaxDONDiff != 0 {
fmtp["sprop-max-don-diff"] = strconv.FormatInt(int64(f.MaxDONDiff), 10)
}
return fmtp
}
// PTSEqualsDTS implements Format.
func (f *H265) PTSEqualsDTS(pkt *rtp.Packet) bool {
if len(pkt.Payload) == 0 {
return false
}
typ := h265.NALUType((pkt.Payload[0] >> 1) & 0b111111)
switch typ {
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT,
h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT:
return true
case h265.NALUType_AggregationUnit:
if len(pkt.Payload) < 4 {
return false
}
payload := pkt.Payload[2:]
for {
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 || int(size) > len(payload) {
return false
}
var nalu []byte
nalu, payload = payload[:size], payload[size:]
typ = h265.NALUType((nalu[0] >> 1) & 0b111111)
switch typ {
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT,
h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT:
return true
}
if len(payload) == 0 {
break
}
if len(payload) < 2 {
return false
}
}
case h265.NALUType_FragmentationUnit:
if len(pkt.Payload) < 3 {
return false
}
start := pkt.Payload[2] >> 7
if start != 1 {
return false
}
typ := h265.NALUType(pkt.Payload[2] & 0b111111)
switch typ {
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT,
h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT:
return true
}
}
return false
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *H265) CreateDecoder() (*rtph265.Decoder, error) {
d := &rtph265.Decoder{
MaxDONDiff: f.MaxDONDiff,
}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *H265) CreateEncoder() (*rtph265.Encoder, error) {
e := &rtph265.Encoder{
PayloadType: f.PayloadTyp,
MaxDONDiff: f.MaxDONDiff,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}
// SafeSetParams sets the codec parameters.
func (f *H265) SafeSetParams(vps []byte, sps []byte, pps []byte) {
f.mutex.Lock()
defer f.mutex.Unlock()
f.VPS = vps
f.SPS = sps
f.PPS = pps
}
// SafeParams returns the codec parameters.
func (f *H265) SafeParams() ([]byte, []byte, []byte) {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.VPS, f.SPS, f.PPS
}

View File

@ -0,0 +1,143 @@
package format
import (
"strconv"
"strings"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
)
// LPCM is the RTP format for the LPCM codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc3190
// Specification: https://datatracker.ietf.org/doc/html/rfc3551
type LPCM struct {
PayloadTyp uint8
BitDepth int
SampleRate int
ChannelCount int
}
func (f *LPCM) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
if ctx.payloadType == 10 {
f.BitDepth = 16
f.SampleRate = 44100
f.ChannelCount = 2
return nil
}
if ctx.payloadType == 11 {
f.BitDepth = 16
f.SampleRate = 44100
f.ChannelCount = 1
return nil
}
switch ctx.codec {
case "l8":
f.BitDepth = 8
case "l16":
f.BitDepth = 16
case "l24":
f.BitDepth = 24
}
tmp := strings.SplitN(ctx.clock, "/", 2)
tmp1, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil {
return err
}
f.SampleRate = int(tmp1)
if len(tmp) >= 2 {
tmp1, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return err
}
f.ChannelCount = int(tmp1)
} else {
f.ChannelCount = 1
}
return nil
}
// Codec implements Format.
func (f *LPCM) Codec() string {
return "LPCM"
}
// ClockRate implements Format.
func (f *LPCM) ClockRate() int {
return f.SampleRate
}
// PayloadType implements Format.
func (f *LPCM) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *LPCM) RTPMap() string {
var codec string
switch f.BitDepth {
case 8:
codec = "L8"
case 16:
codec = "L16"
case 24:
codec = "L24"
}
return codec + "/" + strconv.FormatInt(int64(f.SampleRate), 10) +
"/" + strconv.FormatInt(int64(f.ChannelCount), 10)
}
// FMTP implements Format.
func (f *LPCM) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *LPCM) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *LPCM) CreateDecoder() (*rtplpcm.Decoder, error) {
d := &rtplpcm.Decoder{
BitDepth: f.BitDepth,
ChannelCount: f.ChannelCount,
}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *LPCM) CreateEncoder() (*rtplpcm.Encoder, error) {
e := &rtplpcm.Encoder{
PayloadType: f.PayloadTyp,
BitDepth: f.BitDepth,
ChannelCount: f.ChannelCount,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,74 @@
package format //nolint:dupl
import (
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg"
)
// MJPEG is the RTP format for the Motion-JPEG codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
type MJPEG struct {
// in Go, empty structs share the same pointer,
// therefore they cannot be used as map keys
// or in equality operations. Prevent this.
unused int //nolint:unused
}
func (f *MJPEG) unmarshal(_ *unmarshalContext) error {
return nil
}
// Codec implements Format.
func (f *MJPEG) Codec() string {
return "M-JPEG"
}
// ClockRate implements Format.
func (f *MJPEG) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MJPEG) PayloadType() uint8 {
return 26
}
// RTPMap implements Format.
func (f *MJPEG) RTPMap() string {
return "JPEG/90000"
}
// FMTP implements Format.
func (f *MJPEG) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *MJPEG) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MJPEG) CreateDecoder() (*rtpmjpeg.Decoder, error) {
d := &rtpmjpeg.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MJPEG) CreateEncoder() (*rtpmjpeg.Encoder, error) {
e := &rtpmjpeg.Encoder{}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,74 @@
package format //nolint:dupl
import (
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio"
)
// MPEG1Audio is the RTP format for a MPEG-1/2 Audio codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type MPEG1Audio struct {
// in Go, empty structs share the same pointer,
// therefore they cannot be used as map keys
// or in equality operations. Prevent this.
unused int //nolint:unused
}
func (f *MPEG1Audio) unmarshal(_ *unmarshalContext) error {
return nil
}
// Codec implements Format.
func (f *MPEG1Audio) Codec() string {
return "MPEG-1/2 Audio"
}
// ClockRate implements Format.
func (f *MPEG1Audio) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MPEG1Audio) PayloadType() uint8 {
return 14
}
// RTPMap implements Format.
func (f *MPEG1Audio) RTPMap() string {
return ""
}
// FMTP implements Format.
func (f *MPEG1Audio) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *MPEG1Audio) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG1Audio) CreateDecoder() (*rtpmpeg1audio.Decoder, error) {
d := &rtpmpeg1audio.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG1Audio) CreateEncoder() (*rtpmpeg1audio.Encoder, error) {
e := &rtpmpeg1audio.Encoder{}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,74 @@
package format //nolint:dupl
import (
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video"
)
// MPEG1Video is the RTP format for a MPEG-1/2 Video codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type MPEG1Video struct {
// in Go, empty structs share the same pointer,
// therefore they cannot be used as map keys
// or in equality operations. Prevent this.
unused int //nolint:unused
}
func (f *MPEG1Video) unmarshal(_ *unmarshalContext) error {
return nil
}
// Codec implements Format.
func (f *MPEG1Video) Codec() string {
return "MPEG-1/2 Video"
}
// ClockRate implements Format.
func (f *MPEG1Video) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MPEG1Video) PayloadType() uint8 {
return 32
}
// RTPMap implements Format.
func (f *MPEG1Video) RTPMap() string {
return ""
}
// FMTP implements Format.
func (f *MPEG1Video) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *MPEG1Video) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG1Video) CreateDecoder() (*rtpmpeg1video.Decoder, error) {
d := &rtpmpeg1video.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG1Video) CreateEncoder() (*rtpmpeg1video.Encoder, error) {
e := &rtpmpeg1video.Encoder{}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,343 @@
package format
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio"
)
// MPEG4Audio is the RTP format for a MPEG-4 Audio codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3
type MPEG4Audio struct {
// payload type of packets.
PayloadTyp uint8
// use LATM format (RFC6416) instead of generic format (RFC3640).
LATM bool
// profile level ID.
ProfileLevelID int
// generic only
Config *mpeg4audio.Config
SizeLength int
IndexLength int
IndexDeltaLength int
// LATM only
Bitrate *int
CPresent bool
StreamMuxConfig *mpeg4audio.StreamMuxConfig
SBREnabled *bool
}
func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
f.LATM = (ctx.codec != "mpeg4-generic")
if !f.LATM {
for key, val := range ctx.fmtp {
switch key {
case "streamtype":
if val != "5" { // AudioStream in ISO 14496-1
return fmt.Errorf("streamtype of AAC must be 5")
}
case "mode":
if strings.ToLower(val) != "aac-hbr" && strings.ToLower(val) != "aac_hbr" {
return fmt.Errorf("unsupported AAC mode: %v", val)
}
case "profile-level-id":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid profile-level-id: %v", val)
}
f.ProfileLevelID = int(tmp)
case "config":
enc, err := hex.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid AAC config: %v", val)
}
f.Config = &mpeg4audio.Config{}
err = f.Config.Unmarshal(enc)
if err != nil {
return fmt.Errorf("invalid AAC config: %v", val)
}
case "sizelength":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil || n > 100 {
return fmt.Errorf("invalid AAC SizeLength: %v", val)
}
f.SizeLength = int(n)
case "indexlength":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil || n > 100 {
return fmt.Errorf("invalid AAC IndexLength: %v", val)
}
f.IndexLength = int(n)
case "indexdeltalength":
n, err := strconv.ParseUint(val, 10, 31)
if err != nil || n > 100 {
return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val)
}
f.IndexDeltaLength = int(n)
}
}
if f.Config == nil {
return fmt.Errorf("config is missing")
}
if f.SizeLength == 0 {
return fmt.Errorf("sizelength is missing")
}
} else {
// default value set by specification
f.ProfileLevelID = 30
f.CPresent = true
for key, val := range ctx.fmtp {
switch key {
case "profile-level-id":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid profile-level-id: %v", val)
}
f.ProfileLevelID = int(tmp)
case "bitrate":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid bitrate: %v", val)
}
v := int(tmp)
f.Bitrate = &v
case "cpresent":
f.CPresent = (val == "1")
case "config":
enc, err := hex.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid AAC config: %v", val)
}
f.StreamMuxConfig = &mpeg4audio.StreamMuxConfig{}
err = f.StreamMuxConfig.Unmarshal(enc)
if err != nil {
return fmt.Errorf("invalid AAC config: %w", err)
}
case "sbr-enabled":
v := (val == "1")
f.SBREnabled = &v
}
}
if f.CPresent {
if f.StreamMuxConfig != nil {
return fmt.Errorf("config and cpresent can't be used at the same time")
}
} else {
if f.StreamMuxConfig == nil {
return fmt.Errorf("config is missing")
}
}
}
return nil
}
// Codec implements Format.
func (f *MPEG4Audio) Codec() string {
return "MPEG-4 Audio"
}
// ClockRate implements Format.
func (f *MPEG4Audio) ClockRate() int {
if !f.LATM {
return f.Config.SampleRate
}
if f.CPresent {
return 16000
}
return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.SampleRate
}
// PayloadType implements Format.
func (f *MPEG4Audio) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *MPEG4Audio) RTPMap() string {
if !f.LATM {
sampleRate := f.Config.SampleRate
if f.Config.ExtensionSampleRate != 0 {
sampleRate = f.Config.ExtensionSampleRate
}
channelCount := f.Config.ChannelCount
if f.Config.ExtensionType == mpeg4audio.ObjectTypePS {
channelCount = 2
}
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
"/" + strconv.FormatInt(int64(channelCount), 10)
}
if f.CPresent {
return "MP4A-LATM/16000/1"
}
aoc := f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig
sampleRate := aoc.SampleRate
if aoc.ExtensionSampleRate != 0 {
sampleRate = aoc.ExtensionSampleRate
}
channelCount := aoc.ChannelCount
if aoc.ExtensionType == mpeg4audio.ObjectTypePS {
channelCount = 2
}
return "MP4A-LATM/" + strconv.FormatInt(int64(sampleRate), 10) +
"/" + strconv.FormatInt(int64(channelCount), 10)
}
// FMTP implements Format.
func (f *MPEG4Audio) FMTP() map[string]string {
if !f.LATM {
enc, err := f.Config.Marshal()
if err != nil {
return nil
}
profileLevelID := f.ProfileLevelID
if profileLevelID == 0 { // support legacy definition which didn't include profile-level-id
profileLevelID = 1
}
fmtp := map[string]string{
"streamtype": "5",
"mode": "AAC-hbr",
"profile-level-id": strconv.FormatInt(int64(profileLevelID), 10),
}
if f.SizeLength > 0 {
fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10)
}
if f.IndexLength > 0 {
fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10)
}
if f.IndexDeltaLength > 0 {
fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10)
}
fmtp["config"] = hex.EncodeToString(enc)
return fmtp
}
fmtp := map[string]string{
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
}
if f.Bitrate != nil {
fmtp["bitrate"] = strconv.FormatInt(int64(*f.Bitrate), 10)
}
if f.CPresent {
fmtp["cpresent"] = "1"
} else {
fmtp["cpresent"] = "0"
enc, err := f.StreamMuxConfig.Marshal()
if err != nil {
return nil
}
fmtp["config"] = hex.EncodeToString(enc)
fmtp["object"] = strconv.FormatInt(int64(f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.Type), 10)
}
if f.SBREnabled != nil {
if *f.SBREnabled {
fmtp["SBR-enabled"] = "1"
} else {
fmtp["SBR-enabled"] = "0"
}
}
return fmtp
}
// PTSEqualsDTS implements Format.
func (f *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) {
d := &rtpmpeg4audio.Decoder{
LATM: f.LATM,
SizeLength: f.SizeLength,
IndexLength: f.IndexLength,
IndexDeltaLength: f.IndexDeltaLength,
}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) {
e := &rtpmpeg4audio.Encoder{
LATM: f.LATM,
PayloadType: f.PayloadTyp,
SizeLength: f.SizeLength,
IndexLength: f.IndexLength,
IndexDeltaLength: f.IndexDeltaLength,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}
// GetConfig returns the MPEG-4 Audio configuration.
func (f *MPEG4Audio) GetConfig() *mpeg4audio.Config {
if !f.LATM {
return f.Config
}
if f.CPresent {
return nil
}
return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig
}

View File

@ -0,0 +1,133 @@
package format
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"sync"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video"
)
// MPEG4Video is the RTP format for a MPEG-4 Video codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.1
type MPEG4Video struct {
PayloadTyp uint8
ProfileLevelID int
Config []byte
mutex sync.RWMutex
}
func (f *MPEG4Video) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
f.ProfileLevelID = 1 // default value imposed by specification
for key, val := range ctx.fmtp {
switch key {
case "profile-level-id":
tmp, err := strconv.ParseUint(val, 10, 31)
if err != nil {
return fmt.Errorf("invalid profile-level-id: %v", val)
}
f.ProfileLevelID = int(tmp)
case "config":
var err error
f.Config, err = hex.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid config: %v", val)
}
err = mpeg4video.IsValidConfig(f.Config)
if err != nil {
return fmt.Errorf("invalid config: %w", err)
}
}
}
return nil
}
// Codec implements Format.
func (f *MPEG4Video) Codec() string {
return "MPEG-4 Video"
}
// ClockRate implements Format.
func (f *MPEG4Video) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MPEG4Video) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *MPEG4Video) RTPMap() string {
return "MP4V-ES/90000"
}
// FMTP implements Format.
func (f *MPEG4Video) FMTP() map[string]string {
fmtp := map[string]string{
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
}
if f.Config != nil {
fmtp["config"] = strings.ToUpper(hex.EncodeToString(f.Config))
}
return fmtp
}
// PTSEqualsDTS implements Format.
func (f *MPEG4Video) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG4Video) CreateDecoder() (*rtpmpeg4video.Decoder, error) {
d := &rtpmpeg4video.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG4Video) CreateEncoder() (*rtpmpeg4video.Encoder, error) {
e := &rtpmpeg4video.Encoder{
PayloadType: f.PayloadTyp,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}
// SafeSetParams sets the codec parameters.
func (f *MPEG4Video) SafeSetParams(config []byte) {
f.mutex.Lock()
defer f.mutex.Unlock()
f.Config = config
}
// SafeParams returns the codec parameters.
func (f *MPEG4Video) SafeParams() []byte {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.Config
}

View File

@ -0,0 +1,48 @@
package format //nolint:dupl
import (
"github.com/pion/rtp"
)
// MPEGTS is the RTP format for MPEG-TS.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type MPEGTS struct {
// in Go, empty structs share the same pointer,
// therefore they cannot be used as map keys
// or in equality operations. Prevent this.
unused int //nolint:unused
}
func (f *MPEGTS) unmarshal(_ *unmarshalContext) error {
return nil
}
// Codec implements Format.
func (f *MPEGTS) Codec() string {
return "MPEG-TS"
}
// ClockRate implements Format.
func (f *MPEGTS) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MPEGTS) PayloadType() uint8 {
return 33
}
// RTPMap implements Format.
func (f *MPEGTS) RTPMap() string {
return "MP2T/90000"
}
// FMTP implements Format.
func (f *MPEGTS) FMTP() map[string]string {
return nil
}
// PTSEqualsDTS implements Format.
func (f *MPEGTS) PTSEqualsDTS(*rtp.Packet) bool {
return true
}

View File

@ -0,0 +1,199 @@
package format
import (
"fmt"
"strconv"
"strings"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio"
)
// Opus is the RTP format for the Opus codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc7587
// Specification: https://webrtc-review.googlesource.com/c/src/+/129768
type Opus struct {
PayloadTyp uint8
ChannelCount int
//
// Deprecated: replaced by ChannelCount.
IsStereo bool
}
func (f *Opus) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType
if ctx.codec == "opus" {
tmp := strings.SplitN(ctx.clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock (%v)", ctx.clock)
}
sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil || sampleRate != 48000 {
return fmt.Errorf("invalid sample rate: '%s", tmp[0])
}
channelCount, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil || channelCount != 2 {
return fmt.Errorf("invalid channel count: '%s'", tmp[1])
}
// assume mono
f.ChannelCount = 1
f.IsStereo = false
for key, val := range ctx.fmtp {
if key == "sprop-stereo" {
if val == "1" {
f.ChannelCount = 2
f.IsStereo = true
}
}
}
} else {
tmp := strings.SplitN(ctx.clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock (%v)", ctx.clock)
}
sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil || sampleRate != 48000 {
return fmt.Errorf("invalid sample rate: '%s'", tmp[0])
}
channelCount, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return fmt.Errorf("invalid channel count: '%s'", tmp[1])
}
f.ChannelCount = int(channelCount)
}
return nil
}
// Codec implements Format.
func (f *Opus) Codec() string {
return "Opus"
}
// ClockRate implements Format.
func (f *Opus) ClockRate() int {
// RFC7587: the RTP timestamp is incremented with a 48000 Hz
// clock rate for all modes of Opus and all sampling rates.
return 48000
}
// PayloadType implements Format.
func (f *Opus) PayloadType() uint8 {
return f.PayloadTyp
}
// RTPMap implements Format.
func (f *Opus) RTPMap() string {
if f.ChannelCount <= 2 {
// RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the
// number of channels MUST be 2.
return "opus/48000/2"
}
return "multiopus/48000/" + strconv.FormatUint(uint64(f.ChannelCount), 10)
}
// FMTP implements Format.
func (f *Opus) FMTP() map[string]string {
if f.ChannelCount <= 2 {
return map[string]string{
"sprop-stereo": func() string {
if f.ChannelCount == 2 || (f.ChannelCount == 0 && f.IsStereo) {
return "1"
}
return "0"
}(),
}
}
switch f.ChannelCount {
case 3:
return map[string]string{
"num_streams": "2",
"coupled_streams": "1",
"channel_mapping": "0,2,1",
"sprop-maxcapturerate": "48000",
}
case 4:
return map[string]string{
"num_streams": "2",
"coupled_streams": "2",
"channel_mapping": "0,1,2,3",
"sprop-maxcapturerate": "48000",
}
case 5:
return map[string]string{
"num_streams": "3",
"coupled_streams": "2",
"channel_mapping": "0,4,1,2,3",
"sprop-maxcapturerate": "48000",
}
case 6:
return map[string]string{
"num_streams": "4",
"coupled_streams": "2",
"channel_mapping": "0,4,1,2,3,5",
"sprop-maxcapturerate": "48000",
}
case 7:
return map[string]string{
"num_streams": "4",
"coupled_streams": "3",
"channel_mapping": "0,4,1,2,3,5,6",
"sprop-maxcapturerate": "48000",
}
default: // assume 8
return map[string]string{
"num_streams": "5",
"coupled_streams": "3",
"channel_mapping": "0,6,1,4,5,2,3,7",
"sprop-maxcapturerate": "48000",
}
}
}
// PTSEqualsDTS implements Format.
func (f *Opus) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *Opus) CreateDecoder() (*rtpsimpleaudio.Decoder, error) {
d := &rtpsimpleaudio.Decoder{}
err := d.Init()
if err != nil {
return nil, err
}
return d, nil
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *Opus) CreateEncoder() (*rtpsimpleaudio.Encoder, error) {
e := &rtpsimpleaudio.Encoder{
PayloadType: f.PayloadTyp,
}
err := e.Init()
if err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,149 @@
package rtpac3
import (
"errors"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented frame and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a AC-3 decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc4184
type Decoder struct {
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
fragmentsExpected int
fragmentNextSeqNum uint16
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes frames from a RTP packet.
// It returns the frames and the PTS of the first frame.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 2 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
mbz := pkt.Payload[0] >> 2
ft := pkt.Payload[0] & 0b11
if mbz != 0 {
d.resetFragments()
return nil, fmt.Errorf("invalid MBZ: %v", mbz)
}
var frames [][]byte
switch ft {
case 0:
d.resetFragments()
d.firstPacketReceived = true
buf := pkt.Payload[2:]
for {
var syncInfo ac3.SyncInfo
err := syncInfo.Unmarshal(buf)
if err != nil {
return nil, err
}
size := syncInfo.FrameSize()
if len(buf) < size {
return nil, fmt.Errorf("payload is too short")
}
frames = append(frames, buf[:size])
buf = buf[size:]
if len(buf) == 0 {
break
}
}
case 1, 2:
d.resetFragments()
var syncInfo ac3.SyncInfo
err := syncInfo.Unmarshal(pkt.Payload[2:])
if err != nil {
return nil, err
}
size := syncInfo.FrameSize()
le := len(pkt.Payload[2:])
d.fragmentsSize = le
d.fragmentsExpected = size - le
d.fragments = append(d.fragments, pkt.Payload[2:])
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
d.firstPacketReceived = true
return nil, ErrMorePacketsNeeded
case 3:
if d.fragmentsSize == 0 {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
return nil, fmt.Errorf("received a subsequent fragment without previous fragments")
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
le := len(pkt.Payload[2:])
d.fragmentsSize += le
d.fragmentsExpected -= le
if d.fragmentsExpected < 0 {
d.resetFragments()
return nil, fmt.Errorf("fragment is too big")
}
d.fragments = append(d.fragments, pkt.Payload[2:])
d.fragmentNextSeqNum++
if d.fragmentsExpected > 0 {
return nil, ErrMorePacketsNeeded
}
frames = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
}
return frames, nil
}

View File

@ -0,0 +1,201 @@
package rtpac3
import (
"crypto/rand"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a AC-3 encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc4184
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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes frames into RTP packets.
func (e *Encoder) Encode(frames [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
timestamp := uint32(0)
// split frames into batches
for _, frame := range frames {
if e.lenAggregated(batch, frame) <= e.PayloadMaxSize {
// add to existing batch
batch = append(batch, frame)
} else {
// write current batch
if batch != nil {
pkts, err := e.writeBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
timestamp += uint32(len(batch)) * ac3.SamplesPerFrame
}
// initialize new batch
batch = [][]byte{frame}
}
}
// write last batch
pkts, err := e.writeBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
return rets, nil
}
func (e *Encoder) writeBatch(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
if len(frames) != 1 || e.lenAggregated(frames, nil) < e.PayloadMaxSize {
return e.writeAggregated(frames, timestamp)
}
return e.writeFragmented(frames[0], timestamp)
}
func (e *Encoder) writeFragmented(frame []byte, timestamp uint32) ([]*rtp.Packet, error) {
avail := e.PayloadMaxSize - 4
le := len(frame)
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
le = avail
ft := uint8(2)
if avail >= (len(frame) * 5 / 8) {
ft = 1
}
for i := range ret {
if i == (packetCount - 1) {
le = len(frame)
}
payload := make([]byte, 2+le)
payload[0] = ft
payload[1] = uint8(packetCount)
n := copy(payload[2:], frame)
frame = frame[n:]
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: i == (packetCount - 1),
},
Payload: payload,
}
e.sequenceNumber++
ft = 3
}
return ret, nil
}
func (e *Encoder) lenAggregated(frames [][]byte, addFrame []byte) int {
n := 2 + len(addFrame)
for _, frame := range frames {
n += len(frame)
}
return n
}
func (e *Encoder) writeAggregated(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
payload := make([]byte, e.lenAggregated(frames, nil))
payload[1] = uint8(len(frames))
n := 2
for _, frame := range frames {
n += copy(payload[n:], frame)
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: true,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpac3 contains a RTP/AC-3 decoder and encoder.
package rtpac3

View File

@ -0,0 +1,202 @@
package rtpav1
import (
"errors"
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/pion/rtp"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented NALU and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
func tuSize(tu [][]byte) int {
s := 0
for _, obu := range tu {
s += len(obu)
}
return s
}
// Decoder is a RTP/AV1 decoder.
// Specification: https://aomediacodec.github.io/av1-rtp-spec/
type Decoder struct {
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
fragmentNextSeqNum uint16
// for Decode()
frameBuffer [][]byte
frameBufferLen int
frameBufferSize int
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
func (d *Decoder) decodeOBUs(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 2 {
return nil, fmt.Errorf("invalid payload size")
}
z := (pkt.Payload[0] & 0b10000000) != 0
y := (pkt.Payload[0] & 0b01000000) != 0
w := (pkt.Payload[0] >> 4) & 0b11
payload := pkt.Payload[1:]
var obus [][]byte
for len(payload) > 0 {
var obu []byte
if w == 0 || byte(len(obus)) < (w-1) {
var size av1.LEB128
n, err := size.Unmarshal(payload)
if err != nil {
d.resetFragments()
return nil, err
}
payload = payload[n:]
if size == 0 || len(payload) < int(size) {
d.resetFragments()
return nil, fmt.Errorf("invalid OBU size")
}
obu, payload = payload[:size], payload[size:]
} else {
obu, payload = payload, nil
}
obus = append(obus, obu)
}
if w != 0 && len(obus) != int(w) {
return nil, fmt.Errorf("invalid W field")
}
// first OBU is continuation of previous one
if z {
if d.fragmentsSize == 0 {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
return nil, fmt.Errorf("received a subsequent fragment without previous fragments")
}
d.firstPacketReceived = true
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += len(obus[0])
if d.fragmentsSize > av1.MaxTemporalUnitSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("temporal unit size (%d) is too big, maximum is %d",
errSize, av1.MaxTemporalUnitSize)
}
d.fragments = append(d.fragments, obus[0])
d.fragmentNextSeqNum++
if len(obus) == 1 && y {
return nil, ErrMorePacketsNeeded
}
obus[0] = joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
} else {
d.firstPacketReceived = true
}
// last OBU will continue in next packet
if y {
var obu []byte
obu, obus = obus[len(obus)-1], obus[:len(obus)-1]
d.fragmentsSize = len(obu)
d.fragments = append(d.fragments, obu)
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
if len(obus) == 0 {
return nil, ErrMorePacketsNeeded
}
}
return obus, nil
}
// Decode decodes a temporal unit from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
obus, err := d.decodeOBUs(pkt)
if err != nil {
return nil, err
}
l := len(obus)
if (d.frameBufferLen + l) > av1.MaxOBUsPerTemporalUnit {
errCount := d.frameBufferLen + l
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return nil, fmt.Errorf("OBU count (%d) exceeds maximum allowed (%d)",
errCount, av1.MaxOBUsPerTemporalUnit)
}
addSize := tuSize(obus)
if (d.frameBufferSize + addSize) > av1.MaxTemporalUnitSize {
errSize := d.frameBufferSize + addSize
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return nil, fmt.Errorf("temporal unit size (%d) is too big, maximum is %d",
errSize, av1.MaxOBUsPerTemporalUnit)
}
d.frameBuffer = append(d.frameBuffer, obus...)
d.frameBufferLen += l
d.frameBufferSize += addSize
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
ret := d.frameBuffer
// do not reuse frameBuffer to avoid race conditions
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return ret, nil
}

View File

@ -0,0 +1,171 @@
package rtpav1
import (
"crypto/rand"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/AV1 encoder.
// Specification: https://aomediacodec.github.io/av1-rtp-spec/
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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes OBUs into RTP packets.
func (e *Encoder) Encode(obus [][]byte) ([]*rtp.Packet, error) {
var curPacket *rtp.Packet
var packets []*rtp.Packet
obusInPacket := 0
createNewPacket := func(z bool) {
curPacket = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
},
Payload: []byte{0},
}
e.sequenceNumber++
packets = append(packets, curPacket)
obusInPacket = 0
if z {
curPacket.Payload[0] |= 1 << 7
}
}
finalizeCurPacket := func(y bool) {
if y {
curPacket.Payload[0] |= 1 << 6
}
}
createNewPacket(false)
maxFragmentedLEBSize := av1.LEB128(e.PayloadMaxSize).MarshalSize()
for i, obu := range obus {
for {
avail := e.PayloadMaxSize - len(curPacket.Payload)
obuLen := len(obu)
omitSize := (i == (len(obus)-1) && obusInPacket < 3)
var obuLenLEB av1.LEB128
var obuLenLEBSize int
var needed int
if omitSize {
needed = obuLen
} else {
obuLenLEB = av1.LEB128(obuLen)
obuLenLEBSize = obuLenLEB.MarshalSize()
needed = obuLen + obuLenLEBSize
}
if needed <= avail {
if omitSize {
curPacket.Payload[0] |= byte((obusInPacket + 1) << 4) // W
curPacket.Payload = append(curPacket.Payload, obu...)
} else {
buf := make([]byte, obuLenLEBSize)
obuLenLEB.MarshalTo(buf)
curPacket.Payload = append(curPacket.Payload, buf...)
curPacket.Payload = append(curPacket.Payload, obu...)
obusInPacket++
}
break
}
if omitSize {
if avail > 0 {
curPacket.Payload[0] |= byte((obusInPacket + 1) << 4) // W
curPacket.Payload = append(curPacket.Payload, obu[:avail]...)
obu = obu[avail:]
}
} else {
if avail > maxFragmentedLEBSize {
fragmentLen := avail - maxFragmentedLEBSize
fragmentLenLEB := av1.LEB128(fragmentLen)
fragmentLenLEBSize := fragmentLenLEB.MarshalSize()
buf := make([]byte, fragmentLenLEBSize)
fragmentLenLEB.MarshalTo(buf)
curPacket.Payload = append(curPacket.Payload, buf...)
curPacket.Payload = append(curPacket.Payload, obu[:fragmentLen]...)
obu = obu[fragmentLen:]
}
}
finalizeCurPacket(true)
createNewPacket(true)
}
}
finalizeCurPacket(false)
if av1.IsRandomAccess2(obus) {
packets[0].Payload[0] |= 1 << 3
}
packets[len(packets)-1].Marker = true
return packets, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpav1 contains a RTP/AV1 decoder and encoder.
package rtpav1

View File

@ -0,0 +1,306 @@
package rtph264
import (
"bytes"
"errors"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented NALU and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
func isAllZero(buf []byte) bool {
for _, b := range buf {
if b != 0 {
return false
}
}
return true
}
func auSize(au [][]byte) int {
s := 0
for _, nalu := range au {
s += len(nalu)
}
return s
}
// Decoder is a RTP/H264 decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc6184
type Decoder struct {
// indicates the packetization mode.
PacketizationMode int
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
fragmentNextSeqNum uint16
annexBMode bool
// for Decode()
frameBuffer [][]byte
frameBufferLen int
frameBufferSize int
frameBufferTimestamp uint32
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
if d.PacketizationMode >= 2 {
return fmt.Errorf("PacketizationMode >= 2 is not supported")
}
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
func (d *Decoder) decodeNALUs(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 1 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
typ := h264.NALUType(pkt.Payload[0] & 0x1F)
var nalus [][]byte
switch typ {
case h264.NALUTypeFUA:
if len(pkt.Payload) < 2 {
return nil, fmt.Errorf("invalid FU-A packet (invalid size)")
}
start := pkt.Payload[1] >> 7
end := (pkt.Payload[1] >> 6) & 0x01
if start == 1 {
d.resetFragments()
nri := (pkt.Payload[0] >> 5) & 0x03
typ := pkt.Payload[1] & 0x1F
d.fragmentsSize = len(pkt.Payload[1:])
d.fragments = append(d.fragments, []byte{(nri << 5) | typ}, pkt.Payload[2:])
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
d.firstPacketReceived = true
// RFC 6184 clearly states:
//
// A fragmented NAL unit MUST NOT be transmitted in one FU; that is, the
// Start bit and End bit MUST NOT both be set to one in the same FU
// header.
//
// However, some vendors camera (e.g. CostarHD) have been observed to nevertheless
// emit one fragmented NAL unit for sufficiently small P-frames.
if end != 0 {
nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
break
}
return nil, ErrMorePacketsNeeded
}
if d.fragmentsSize == 0 {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
return nil, fmt.Errorf("invalid FU-A packet (non-starting)")
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += len(pkt.Payload[2:])
if d.fragmentsSize > h264.MaxAccessUnitSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("NALU size (%d) is too big, maximum is %d",
errSize, h264.MaxAccessUnitSize)
}
d.fragments = append(d.fragments, pkt.Payload[2:])
d.fragmentNextSeqNum++
if end != 1 {
return nil, ErrMorePacketsNeeded
}
nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
case h264.NALUTypeSTAPA:
d.resetFragments()
payload := pkt.Payload[1:]
for {
if len(payload) < 2 {
return nil, fmt.Errorf("invalid STAP-A packet (invalid size)")
}
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 {
// discard padding
if isAllZero(payload) {
break
}
return nil, fmt.Errorf("invalid STAP-A packet (invalid size)")
}
if int(size) > len(payload) {
return nil, fmt.Errorf("invalid STAP-A packet (invalid size)")
}
nalus = append(nalus, payload[:size])
payload = payload[size:]
if len(payload) == 0 {
break
}
}
if nalus == nil {
return nil, fmt.Errorf("STAP-A packet doesn't contain any NALU")
}
d.firstPacketReceived = true
case h264.NALUTypeSTAPB, h264.NALUTypeMTAP16,
h264.NALUTypeMTAP24, h264.NALUTypeFUB:
d.resetFragments()
d.firstPacketReceived = true
return nil, fmt.Errorf("packet type not supported (%v)", typ)
default:
d.resetFragments()
d.firstPacketReceived = true
nalus = [][]byte{pkt.Payload}
}
nalus, err := d.removeAnnexB(nalus)
if err != nil {
return nil, err
}
return nalus, nil
}
// Decode decodes an access unit from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
nalus, err := d.decodeNALUs(pkt)
if err != nil {
return nil, err
}
l := len(nalus)
// support splitting access units by timestamp.
// (some cameras do not use the Marker field, like the FLIR M400)
if d.frameBuffer != nil && pkt.Timestamp != d.frameBufferTimestamp {
ret := d.frameBuffer
d.resetFrameBuffer()
err = d.addToFrameBuffer(nalus, l, pkt.Timestamp)
if err != nil {
return nil, err
}
return ret, nil
}
err = d.addToFrameBuffer(nalus, l, pkt.Timestamp)
if err != nil {
return nil, err
}
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
ret := d.frameBuffer
d.resetFrameBuffer()
return ret, nil
}
func (d *Decoder) resetFrameBuffer() {
d.frameBuffer = nil // do not reuse frameBuffer to avoid race conditions
d.frameBufferLen = 0
d.frameBufferSize = 0
}
func (d *Decoder) addToFrameBuffer(nalus [][]byte, l int, ts uint32) error {
if (d.frameBufferLen + l) > h264.MaxNALUsPerAccessUnit {
errCount := d.frameBufferLen + l
d.resetFrameBuffer()
return fmt.Errorf("NALU count (%d) exceeds maximum allowed (%d)",
errCount, h264.MaxNALUsPerAccessUnit)
}
addSize := auSize(nalus)
if (d.frameBufferSize + addSize) > h264.MaxAccessUnitSize {
errSize := d.frameBufferSize + addSize
d.resetFrameBuffer()
return fmt.Errorf("access unit size (%d) is too big, maximum is %d",
errSize, h264.MaxAccessUnitSize)
}
d.frameBuffer = append(d.frameBuffer, nalus...)
d.frameBufferLen += l
d.frameBufferSize += addSize
d.frameBufferTimestamp = ts
return nil
}
// some cameras / servers wrap NALUs into Annex-B
func (d *Decoder) removeAnnexB(nalus [][]byte) ([][]byte, error) {
if len(nalus) == 1 {
nalu := nalus[0]
if !d.annexBMode && bytes.Contains(nalu, []byte{0x00, 0x00, 0x00, 0x01}) {
d.annexBMode = true
}
if d.annexBMode {
if !bytes.HasPrefix(nalu, []byte{0x00, 0x00, 0x00, 0x01}) {
nalu = append([]byte{0x00, 0x00, 0x00, 0x01}, nalu...)
}
var annexb h264.AnnexB
err := annexb.Unmarshal(nalu)
return annexb, err
}
}
return nalus, nil
}

View File

@ -0,0 +1,248 @@
package rtph264
import (
"crypto/rand"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func lenAggregated(nalus [][]byte, addNALU []byte) int {
n := 1 // header
for _, nalu := range nalus {
n += 2 // size
n += len(nalu) // nalu
}
if addNALU != nil {
n += 2 // size
n += len(addNALU) // nalu
}
return n
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a RTP/H264 encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc6184
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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
PacketizationMode int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.PacketizationMode >= 2 {
return fmt.Errorf("PacketizationMode >= 2 is not supported")
}
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes an access unit into RTP/H264 packets.
func (e *Encoder) Encode(au [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
// split NALUs into batches
for _, nalu := range au {
if lenAggregated(batch, nalu) <= e.PayloadMaxSize {
// add to existing batch
batch = append(batch, nalu)
} else {
// write current batch
if batch != nil {
pkts, err := e.writeBatch(batch, 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, true)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
return rets, nil
}
func (e *Encoder) writeBatch(nalus [][]byte, 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], marker)
}
// split the NALU into multiple fragmentation packet
return e.writeFragmented(nalus[0], marker)
}
return e.writeAggregated(nalus, marker)
}
func (e *Encoder) writeSingle(nalu []byte, marker bool) ([]*rtp.Packet, error) {
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: marker,
},
Payload: nalu,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}
func (e *Encoder) writeFragmented(nalu []byte, marker bool) ([]*rtp.Packet, error) {
// use only FU-A, not FU-B, since we always use non-interleaved mode
// (packetization-mode=1)
avail := e.PayloadMaxSize - 2
le := len(nalu) - 1
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
nri := (nalu[0] >> 5) & 0x03
typ := nalu[0] & 0x1F
nalu = nalu[1:] // remove header
le = avail
start := uint8(1)
end := uint8(0)
for i := range ret {
if i == (packetCount - 1) {
end = 1
le = len(nalu)
}
data := make([]byte, 2+le)
data[0] = (nri << 5) | uint8(h264.NALUTypeFUA)
data[1] = (start << 7) | (end << 6) | typ
copy(data[2:], nalu)
nalu = nalu[le:]
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: (i == (packetCount-1) && marker),
},
Payload: data,
}
e.sequenceNumber++
start = 0
}
return ret, nil
}
func (e *Encoder) writeAggregated(nalus [][]byte, marker bool) ([]*rtp.Packet, error) {
payload := make([]byte, lenAggregated(nalus, nil))
// header
payload[0] = uint8(h264.NALUTypeSTAPA)
pos := 1
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,
SSRC: *e.SSRC,
Marker: marker,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,2 @@
// Package rtph264 contains a RTP/H264 decoder and encoder.
package rtph264

View File

@ -0,0 +1,220 @@
package rtph265
import (
"errors"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented NALU and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
func auSize(au [][]byte) int {
s := 0
for _, nalu := range au {
s += len(nalu)
}
return s
}
// Decoder is a RTP/H265 decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc7798
type Decoder struct {
// indicates that NALUs have an additional field that specifies the decoding order.
MaxDONDiff int
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
fragmentNextSeqNum uint16
// for Decode()
frameBuffer [][]byte
frameBufferLen int
frameBufferSize int
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
if d.MaxDONDiff != 0 {
return fmt.Errorf("MaxDONDiff != 0 is not supported (yet)")
}
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
func (d *Decoder) decodeNALUs(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 2 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
typ := h265.NALUType((pkt.Payload[0] >> 1) & 0b111111)
var nalus [][]byte
switch typ {
case h265.NALUType_AggregationUnit:
d.resetFragments()
payload := pkt.Payload[2:]
for {
if len(payload) < 2 {
return nil, fmt.Errorf("invalid aggregation unit (invalid size)")
}
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 || int(size) > len(payload) {
return nil, fmt.Errorf("invalid aggregation unit (invalid size)")
}
nalus = append(nalus, payload[:size])
payload = payload[size:]
if len(payload) == 0 {
break
}
}
d.firstPacketReceived = true
case h265.NALUType_FragmentationUnit:
if len(pkt.Payload) < 3 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
start := pkt.Payload[2] >> 7
end := (pkt.Payload[2] >> 6) & 0x01
if start == 1 {
d.resetFragments()
if end != 0 {
return nil, 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.fragmentsSize = len(pkt.Payload[1:])
d.fragments = append(d.fragments, []byte{byte(head >> 8), byte(head)}, pkt.Payload[3:])
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
d.firstPacketReceived = true
return nil, ErrMorePacketsNeeded
}
if d.fragmentsSize == 0 {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
return nil, fmt.Errorf("invalid fragmentation unit (non-starting)")
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += len(pkt.Payload[3:])
if d.fragmentsSize > h265.MaxAccessUnitSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("NALU size (%d) is too big, maximum is %d",
errSize, h265.MaxAccessUnitSize)
}
d.fragments = append(d.fragments, pkt.Payload[3:])
d.fragmentNextSeqNum++
if end != 1 {
return nil, ErrMorePacketsNeeded
}
nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
case h265.NALUType_PACI:
d.resetFragments()
return nil, fmt.Errorf("PACI packets are not supported (yet)")
default:
d.resetFragments()
nalus = [][]byte{pkt.Payload}
}
return nalus, nil
}
// Decode decodes an access unit from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
nalus, err := d.decodeNALUs(pkt)
if err != nil {
return nil, err
}
l := len(nalus)
if (d.frameBufferLen + l) > h265.MaxNALUsPerAccessUnit {
errCount := d.frameBufferLen + l
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return nil, fmt.Errorf("NALU count (%d) exceeds maximum allowed (%d)",
errCount, h265.MaxNALUsPerAccessUnit)
}
addSize := auSize(nalus)
if (d.frameBufferSize + addSize) > h265.MaxAccessUnitSize {
errSize := d.frameBufferSize + addSize
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d",
errSize, h265.MaxAccessUnitSize)
}
d.frameBuffer = append(d.frameBuffer, nalus...)
d.frameBufferLen += l
d.frameBufferSize += addSize
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
ret := d.frameBuffer
// do not reuse frameBuffer to avoid race conditions
d.frameBuffer = nil
d.frameBufferLen = 0
d.frameBufferSize = 0
return ret, nil
}

View File

@ -0,0 +1,247 @@
package rtph265
import (
"crypto/rand"
"fmt"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a RTP/H265 encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc7798
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
// 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() error {
if e.MaxDONDiff != 0 {
return fmt.Errorf("MaxDONDiff != 0 is not supported (yet)")
}
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes an access unit into RTP/H265 packets.
func (e *Encoder) Encode(au [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
// split NALUs into batches
for _, nalu := range au {
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, 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 that the entire access unit has been sent
pkts, err := e.writeBatch(batch, true)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
return rets, nil
}
func (e *Encoder) writeBatch(nalus [][]byte, 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], marker)
}
// split the NALU into multiple fragmentation packet
return e.writeFragmentationUnits(nalus[0], marker)
}
return e.writeAggregationUnit(nalus, marker)
}
func (e *Encoder) writeSingle(nalu []byte, marker bool) ([]*rtp.Packet, error) {
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: marker,
},
Payload: nalu,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}
func (e *Encoder) writeFragmentationUnits(nalu []byte, marker bool) ([]*rtp.Packet, error) {
avail := e.PayloadMaxSize - 3
le := len(nalu) - 2
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
head := nalu[:2]
nalu = nalu[2:]
le = avail
start := uint8(1)
end := uint8(0)
for i := range ret {
if i == (packetCount - 1) {
le = len(nalu)
end = 1
}
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)
nalu = nalu[le:]
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: (i == (packetCount-1) && marker),
},
Payload: data,
}
e.sequenceNumber++
start = 0
}
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, 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,
SSRC: *e.SSRC,
Marker: marker,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,2 @@
// Package rtph265 contains a RTP/H265 decoder and encoder.
package rtph265

View File

@ -0,0 +1,33 @@
package rtplpcm
import (
"fmt"
"github.com/pion/rtp"
)
// Decoder is a RTP/LPCM decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc3190
type Decoder struct {
BitDepth int
ChannelCount int
sampleSize int
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
d.sampleSize = d.BitDepth * d.ChannelCount / 8
return nil
}
// Decode decodes audio samples from a RTP packet.
// It returns audio samples and PTS of the first sample.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
plen := len(pkt.Payload)
if (plen % d.sampleSize) != 0 {
return nil, fmt.Errorf("received payload of wrong size")
}
return pkt.Payload, nil
}

View File

@ -0,0 +1,124 @@
package rtplpcm
import (
"crypto/rand"
"fmt"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/LPCM encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc3190
type Encoder struct {
// payload type of packets.
PayloadType uint8
// bit depth.
BitDepth int
// channel count.
ChannelCount int
// 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
sampleSize int
maxPayloadSize int
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
e.sampleSize = e.BitDepth * e.ChannelCount / 8
e.maxPayloadSize = (e.PayloadMaxSize / e.sampleSize) * e.sampleSize
return nil
}
func (e *Encoder) packetCount(slen int) int {
n := (slen / e.maxPayloadSize)
if (slen % e.maxPayloadSize) != 0 {
n++
}
return n
}
// Encode encodes audio samples into RTP packets.
func (e *Encoder) Encode(samples []byte) ([]*rtp.Packet, error) {
slen := len(samples)
if (slen % e.sampleSize) != 0 {
return nil, fmt.Errorf("invalid samples")
}
packetCount := e.packetCount(slen)
ret := make([]*rtp.Packet, packetCount)
pos := 0
payloadSize := e.maxPayloadSize
timestamp := uint32(0)
for i := range ret {
if payloadSize > len(samples[pos:]) {
payloadSize = len(samples[pos:])
}
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: false,
},
Payload: samples[pos : pos+payloadSize],
}
e.sequenceNumber++
pos += payloadSize
timestamp += uint32(payloadSize / e.sampleSize)
}
return ret, nil
}

View File

@ -0,0 +1,2 @@
// Package rtplpcm contains a RTP/LPCM decoder and encoder.
package rtplpcm

View File

@ -0,0 +1,304 @@
package rtpmjpeg
import (
"errors"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// fragment of an image and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
var lumDcCodeLens = []byte{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
}
var lumDcSymbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var lumAcCodelens = []byte{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
}
var lumAcSymbols = []byte{ //nolint:dupl
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var chmDcCodelens = []byte{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
}
var chmDcSymbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var chmAcCodelens = []byte{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
}
var chmAcSymbols = []byte{ //nolint:dupl
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var lumaQuantizers = []int{
0x10, 0x0b, 0x0c, 0x0e, 0x0c, 0x0a, 0x10, 0x0e,
0x0d, 0x0e, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28,
0x1a, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25,
0x1d, 0x28, 0x3a, 0x33, 0x3d, 0x3c, 0x39, 0x33,
0x38, 0x37, 0x40, 0x48, 0x5c, 0x4e, 0x40, 0x44,
0x57, 0x45, 0x37, 0x38, 0x50, 0x6d, 0x51, 0x57,
0x5f, 0x62, 0x67, 0x68, 0x67, 0x3e, 0x4d, 0x71,
0x79, 0x70, 0x64, 0x78, 0x5c, 0x65, 0x67, 0x63,
}
var chromaQuantizers = []int{
0x11, 0x12, 0x12, 0x18, 0x15, 0x18, 0x2f, 0x1a,
0x1a, 0x2f, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
}
func makeQuantizationTables(q uint8) [][]byte {
var scale int
if q < 50 {
scale = 5000 / int(q)
} else {
scale = 200 - 2*int(q)
}
tables := make([][]byte, 2)
tables[0] = make([]byte, 64)
for i := 0; i < 64; i++ {
v := (lumaQuantizers[i]*scale + 50) / 100
if v > 255 {
v = 255
} else if v == 0 {
v = 1
}
tables[0][i] = byte(v)
}
tables[1] = make([]byte, 64)
for i := 0; i < 64; i++ {
v := (chromaQuantizers[i]*scale + 50) / 100
if v > 255 {
v = 255
} else if v == 0 {
v = 1
}
tables[1][i] = byte(v)
}
return tables
}
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/M-JPEG decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
type Decoder struct {
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
firstJpegHeader *headerJPEG
quantizationTables [][]byte
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes an image from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
byts := pkt.Payload
var jh headerJPEG
n, err := jh.unmarshal(byts)
if err != nil {
return nil, err
}
byts = byts[n:]
if jh.Width > maxDimension {
return nil, fmt.Errorf("width of %d is not supported", jh.Width)
}
if jh.Height > maxDimension {
return nil, fmt.Errorf("height of %d is not supported", jh.Height)
}
if jh.FragmentOffset == 0 {
d.resetFragments()
d.firstPacketReceived = true
if jh.Quantization >= 128 {
var hqt headerQuantizationTable
n, err := hqt.unmarshal(byts)
if err != nil {
return nil, err
}
d.quantizationTables = hqt.Tables
byts = byts[n:]
} else {
d.quantizationTables = makeQuantizationTables(jh.Quantization)
}
d.fragments = append(d.fragments, byts)
d.fragmentsSize = len(byts)
d.firstJpegHeader = &jh
} else {
if int(jh.FragmentOffset) != d.fragmentsSize {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
d.resetFragments()
return nil, fmt.Errorf("received wrong fragment")
}
d.fragmentsSize += len(byts)
d.fragments = append(d.fragments, byts)
}
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
if d.fragmentsSize < 2 {
return nil, fmt.Errorf("invalid data")
}
data := joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
var buf []byte
buf = jpeg.StartOfImage{}.Marshal(buf)
var dqt jpeg.DefineQuantizationTable
id := uint8(0)
for _, table := range d.quantizationTables {
dqt.Tables = append(dqt.Tables, jpeg.QuantizationTable{
ID: id,
Data: table,
})
id++
}
buf = dqt.Marshal(buf)
buf = jpeg.StartOfFrame1{
Type: d.firstJpegHeader.Type,
Width: d.firstJpegHeader.Width,
Height: d.firstJpegHeader.Height,
QuantizationTableCount: id,
}.Marshal(buf)
buf = jpeg.DefineHuffmanTable{
Codes: lumDcCodeLens,
Symbols: lumDcSymbols,
TableNumber: 0,
TableClass: 0,
}.Marshal(buf)
buf = jpeg.DefineHuffmanTable{
Codes: lumAcCodelens,
Symbols: lumAcSymbols,
TableNumber: 0,
TableClass: 1,
}.Marshal(buf)
buf = jpeg.DefineHuffmanTable{
Codes: chmDcCodelens,
Symbols: chmDcSymbols,
TableNumber: 1,
TableClass: 0,
}.Marshal(buf)
buf = jpeg.DefineHuffmanTable{
Codes: chmAcCodelens,
Symbols: chmAcSymbols,
TableNumber: 1,
TableClass: 1,
}.Marshal(buf)
buf = jpeg.StartOfScan{}.Marshal(buf)
buf = append(buf, data...)
if data[len(data)-2] != 0xFF || data[len(data)-1] != jpeg.MarkerEndOfImage {
buf = append(buf, []byte{0xFF, jpeg.MarkerEndOfImage}...)
}
return buf, nil
}

View File

@ -0,0 +1,279 @@
package rtpmjpeg
import (
"crypto/rand"
"fmt"
"sort"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/M-JPEG encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
type Encoder struct {
// 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes an image into RTP/M-JPEG packets.
func (e *Encoder) Encode(image []byte) ([]*rtp.Packet, error) {
l := len(image)
if l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage {
return nil, fmt.Errorf("SOI not found")
}
image = image[2:]
var sof *jpeg.StartOfFrame1
var dri *jpeg.DefineRestartInterval
quantizationTables := make(map[uint8][]byte)
var data []byte
outer:
for {
if len(image) < 2 {
break
}
h0, h1 := image[0], image[1]
image = image[2:]
if h0 != 0xFF {
return nil, fmt.Errorf("invalid image")
}
switch h1 {
case 0xE0, 0xE1, 0xE2, // JFIF
jpeg.MarkerDefineHuffmanTable,
jpeg.MarkerComment:
mlen := int(image[0])<<8 | int(image[1])
if len(image) < mlen {
return nil, fmt.Errorf("image is too short")
}
image = image[mlen:]
case jpeg.MarkerDefineQuantizationTable:
mlen := int(image[0])<<8 | int(image[1])
if len(image) < mlen {
return nil, fmt.Errorf("image is too short")
}
var dqt jpeg.DefineQuantizationTable
err := dqt.Unmarshal(image[2:mlen])
if err != nil {
return nil, err
}
image = image[mlen:]
for _, t := range dqt.Tables {
quantizationTables[t.ID] = t.Data
}
case jpeg.MarkerDefineRestartInterval:
mlen := int(image[0])<<8 | int(image[1])
if len(image) < mlen {
return nil, fmt.Errorf("image is too short")
}
dri = &jpeg.DefineRestartInterval{}
err := dri.Unmarshal(image[2:mlen])
if err != nil {
return nil, err
}
image = image[mlen:]
case jpeg.MarkerStartOfFrame1:
mlen := int(image[0])<<8 | int(image[1])
if len(image) < mlen {
return nil, fmt.Errorf("image is too short")
}
sof = &jpeg.StartOfFrame1{}
err := sof.Unmarshal(image[2:mlen])
if err != nil {
return nil, err
}
image = image[mlen:]
if sof.Width > maxDimension {
return nil, fmt.Errorf("an image with width of %d can't be sent with RTSP", sof.Width)
}
if sof.Height > maxDimension {
return nil, fmt.Errorf("an image with height of %d can't be sent with RTSP", sof.Height)
}
if (sof.Width % 8) != 0 {
return nil, fmt.Errorf("width must be multiple of 8")
}
if (sof.Height % 8) != 0 {
return nil, fmt.Errorf("height must be multiple of 8")
}
case jpeg.MarkerStartOfScan:
mlen := int(image[0])<<8 | int(image[1])
if len(image) < mlen {
return nil, fmt.Errorf("image is too short")
}
var sos jpeg.StartOfScan
err := sos.Unmarshal(image[2:mlen])
if err != nil {
return nil, err
}
image = image[mlen:]
data = image
break outer
default:
return nil, fmt.Errorf("unknown marker: 0x%.2x", h1)
}
}
if sof == nil {
return nil, fmt.Errorf("SOF not found")
}
if sof.Type > 63 {
return nil, fmt.Errorf("JPEG type %d is not supported", sof.Type)
}
if len(data) == 0 {
return nil, fmt.Errorf("image data not found")
}
jh := headerJPEG{
TypeSpecific: 0,
Type: sof.Type,
Quantization: 255,
Width: sof.Width,
Height: sof.Height,
}
if dri != nil {
jh.Type += 64
}
first := true
offset := 0
var ret []*rtp.Packet
for {
var buf []byte
jh.FragmentOffset = uint32(offset)
buf = jh.marshal(buf)
if dri != nil {
buf = headerRestartMarker{
Interval: dri.Interval,
Count: 0xFFFF,
}.marshal(buf)
}
if first {
first = false
qth := headerQuantizationTable{}
// gather and sort tables IDs
ids := make([]uint8, len(quantizationTables))
i := 0
for id := range quantizationTables {
ids[i] = id
i++
}
sort.Slice(ids, func(i, j int) bool {
return ids[i] < ids[j]
})
// add tables sorted by ID
for _, id := range ids {
qth.Tables = append(qth.Tables, quantizationTables[id])
}
buf = qth.marshal(buf)
}
remaining := e.PayloadMaxSize - len(buf)
ldata := len(data)
if remaining > ldata {
remaining = ldata
}
buf = append(buf, data[:remaining]...)
data = data[remaining:]
offset += remaining
ret = append(ret, &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: 26,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: len(data) == 0,
},
Payload: buf,
})
e.sequenceNumber++
if len(data) == 0 {
break
}
}
return ret, nil
}

View File

@ -0,0 +1,49 @@
package rtpmjpeg
import (
"fmt"
)
type headerJPEG struct {
TypeSpecific uint8
FragmentOffset uint32
Type uint8
Quantization uint8
Width int
Height int
}
func (h *headerJPEG) unmarshal(byts []byte) (int, error) {
if len(byts) < 8 {
return 0, fmt.Errorf("buffer is too short")
}
h.TypeSpecific = byts[0]
h.FragmentOffset = uint32(byts[1])<<16 | uint32(byts[2])<<8 | uint32(byts[3])
h.Type = byts[4]
if h.Type > 63 {
return 0, fmt.Errorf("type %d is not supported", h.Type)
}
h.Quantization = byts[5]
if h.Quantization == 0 ||
(h.Quantization > 99 && h.Quantization < 127) {
return 0, fmt.Errorf("quantization %d is invalid", h.Quantization)
}
h.Width = int(byts[6]) * 8
h.Height = int(byts[7]) * 8
return 8, nil
}
func (h headerJPEG) marshal(byts []byte) []byte {
byts = append(byts, h.TypeSpecific)
byts = append(byts, []byte{byte(h.FragmentOffset >> 16), byte(h.FragmentOffset >> 8), byte(h.FragmentOffset)}...)
byts = append(byts, h.Type)
byts = append(byts, h.Quantization)
byts = append(byts, byte(h.Width/8))
byts = append(byts, byte(h.Height/8))
return byts
}

View File

@ -0,0 +1,59 @@
package rtpmjpeg
import (
"fmt"
)
type headerQuantizationTable struct {
MBZ uint8
Precision uint8
Tables [][]byte
}
func (h *headerQuantizationTable) unmarshal(byts []byte) (int, error) {
if len(byts) < 4 {
return 0, fmt.Errorf("buffer is too short")
}
h.MBZ = byts[0]
h.Precision = byts[1]
if h.Precision != 0 {
return 0, fmt.Errorf("precision %d is not supported", h.Precision)
}
length := int(byts[2])<<8 | int(byts[3])
switch length {
case 64, 128:
default:
return 0, fmt.Errorf("table length %d is not supported", length)
}
if (len(byts) - 4) < length {
return 0, fmt.Errorf("buffer is too short")
}
tableCount := length / 64
h.Tables = make([][]byte, tableCount)
n := 0
for i := 0; i < tableCount; i++ {
h.Tables[i] = byts[4+n : 4+64+n]
n += 64
}
return 4 + length, nil
}
func (h headerQuantizationTable) marshal(byts []byte) []byte {
byts = append(byts, h.MBZ)
byts = append(byts, h.Precision)
l := len(h.Tables) * 64
byts = append(byts, []byte{byte(l >> 8), byte(l)}...)
for i := 0; i < len(h.Tables); i++ {
byts = append(byts, h.Tables[i]...)
}
return byts
}

View File

@ -0,0 +1,26 @@
package rtpmjpeg
import (
"fmt"
)
type headerRestartMarker struct {
Interval uint16
Count uint16
}
func (h *headerRestartMarker) unmarshal(byts []byte) (int, error) {
if len(byts) < 4 {
return 0, fmt.Errorf("buffer is too short")
}
h.Interval = uint16(byts[0])<<8 | uint16(byts[1])
h.Count = uint16(byts[2])<<8 | uint16(byts[3])
return 4, nil
}
func (h headerRestartMarker) marshal(byts []byte) []byte {
byts = append(byts, []byte{byte(h.Interval >> 8), byte(h.Interval)}...)
byts = append(byts, []byte{byte(h.Count >> 8), byte(h.Count)}...)
return byts
}

View File

@ -0,0 +1,6 @@
// Package rtpmjpeg contains a RTP/M-JPEG decoder and encoder.
package rtpmjpeg
const (
maxDimension = 2040
)

View File

@ -0,0 +1,127 @@
package rtpmpeg1audio
import (
"errors"
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
"github.com/pion/rtp"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented frame and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/MPEG-1/2 Audio decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type Decoder struct {
firstPacketReceived bool
fragments [][]byte
fragmentsSize int
fragmentsExpected int
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes frames from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 5 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
mbz := uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])
if mbz != 0 {
d.resetFragments()
return nil, fmt.Errorf("invalid MBZ: %v", mbz)
}
offset := uint16(pkt.Payload[2])<<8 | uint16(pkt.Payload[3])
var frames [][]byte
if offset == 0 {
d.resetFragments()
d.firstPacketReceived = true
buf := pkt.Payload[4:]
for {
var h mpeg1audio.FrameHeader
err := h.Unmarshal(buf)
if err != nil {
return nil, err
}
fl := h.FrameLen()
bl := len(buf)
if bl >= fl {
frames = append(frames, buf[:fl])
buf = buf[fl:]
if len(buf) == 0 {
break
}
} else {
if len(frames) != 0 {
return nil, fmt.Errorf("invalid packet")
}
d.fragments = append(d.fragments, buf)
d.fragmentsSize = bl
d.fragmentsExpected = fl - bl
return nil, ErrMorePacketsNeeded
}
}
} else {
if int(offset) != d.fragmentsSize {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
d.resetFragments()
return nil, fmt.Errorf("unexpected offset %v, expected %v", offset, d.fragmentsSize)
}
bl := len(pkt.Payload[4:])
d.fragmentsSize += bl
d.fragmentsExpected -= bl
if d.fragmentsExpected < 0 {
d.resetFragments()
return nil, fmt.Errorf("fragment is too big")
}
d.fragments = append(d.fragments, pkt.Payload[4:])
if d.fragmentsExpected > 0 {
return nil, ErrMorePacketsNeeded
}
frames = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
}
return frames, nil
}

View File

@ -0,0 +1,196 @@
package rtpmpeg1audio
import (
"crypto/rand"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func lenAggregated(frames [][]byte, frame []byte) int {
n := 4 + len(frame)
for _, fr := range frames {
n += len(fr)
}
return n
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a RTP/MPEG-1/2 Audio encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type Encoder struct {
// 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes frames into RTP packets.
func (e *Encoder) Encode(frames [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
timestamp := uint32(0)
for _, frame := range frames {
if lenAggregated(batch, frame) <= e.PayloadMaxSize {
batch = append(batch, frame)
} else {
// write current batch
if batch != nil {
pkts, err := e.writeBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
for _, frame := range batch {
var h mpeg1audio.FrameHeader
err := h.Unmarshal(frame)
if err != nil {
return nil, err
}
timestamp += uint32(h.SampleCount())
}
}
// initialize new batch
batch = [][]byte{frame}
}
}
// write last batch
pkts, err := e.writeBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
return rets, nil
}
func (e *Encoder) writeBatch(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
if len(frames) != 1 || lenAggregated(frames, nil) < e.PayloadMaxSize {
return e.writeAggregated(frames, timestamp)
}
return e.writeFragmented(frames[0], timestamp)
}
func (e *Encoder) writeFragmented(frame []byte, timestamp uint32) ([]*rtp.Packet, error) {
avail := e.PayloadMaxSize - 4
le := len(frame)
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
pos := 0
le = avail
for i := range ret {
if i == (packetCount - 1) {
le = len(frame) - pos
}
payload := make([]byte, 4+le)
payload[2] = byte(pos >> 8)
payload[3] = byte(pos)
pos += copy(payload[4:], frame[pos:])
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: 14,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: true,
},
Payload: payload,
}
e.sequenceNumber++
}
return ret, nil
}
func (e *Encoder) writeAggregated(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
payload := make([]byte, lenAggregated(frames, nil))
n := 4
for _, frame := range frames {
n += copy(payload[n:], frame)
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: 14,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: true,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpmpeg1audio contains a RTP/MPEG-1/2 Audio decoder and encoder.
package rtpmpeg1audio

View File

@ -0,0 +1,163 @@
package rtpmpeg1video
import (
"errors"
"fmt"
"github.com/pion/rtp"
)
const (
maxFrameSize = 1 * 1024 * 1024
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented frame and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/MPEG-1/2 Video decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type Decoder struct {
fragments [][]byte
fragmentsSize int
fragmentNextSeqNum uint16
sliceBuffer [][]byte
sliceBufferSize int
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
func (d *Decoder) decodeSlice(pkt *rtp.Packet) ([]byte, error) {
if len(pkt.Payload) < 4 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
mbz := pkt.Payload[0] >> 3
if mbz != 0 {
d.resetFragments()
return nil, fmt.Errorf("invalid MBZ: %v", mbz)
}
t := (pkt.Payload[0] >> 2) & 0x01
if t != 0 {
d.resetFragments()
return nil, fmt.Errorf("MPEG-2 video-specific header extension is not supported yet")
}
an := pkt.Payload[2] >> 7
if an != 0 {
d.resetFragments()
return nil, fmt.Errorf("AN not supported yet")
}
n := (pkt.Payload[2] >> 6) & 0x01
if n != 0 {
d.resetFragments()
return nil, fmt.Errorf("N not supported yet")
}
b := (pkt.Payload[2] >> 4) & 0x01
e := (pkt.Payload[2] >> 3) & 0x01
switch {
case b == 1 && e == 1:
return pkt.Payload[4:], nil
case b == 1:
d.fragments = d.fragments[:0]
d.fragments = append(d.fragments, pkt.Payload[4:])
d.fragmentsSize = len(pkt.Payload[4:])
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
return nil, ErrMorePacketsNeeded
case e == 1:
if d.fragmentsSize == 0 {
return nil, ErrNonStartingPacketAndNoPrevious
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragments = append(d.fragments, pkt.Payload[4:])
d.fragmentsSize += len(pkt.Payload[4:])
slice := joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
return slice, nil
default:
if d.fragmentsSize == 0 {
return nil, ErrNonStartingPacketAndNoPrevious
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragments = append(d.fragments, pkt.Payload[4:])
d.fragmentsSize += len(pkt.Payload[4:])
d.fragmentNextSeqNum++
return nil, ErrMorePacketsNeeded
}
}
// Decode decodes frames from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
slice, err := d.decodeSlice(pkt)
if err != nil {
return nil, err
}
addSize := len(slice)
if (d.sliceBufferSize + addSize) > maxFrameSize {
errSize := d.sliceBufferSize + addSize
d.sliceBuffer = nil
d.sliceBufferSize = 0
return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d",
errSize, maxFrameSize)
}
d.sliceBuffer = append(d.sliceBuffer, slice)
d.sliceBufferSize += addSize
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
ret := joinFragments(d.sliceBuffer, d.sliceBufferSize)
// do not reuse sliceBuffer to avoid race conditions
d.sliceBuffer = nil
d.sliceBufferSize = 0
return ret, nil
}

View File

@ -0,0 +1,239 @@
package rtpmpeg1video
import (
"bytes"
"crypto/rand"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func lenAggregated(slices [][]byte, slice []byte) int {
n := 4 + len(slice)
for _, fr := range slices {
n += len(fr)
}
return n
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a RTP/MPEG-1/2 Video encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
type Encoder struct {
// 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes frames into RTP packets.
func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
var temporalReference uint16
beginOfSequence := uint8(0)
var frameType uint8
for {
var slice []byte
end := bytes.Index(frame[4:], []byte{0, 0, 1})
if end >= 0 {
slice, frame = frame[:end+4], frame[end+4:]
} else {
slice, frame = frame, nil
}
if lenAggregated(batch, slice) <= e.PayloadMaxSize {
batch = append(batch, slice)
} else {
// write current batch
if batch != nil {
pkts, err := e.writeBatch(batch,
temporalReference,
beginOfSequence,
frameType)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
beginOfSequence = 0
}
// initialize new batch
batch = [][]byte{slice}
}
switch slice[3] {
case 0:
temporalReference = uint16(slice[4])<<2 | uint16(slice[5])>>6
frameType = (slice[5] >> 3) & 0b111
case 0xB8:
beginOfSequence = 1
}
if frame == nil {
break
}
}
// write last batch
pkts, err := e.writeBatch(batch,
temporalReference,
beginOfSequence,
frameType)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
rets[len(rets)-1].Marker = true
return rets, nil
}
func (e *Encoder) writeBatch(
slices [][]byte,
temporalReference uint16,
beginOfSequence uint8,
frameType uint8,
) ([]*rtp.Packet, error) {
if len(slices) != 1 || lenAggregated(slices, nil) < e.PayloadMaxSize {
return e.writeAggregated(slices, temporalReference, beginOfSequence, frameType)
}
return e.writeFragmented(slices[0], temporalReference, beginOfSequence, frameType)
}
func (e *Encoder) writeFragmented(
slice []byte,
temporalReference uint16,
beginOfSequence uint8,
frameType uint8,
) ([]*rtp.Packet, error) {
avail := e.PayloadMaxSize - 4
le := len(slice)
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
le = avail
start := uint8(1)
end := uint8(0)
for i := range ret {
if i == (packetCount - 1) {
le = len(slice)
end = 1
}
payload := make([]byte, 4+le)
payload[0] = byte(temporalReference >> 8)
payload[1] = byte(temporalReference)
payload[2] = beginOfSequence<<5 | start<<4 | end<<3 | frameType
copy(payload[4:], slice)
slice = slice[le:]
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: 32,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
},
Payload: payload,
}
e.sequenceNumber++
start = 0
beginOfSequence = 0
}
return ret, nil
}
func (e *Encoder) writeAggregated(
slices [][]byte,
temporalReference uint16,
beginOfSequence uint8,
frameType uint8,
) ([]*rtp.Packet, error) {
payload := make([]byte, lenAggregated(slices, nil))
payload[0] = byte(temporalReference >> 8)
payload[1] = byte(temporalReference)
payload[2] = beginOfSequence<<5 | 1<<4 | 1<<3 | frameType
n := 4
for _, slice := range slices {
n += copy(payload[n:], slice)
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: 32,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpmpeg1video contains a RTP/MPEG-1/2 Video decoder and encoder.
package rtpmpeg1video

View File

@ -0,0 +1,62 @@
package rtpmpeg4audio
import (
"errors"
"github.com/pion/rtp"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/MPEG-4 Audio decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3
type Decoder struct {
// use RFC6416 (LATM) instead of RFC3640 (generic).
LATM bool
// Generic-only
// The number of bits in which the AU-size field is encoded in the AU-header.
SizeLength int
// The number of bits in which the AU-Index is encoded in the first AU-header.
IndexLength int
// The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header.
IndexDeltaLength int
firstAUParsed bool
adtsMode bool
fragments [][]byte
fragmentsSize int
fragmentsExpected int
fragmentNextSeqNum uint16
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes AUs from a RTP packet.
// It returns the AUs and the PTS of the first AU.
// The PTS of subsequent AUs can be calculated by adding time.Second*mpeg4audio.SamplesPerAccessUnit/clockRate.
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
if !d.LATM {
return d.decodeGeneric(pkt)
}
return d.decodeLATM(pkt)
}

View File

@ -0,0 +1,201 @@
package rtpmpeg4audio
import (
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/bits"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/pion/rtp"
)
func (d *Decoder) decodeGeneric(pkt *rtp.Packet) ([][]byte, error) {
if len(pkt.Payload) < 2 {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
// AU-headers-length (16 bits)
headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1]))
if headersLen == 0 {
d.resetFragments()
return nil, fmt.Errorf("invalid AU-headers-length")
}
payload := pkt.Payload[2:]
// AU-headers
dataLens, err := d.readAUHeaders(payload, headersLen)
if err != nil {
d.resetFragments()
return nil, err
}
pos := (headersLen / 8)
if (headersLen % 8) != 0 {
pos++
}
payload = payload[pos:]
var aus [][]byte
if d.fragmentsSize == 0 {
d.resetFragments()
if pkt.Marker {
// AUs
aus = make([][]byte, len(dataLens))
for i, dataLen := range dataLens {
if len(payload) < int(dataLen) {
return nil, fmt.Errorf("payload is too short")
}
aus[i] = payload[:dataLen]
payload = payload[dataLen:]
}
} else {
if len(dataLens) != 1 {
return nil, fmt.Errorf("a fragmented packet can only contain one AU")
}
if len(payload) < int(dataLens[0]) {
return nil, fmt.Errorf("payload is too short")
}
d.fragmentsSize = int(dataLens[0])
d.fragments = append(d.fragments, payload[:dataLens[0]])
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
return nil, ErrMorePacketsNeeded
}
} else {
// we are decoding a fragmented AU
if len(dataLens) != 1 {
d.resetFragments()
return nil, fmt.Errorf("a fragmented packet can only contain one AU")
}
if len(payload) < int(dataLens[0]) {
d.resetFragments()
return nil, fmt.Errorf("payload is too short")
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += int(dataLens[0])
if d.fragmentsSize > mpeg4audio.MaxAccessUnitSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d",
errSize, mpeg4audio.MaxAccessUnitSize)
}
d.fragments = append(d.fragments, payload[:dataLens[0]])
d.fragmentNextSeqNum++
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
aus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
d.resetFragments()
}
return d.removeADTS(aus)
}
func (d *Decoder) readAUHeaders(buf []byte, headersLen int) ([]uint64, error) {
firstRead := false
count := 0
for i := 0; i < headersLen; {
if i == 0 {
i += d.SizeLength
i += d.IndexLength
} else {
i += d.SizeLength
i += d.IndexDeltaLength
}
count++
}
dataLens := make([]uint64, count)
pos := 0
i := 0
for headersLen > 0 {
dataLen, err := bits.ReadBits(buf, &pos, d.SizeLength)
if err != nil {
return nil, err
}
headersLen -= d.SizeLength
if !firstRead {
firstRead = true
if d.IndexLength > 0 {
auIndex, err := bits.ReadBits(buf, &pos, d.IndexLength)
if err != nil {
return nil, err
}
headersLen -= d.IndexLength
if auIndex != 0 {
return nil, fmt.Errorf("AU-index different than zero is not supported")
}
}
} else if d.IndexDeltaLength > 0 {
auIndexDelta, err := bits.ReadBits(buf, &pos, d.IndexDeltaLength)
if err != nil {
return nil, err
}
headersLen -= d.IndexDeltaLength
if auIndexDelta != 0 {
return nil, fmt.Errorf("AU-index-delta different than zero is not supported")
}
}
dataLens[i] = dataLen
i++
}
return dataLens, nil
}
// some cameras wrap AUs into ADTS
func (d *Decoder) removeADTS(aus [][]byte) ([][]byte, error) {
if !d.firstAUParsed {
d.firstAUParsed = true
if len(aus) == 1 && len(aus[0]) >= 2 {
if aus[0][0] == 0xFF && (aus[0][1]&0xF0) == 0xF0 {
var pkts mpeg4audio.ADTSPackets
err := pkts.Unmarshal(aus[0])
if err == nil && len(pkts) == 1 {
d.adtsMode = true
aus[0] = pkts[0].AU
}
}
}
} else if d.adtsMode {
if len(aus) != 1 {
return nil, fmt.Errorf("multiple AUs in ADTS mode are not supported")
}
var pkts mpeg4audio.ADTSPackets
err := pkts.Unmarshal(aus[0])
if err != nil {
return nil, fmt.Errorf("unable to decode ADTS: %w", err)
}
if len(pkts) != 1 {
return nil, fmt.Errorf("multiple ADTS packets are not supported")
}
aus[0] = pkts[0].AU
}
return aus, nil
}

View File

@ -0,0 +1,63 @@
package rtpmpeg4audio
import (
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/pion/rtp"
)
func (d *Decoder) decodeLATM(pkt *rtp.Packet) ([][]byte, error) {
var au []byte
buf := pkt.Payload
if d.fragmentsSize == 0 {
pl, n, err := payloadLengthInfoDecode(buf)
if err != nil {
return nil, err
}
buf = buf[n:]
bl := len(buf)
if pl <= bl {
au = buf[:pl]
// there could be other data, due to otherDataPresent. Ignore it.
} else {
if pl > mpeg4audio.MaxAccessUnitSize {
errSize := pl
d.resetFragments()
return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d",
errSize, mpeg4audio.MaxAccessUnitSize)
}
d.fragments = append(d.fragments, buf)
d.fragmentsSize = pl
d.fragmentsExpected = pl - bl
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
return nil, ErrMorePacketsNeeded
}
} else {
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
bl := len(buf)
if d.fragmentsExpected > bl {
d.fragments = append(d.fragments, buf)
d.fragmentsExpected -= bl
d.fragmentNextSeqNum++
return nil, ErrMorePacketsNeeded
}
d.fragments = append(d.fragments, buf[:d.fragmentsExpected])
// there could be other data, due to otherDataPresent. Ignore it.
au = joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
}
return [][]byte{au}, nil
}

View File

@ -0,0 +1,88 @@
package rtpmpeg4audio
import (
"crypto/rand"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/MPEG-4 audio encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3
type Encoder struct {
// payload type of packets.
PayloadType uint8
// use RFC6416 (LATM) instead of RFC3640 (generic).
LATM bool
// The number of bits in which the AU-size field is encoded in the AU-header.
SizeLength int
// The number of bits in which the AU-Index is encoded in the first AU-header.
IndexLength int
// The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header.
IndexDeltaLength int
// 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes AUs into RTP packets.
func (e *Encoder) Encode(aus [][]byte) ([]*rtp.Packet, error) {
if !e.LATM {
return e.encodeGeneric(aus)
}
return e.encodeLATM(aus)
}

View File

@ -0,0 +1,196 @@
package rtpmpeg4audio
import (
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/bits"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
)
func packetCountGeneric(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
func (e *Encoder) encodeGeneric(aus [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
var batch [][]byte
timestamp := uint32(0)
// split AUs into batches
for _, au := range aus {
if e.lenGenericAggregated(batch, au) <= e.PayloadMaxSize {
// add to existing batch
batch = append(batch, au)
} else {
// write current batch
if batch != nil {
pkts, err := e.writeGenericBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
timestamp += uint32(len(batch)) * mpeg4audio.SamplesPerAccessUnit
}
// initialize new batch
batch = [][]byte{au}
}
}
// write last batch
pkts, err := e.writeGenericBatch(batch, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, pkts...)
return rets, nil
}
func (e *Encoder) writeGenericBatch(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
if len(aus) != 1 || e.lenGenericAggregated(aus, nil) < e.PayloadMaxSize {
return e.writeGenericAggregated(aus, timestamp)
}
return e.writeGenericFragmented(aus[0], timestamp)
}
func (e *Encoder) writeGenericFragmented(au []byte, timestamp uint32) ([]*rtp.Packet, error) {
auHeadersLen := e.SizeLength + e.IndexLength
auHeadersLenBytes := auHeadersLen / 8
if (auHeadersLen % 8) != 0 {
auHeadersLenBytes++
}
avail := e.PayloadMaxSize - 2 - auHeadersLenBytes
le := len(au)
packetCount := packetCountGeneric(avail, le)
ret := make([]*rtp.Packet, packetCount)
le = avail
for i := range ret {
if i == (packetCount - 1) {
le = len(au)
}
payload := make([]byte, 2+auHeadersLenBytes+le)
// AU-headers-length
payload[0] = byte(auHeadersLen >> 8)
payload[1] = byte(auHeadersLen)
// AU-headers
pos := 0
bits.WriteBitsUnsafe(payload[2:], &pos, uint64(le), e.SizeLength)
bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength)
// AU
copy(payload[2+auHeadersLenBytes:], au)
au = au[le:]
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: (i == packetCount-1),
},
Payload: payload,
}
e.sequenceNumber++
}
return ret, nil
}
func (e *Encoder) lenGenericAggregated(aus [][]byte, addAU []byte) int {
n := 2 // AU-headers-length
// AU-headers
auHeadersLen := 0
i := 0
for range aus {
if i == 0 {
auHeadersLen += e.SizeLength + e.IndexLength
} else {
auHeadersLen += e.SizeLength + e.IndexDeltaLength
}
i++
}
if addAU != nil {
if i == 0 {
auHeadersLen += e.SizeLength + e.IndexLength
} else {
auHeadersLen += e.SizeLength + e.IndexDeltaLength
}
}
n += auHeadersLen / 8
if (auHeadersLen % 8) != 0 {
n++
}
// AU
for _, au := range aus {
n += len(au)
}
n += len(addAU)
return n
}
func (e *Encoder) writeGenericAggregated(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) {
payload := make([]byte, e.lenGenericAggregated(aus, nil))
// AU-headers
written := 0
pos := 0
for i, au := range aus {
bits.WriteBitsUnsafe(payload[2:], &pos, uint64(len(au)), e.SizeLength)
written += e.SizeLength
if i == 0 {
bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength)
written += e.IndexLength
} else {
bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexDeltaLength)
written += e.IndexDeltaLength
}
}
pos = 2 + (written / 8)
if (written % 8) != 0 {
pos++
}
// AU-headers-length
payload[0] = byte(written >> 8)
payload[1] = byte(written)
// AUs
for _, au := range aus {
auLen := copy(payload[pos:], au)
pos += auLen
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: true,
},
Payload: payload,
}
e.sequenceNumber++
return []*rtp.Packet{pkt}, nil
}

View File

@ -0,0 +1,76 @@
package rtpmpeg4audio
import (
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
)
func (e *Encoder) packetCountLATM(auLen int, plil int) int {
totalLen := plil + auLen
n := totalLen / e.PayloadMaxSize
if (totalLen % e.PayloadMaxSize) != 0 {
n++
}
return n
}
func (e *Encoder) encodeLATM(aus [][]byte) ([]*rtp.Packet, error) {
var rets []*rtp.Packet
for i, au := range aus {
timestamp := uint32(i) * mpeg4audio.SamplesPerAccessUnit
add, err := e.encodeLATMSingle(au, timestamp)
if err != nil {
return nil, err
}
rets = append(rets, add...)
}
return rets, nil
}
func (e *Encoder) encodeLATMSingle(au []byte, timestamp uint32) ([]*rtp.Packet, error) {
auLen := len(au)
plil := payloadLengthInfoEncodeSize(auLen)
packetCount := e.packetCountLATM(auLen, plil)
ret := make([]*rtp.Packet, packetCount)
le := e.PayloadMaxSize - plil
for i := range ret {
if i == (packetCount - 1) {
le = len(au)
}
var payload []byte
if i == 0 {
payload = make([]byte, plil+le)
payloadLengthInfoEncode(plil, auLen, payload)
copy(payload[plil:], au[:le])
au = au[le:]
le = e.PayloadMaxSize
} else {
payload = au[:le]
au = au[le:]
}
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
Timestamp: timestamp,
SSRC: *e.SSRC,
Marker: (i == packetCount-1),
},
Payload: payload,
}
e.sequenceNumber++
}
return ret, nil
}

View File

@ -0,0 +1,38 @@
package rtpmpeg4audio
import (
"fmt"
)
func payloadLengthInfoDecode(buf []byte) (int, int, error) {
lb := len(buf)
l := 0
n := 0
for {
if (lb - n) == 0 {
return 0, 0, fmt.Errorf("not enough bytes")
}
b := buf[n]
n++
l += int(b)
if b != 255 {
break
}
}
return l, n, nil
}
func payloadLengthInfoEncodeSize(auLen int) int {
return auLen/255 + 1
}
func payloadLengthInfoEncode(plil int, auLen int, buf []byte) {
for i := 0; i < (plil - 1); i++ {
buf[i] = 255
}
buf[plil-1] = byte(auLen % 255)
}

View File

@ -0,0 +1,2 @@
// Package rtpmpeg4audio contains a RTP/MPEG-4 Audio decoder and encoder.
package rtpmpeg4audio

View File

@ -0,0 +1,82 @@
package rtpmpeg4video
import (
"errors"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/MPEG-4 Video decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc6416
type Decoder struct {
fragments [][]byte
fragmentsSize int
fragmentNextSeqNum uint16
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes a frame from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
var frame []byte
if d.fragmentsSize == 0 {
if pkt.Marker {
frame = pkt.Payload
} else {
d.fragmentsSize = len(pkt.Payload)
d.fragments = append(d.fragments, pkt.Payload)
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
return nil, ErrMorePacketsNeeded
}
} else {
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += len(pkt.Payload)
if d.fragmentsSize > mpeg4video.MaxFrameSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d",
errSize, mpeg4video.MaxFrameSize)
}
d.fragments = append(d.fragments, pkt.Payload)
d.fragmentNextSeqNum++
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
frame = joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
}
return frame, nil
}

View File

@ -0,0 +1,108 @@
package rtpmpeg4video
import (
"crypto/rand"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func packetCount(avail, le int) int {
n := le / avail
if (le % avail) != 0 {
n++
}
return n
}
// Encoder is a RTP/MPEG-4 Video encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc6416
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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes a frame into RTP packets.
func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) {
avail := e.PayloadMaxSize
le := len(frame)
packetCount := packetCount(avail, le)
ret := make([]*rtp.Packet, packetCount)
pos := 0
le = avail
for i := range ret {
if i == (packetCount - 1) {
le = len(frame[pos:])
}
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: (i == packetCount-1),
},
Payload: frame[pos : pos+le],
}
pos += le
e.sequenceNumber++
}
return ret, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpmpeg4video contains a RTP/MPEG-4 Video decoder and encoder.
package rtpmpeg4video

View File

@ -0,0 +1,18 @@
package rtpsimpleaudio
import (
"github.com/pion/rtp"
)
// Decoder is a RTP/simple audio decoder.
type Decoder struct{}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
// Decode decodes an audio frame from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
return pkt.Payload, nil
}

View File

@ -0,0 +1,89 @@
package rtpsimpleaudio
import (
"crypto/rand"
"fmt"
"github.com/pion/rtp"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/simple audio 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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes an audio frame into a RTP packet.
func (e *Encoder) Encode(frame []byte) (*rtp.Packet, error) {
if len(frame) > e.PayloadMaxSize {
return nil, fmt.Errorf("frame is too big")
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: false,
},
Payload: frame,
}
e.sequenceNumber++
return pkt, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpsimpleaudio contains a RTP decoder and encoder for audio codecs that fit in a single packet.
package rtpsimpleaudio

View File

@ -0,0 +1,113 @@
package rtpvp8
import (
"errors"
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
// ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets")
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
// packet of a fragmented frame and we didn't received anything before.
// It's normal to receive this when decoding a stream that has been already
// running for some time.
var ErrNonStartingPacketAndNoPrevious = errors.New(
"received a non-starting fragment without any previous starting fragment")
func joinFragments(fragments [][]byte, size int) []byte {
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/VP8 decoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc7741
type Decoder struct {
firstPacketReceived bool
fragmentsSize int
fragments [][]byte
fragmentNextSeqNum uint16
}
// Init initializes the decoder.
func (d *Decoder) Init() error {
return nil
}
func (d *Decoder) resetFragments() {
d.fragments = d.fragments[:0]
d.fragmentsSize = 0
}
// Decode decodes a VP8 frame from a RTP packet.
func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) {
var vpkt codecs.VP8Packet
_, err := vpkt.Unmarshal(pkt.Payload)
if err != nil {
d.resetFragments()
return nil, err
}
if vpkt.PID != 0 {
d.resetFragments()
return nil, fmt.Errorf("packets containing single partitions are not supported")
}
var frame []byte
if vpkt.S == 1 {
d.resetFragments()
d.firstPacketReceived = true
if !pkt.Marker {
d.fragmentsSize = len(vpkt.Payload)
d.fragments = append(d.fragments, vpkt.Payload)
d.fragmentNextSeqNum = pkt.SequenceNumber + 1
return nil, ErrMorePacketsNeeded
}
frame = vpkt.Payload
} else {
if d.fragmentsSize == 0 {
if !d.firstPacketReceived {
return nil, ErrNonStartingPacketAndNoPrevious
}
return nil, fmt.Errorf("received a non-starting fragment")
}
if pkt.SequenceNumber != d.fragmentNextSeqNum {
d.resetFragments()
return nil, fmt.Errorf("discarding frame since a RTP packet is missing")
}
d.fragmentsSize += len(vpkt.Payload)
if d.fragmentsSize > vp8.MaxFrameSize {
errSize := d.fragmentsSize
d.resetFragments()
return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d",
errSize, vp8.MaxFrameSize)
}
d.fragments = append(d.fragments, vpkt.Payload)
d.fragmentNextSeqNum++
if !pkt.Marker {
return nil, ErrMorePacketsNeeded
}
frame = joinFragments(d.fragments, d.fragmentsSize)
d.resetFragments()
}
return frame, nil
}

View File

@ -0,0 +1,98 @@
package rtpvp8
import (
"crypto/rand"
"fmt"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
const (
rtpVersion = 2
defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
)
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
// Encoder is a RTP/VP8 encoder.
// Specification: https://datatracker.ietf.org/doc/html/rfc7741
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
// maximum size of packet payloads (optional).
// It defaults to 1460.
PayloadMaxSize int
sequenceNumber uint16
vp codecs.VP8Payloader
}
// Init initializes the encoder.
func (e *Encoder) Init() error {
if e.SSRC == nil {
v, err := randUint32()
if err != nil {
return err
}
e.SSRC = &v
}
if e.InitialSequenceNumber == nil {
v, err := randUint32()
if err != nil {
return err
}
v2 := uint16(v)
e.InitialSequenceNumber = &v2
}
if e.PayloadMaxSize == 0 {
e.PayloadMaxSize = defaultPayloadMaxSize
}
e.sequenceNumber = *e.InitialSequenceNumber
return nil
}
// Encode encodes a VP8 frame into RTP/VP8 packets.
func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) {
payloads := e.vp.Payload(uint16(e.PayloadMaxSize), frame)
if payloads == nil {
return nil, fmt.Errorf("payloader failed")
}
plen := len(payloads)
ret := make([]*rtp.Packet, plen)
for i, payload := range payloads {
ret[i] = &rtp.Packet{
Header: rtp.Header{
Version: rtpVersion,
PayloadType: e.PayloadType,
SequenceNumber: e.sequenceNumber,
SSRC: *e.SSRC,
Marker: i == (plen - 1),
},
Payload: payload,
}
e.sequenceNumber++
}
return ret, nil
}

View File

@ -0,0 +1,2 @@
// Package rtpvp8 contains a RTP/VP8 decoder and encoder.
package rtpvp8

Some files were not shown because too many files have changed in this diff Show More