Browse Source
* support reading UDP/MPEG-TS streams * support reading H265 and Opus tracks inside UDP/MPEG-TS streams * improve timestamp precision of Opus packetspull/1592/head
8 changed files with 437 additions and 39 deletions
@ -0,0 +1,350 @@
@@ -0,0 +1,350 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
||||
"github.com/aler9/gortsplib/v2/pkg/format" |
||||
"github.com/aler9/gortsplib/v2/pkg/media" |
||||
"github.com/asticode/go-astits" |
||||
"github.com/bluenviron/gohlslib/pkg/mpegts" |
||||
"golang.org/x/net/ipv4" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/conf" |
||||
"github.com/aler9/rtsp-simple-server/internal/formatprocessor" |
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
) |
||||
|
||||
const ( |
||||
multicastTTL = 16 |
||||
) |
||||
|
||||
var opusDurations = [32]int{ |
||||
480, 960, 1920, 2880, /* Silk NB */ |
||||
480, 960, 1920, 2880, /* Silk MB */ |
||||
480, 960, 1920, 2880, /* Silk WB */ |
||||
480, 960, /* Hybrid SWB */ |
||||
480, 960, /* Hybrid FB */ |
||||
120, 240, 480, 960, /* CELT NB */ |
||||
120, 240, 480, 960, /* CELT NB */ |
||||
120, 240, 480, 960, /* CELT NB */ |
||||
120, 240, 480, 960, /* CELT NB */ |
||||
} |
||||
|
||||
func opusGetPacketDuration(pkt []byte) time.Duration { |
||||
if len(pkt) == 0 { |
||||
return 0 |
||||
} |
||||
|
||||
frameDuration := opusDurations[pkt[0]>>3] |
||||
|
||||
frameCount := 0 |
||||
switch pkt[0] & 3 { |
||||
case 0: |
||||
frameCount = 1 |
||||
case 1: |
||||
frameCount = 2 |
||||
case 2: |
||||
frameCount = 2 |
||||
case 3: |
||||
if len(pkt) < 2 { |
||||
return 0 |
||||
} |
||||
frameCount = int(pkt[1] & 63) |
||||
} |
||||
|
||||
return (time.Duration(frameDuration) * time.Duration(frameCount) * time.Millisecond) / 48 |
||||
} |
||||
|
||||
type readerFunc func([]byte) (int, error) |
||||
|
||||
func (rf readerFunc) Read(p []byte) (int, error) { |
||||
return rf(p) |
||||
} |
||||
|
||||
type udpSourceParent interface { |
||||
log(logger.Level, string, ...interface{}) |
||||
sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes |
||||
sourceStaticImplSetNotReady(req pathSourceStaticSetNotReadyReq) |
||||
} |
||||
|
||||
type udpSource struct { |
||||
readTimeout conf.StringDuration |
||||
parent udpSourceParent |
||||
} |
||||
|
||||
func newUDPSource( |
||||
readTimeout conf.StringDuration, |
||||
parent udpSourceParent, |
||||
) *udpSource { |
||||
return &udpSource{ |
||||
readTimeout: readTimeout, |
||||
parent: parent, |
||||
} |
||||
} |
||||
|
||||
func (s *udpSource) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.log(level, "[udp source] "+format, args...) |
||||
} |
||||
|
||||
// run implements sourceStaticImpl.
|
||||
func (s *udpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { |
||||
s.Log(logger.Debug, "connecting") |
||||
|
||||
hostPort := cnf.Source[len("udp://"):] |
||||
|
||||
pc, err := net.ListenPacket("udp", hostPort) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pc.Close() |
||||
|
||||
host, _, _ := net.SplitHostPort(hostPort) |
||||
ip := net.ParseIP(host) |
||||
|
||||
if ip.IsMulticast() { |
||||
p := ipv4.NewPacketConn(pc) |
||||
|
||||
err = p.SetMulticastTTL(multicastTTL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
intfs, err := net.Interfaces() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, intf := range intfs { |
||||
err := p.JoinGroup(&intf, &net.UDPAddr{IP: ip}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
midbuffer := make([]byte, 0, 1472) // UDP MTU
|
||||
midbufferPos := 0 |
||||
|
||||
readPacket := func(buf []byte) (int, error) { |
||||
if midbufferPos < len(midbuffer) { |
||||
n := copy(buf, midbuffer[midbufferPos:]) |
||||
midbufferPos += n |
||||
return n, nil |
||||
} |
||||
|
||||
mn, _, err := pc.ReadFrom(midbuffer[:cap(midbuffer)]) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
if (mn % 188) != 0 { |
||||
return 0, fmt.Errorf("received packet with size %d not multiple of 188", mn) |
||||
} |
||||
|
||||
midbuffer = midbuffer[:mn] |
||||
n := copy(buf, midbuffer) |
||||
midbufferPos = n |
||||
return n, nil |
||||
} |
||||
|
||||
dem := astits.NewDemuxer( |
||||
context.Background(), |
||||
readerFunc(readPacket), |
||||
astits.DemuxerOptPacketSize(188)) |
||||
|
||||
readerErr := make(chan error) |
||||
|
||||
go func() { |
||||
readerErr <- func() error { |
||||
pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) |
||||
tracks, err := mpegts.FindTracks(dem) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var medias media.Medias |
||||
mediaCallbacks := make(map[uint16]func(time.Duration, []byte), len(tracks)) |
||||
var stream *stream |
||||
|
||||
for _, track := range tracks { |
||||
medi := &media.Media{ |
||||
Formats: []format.Format{track.Format}, |
||||
} |
||||
medias = append(medias, medi) |
||||
cformat := track.Format |
||||
|
||||
switch track.Format.(type) { |
||||
case *format.H264: |
||||
medi.Type = media.TypeVideo |
||||
|
||||
mediaCallbacks[track.ES.ElementaryPID] = func(pts time.Duration, data []byte) { |
||||
au, err := h264.AnnexBUnmarshal(data) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
return |
||||
} |
||||
|
||||
err = stream.writeData(medi, cformat, &formatprocessor.UnitH264{ |
||||
PTS: pts, |
||||
AU: au, |
||||
NTP: time.Now(), |
||||
}) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
} |
||||
} |
||||
|
||||
case *format.H265: |
||||
medi.Type = media.TypeVideo |
||||
|
||||
mediaCallbacks[track.ES.ElementaryPID] = func(pts time.Duration, data []byte) { |
||||
au, err := h264.AnnexBUnmarshal(data) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
return |
||||
} |
||||
|
||||
err = stream.writeData(medi, cformat, &formatprocessor.UnitH265{ |
||||
PTS: pts, |
||||
AU: au, |
||||
NTP: time.Now(), |
||||
}) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
} |
||||
} |
||||
|
||||
case *format.MPEG4Audio: |
||||
medi.Type = media.TypeAudio |
||||
|
||||
mediaCallbacks[track.ES.ElementaryPID] = func(pts time.Duration, data []byte) { |
||||
var pkts mpeg4audio.ADTSPackets |
||||
err := pkts.Unmarshal(data) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
return |
||||
} |
||||
|
||||
aus := make([][]byte, len(pkts)) |
||||
for i, pkt := range pkts { |
||||
aus[i] = pkt.AU |
||||
} |
||||
|
||||
err = stream.writeData(medi, cformat, &formatprocessor.UnitMPEG4Audio{ |
||||
PTS: pts, |
||||
AUs: aus, |
||||
NTP: time.Now(), |
||||
}) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
} |
||||
} |
||||
|
||||
case *format.Opus: |
||||
medi.Type = media.TypeAudio |
||||
|
||||
mediaCallbacks[track.ES.ElementaryPID] = func(pts time.Duration, data []byte) { |
||||
pos := 0 |
||||
|
||||
for { |
||||
var au mpegts.OpusAccessUnit |
||||
n, err := au.Unmarshal(data[pos:]) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
return |
||||
} |
||||
pos += n |
||||
|
||||
err = stream.writeData(medi, cformat, &formatprocessor.UnitOpus{ |
||||
PTS: pts, |
||||
Frame: au.Frame, |
||||
NTP: time.Now(), |
||||
}) |
||||
if err != nil { |
||||
s.Log(logger.Warn, "%v", err) |
||||
} |
||||
|
||||
if len(data[pos:]) == 0 { |
||||
break |
||||
} |
||||
|
||||
pts += opusGetPacketDuration(au.Frame) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{ |
||||
medias: medias, |
||||
generateRTPPackets: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
defer func() { |
||||
s.parent.sourceStaticImplSetNotReady(pathSourceStaticSetNotReadyReq{}) |
||||
}() |
||||
|
||||
s.Log(logger.Info, "ready: %s", sourceMediaInfo(medias)) |
||||
|
||||
stream = res.stream |
||||
var timedec *mpegts.TimeDecoder |
||||
|
||||
for { |
||||
pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) |
||||
data, err := dem.NextData() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if data.PES == nil { |
||||
continue |
||||
} |
||||
|
||||
if data.PES.Header.OptionalHeader == nil || |
||||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorNoPTSOrDTS || |
||||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorIsForbidden { |
||||
return fmt.Errorf("PTS is missing") |
||||
} |
||||
|
||||
var pts time.Duration |
||||
if timedec == nil { |
||||
timedec = mpegts.NewTimeDecoder(data.PES.Header.OptionalHeader.PTS.Base) |
||||
pts = 0 |
||||
} else { |
||||
pts = timedec.Decode(data.PES.Header.OptionalHeader.PTS.Base) |
||||
} |
||||
|
||||
cb, ok := mediaCallbacks[data.PID] |
||||
if !ok { |
||||
continue |
||||
} |
||||
|
||||
cb(pts, data.PES.Data) |
||||
} |
||||
}() |
||||
}() |
||||
|
||||
select { |
||||
case err := <-readerErr: |
||||
return err |
||||
|
||||
case <-ctx.Done(): |
||||
pc.Close() |
||||
<-readerErr |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
// apiSourceDescribe implements sourceStaticImpl.
|
||||
func (*udpSource) apiSourceDescribe() interface{} { |
||||
return struct { |
||||
Type string `json:"type"` |
||||
}{"udpSource"} |
||||
} |
Loading…
Reference in new issue