21 changed files with 1513 additions and 79 deletions
@ -0,0 +1,191 @@
@@ -0,0 +1,191 @@
|
||||
package aac |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// ADTSPacket is an ADTS packet
|
||||
type ADTSPacket struct { |
||||
SampleRate int |
||||
ChannelCount int |
||||
Frame []byte |
||||
} |
||||
|
||||
// DecodeADTS decodes an ADTS stream into ADTS packets.
|
||||
func DecodeADTS(byts []byte) ([]*ADTSPacket, error) { |
||||
// refs: https://wiki.multimedia.cx/index.php/ADTS
|
||||
|
||||
var ret []*ADTSPacket |
||||
|
||||
for len(byts) > 0 { |
||||
syncWord := (uint16(byts[0]) << 4) | (uint16(byts[1]) >> 4) |
||||
if syncWord != 0xfff { |
||||
return nil, fmt.Errorf("invalid syncword") |
||||
} |
||||
|
||||
protectionAbsent := byts[1] & 0x01 |
||||
if protectionAbsent != 1 { |
||||
return nil, fmt.Errorf("ADTS with CRC is not supported") |
||||
} |
||||
|
||||
pkt := &ADTSPacket{} |
||||
|
||||
profile := (byts[2] >> 6) |
||||
if profile != 0 { |
||||
return nil, fmt.Errorf("only AAC-LC is supported") |
||||
} |
||||
|
||||
sampleRateIndex := (byts[2] >> 2) & 0x0F |
||||
switch sampleRateIndex { |
||||
case 0: |
||||
pkt.SampleRate = 96000 |
||||
case 1: |
||||
pkt.SampleRate = 88200 |
||||
case 2: |
||||
pkt.SampleRate = 64000 |
||||
case 3: |
||||
pkt.SampleRate = 48000 |
||||
case 4: |
||||
pkt.SampleRate = 44100 |
||||
case 5: |
||||
pkt.SampleRate = 32000 |
||||
case 6: |
||||
pkt.SampleRate = 24000 |
||||
case 7: |
||||
pkt.SampleRate = 22050 |
||||
case 8: |
||||
pkt.SampleRate = 16000 |
||||
case 9: |
||||
pkt.SampleRate = 12000 |
||||
case 10: |
||||
pkt.SampleRate = 11025 |
||||
case 11: |
||||
pkt.SampleRate = 8000 |
||||
case 12: |
||||
pkt.SampleRate = 7350 |
||||
default: |
||||
return nil, fmt.Errorf("invalid sample rate index: %d", sampleRateIndex) |
||||
} |
||||
|
||||
channelConfig := ((byts[2] & 0x01) << 2) | ((byts[3] >> 6) & 0x03) |
||||
switch channelConfig { |
||||
case 1: |
||||
pkt.ChannelCount = 1 |
||||
case 2: |
||||
pkt.ChannelCount = 2 |
||||
case 3: |
||||
pkt.ChannelCount = 3 |
||||
case 4: |
||||
pkt.ChannelCount = 4 |
||||
case 5: |
||||
pkt.ChannelCount = 5 |
||||
case 6: |
||||
pkt.ChannelCount = 6 |
||||
case 7: |
||||
pkt.ChannelCount = 8 |
||||
default: |
||||
return nil, fmt.Errorf("invalid channel configuration: %d", channelConfig) |
||||
} |
||||
|
||||
frameLen := int(((uint16(byts[3])&0x03)<<11)| |
||||
(uint16(byts[4])<<3)| |
||||
((uint16(byts[5])>>5)&0x07)) - 7 |
||||
|
||||
fullness := ((uint16(byts[5]) & 0x1F) << 6) | ((uint16(byts[6]) >> 2) & 0x3F) |
||||
if fullness != 1800 { |
||||
return nil, fmt.Errorf("fullness not supported") |
||||
} |
||||
|
||||
frameCount := byts[6] & 0x03 |
||||
if frameCount != 0 { |
||||
return nil, fmt.Errorf("multiple frame count not supported") |
||||
} |
||||
|
||||
if len(byts[7:]) < frameLen { |
||||
return nil, fmt.Errorf("invalid frame length") |
||||
} |
||||
|
||||
pkt.Frame = byts[7 : 7+frameLen] |
||||
byts = byts[7+frameLen:] |
||||
|
||||
ret = append(ret, pkt) |
||||
} |
||||
|
||||
return ret, nil |
||||
} |
||||
|
||||
// EncodeADTS encodes ADTS packets into an ADTS stream.
|
||||
func EncodeADTS(pkts []*ADTSPacket) ([]byte, error) { |
||||
var ret []byte |
||||
|
||||
for _, pkt := range pkts { |
||||
frameLen := len(pkt.Frame) + 7 |
||||
fullness := 1800 |
||||
|
||||
var channelConf uint8 |
||||
switch pkt.ChannelCount { |
||||
case 1: |
||||
channelConf = 1 |
||||
case 2: |
||||
channelConf = 2 |
||||
case 3: |
||||
channelConf = 3 |
||||
case 4: |
||||
channelConf = 4 |
||||
case 5: |
||||
channelConf = 5 |
||||
case 6: |
||||
channelConf = 6 |
||||
case 8: |
||||
channelConf = 7 |
||||
default: |
||||
return nil, fmt.Errorf("invalid channel count: %v", pkt.ChannelCount) |
||||
} |
||||
|
||||
var sampleRateIndex uint8 |
||||
switch pkt.SampleRate { |
||||
case 96000: |
||||
sampleRateIndex = 0 |
||||
case 88200: |
||||
sampleRateIndex = 1 |
||||
case 64000: |
||||
sampleRateIndex = 2 |
||||
case 48000: |
||||
sampleRateIndex = 3 |
||||
case 44100: |
||||
sampleRateIndex = 4 |
||||
case 32000: |
||||
sampleRateIndex = 5 |
||||
case 24000: |
||||
sampleRateIndex = 6 |
||||
case 22050: |
||||
sampleRateIndex = 7 |
||||
case 16000: |
||||
sampleRateIndex = 8 |
||||
case 12000: |
||||
sampleRateIndex = 9 |
||||
case 11025: |
||||
sampleRateIndex = 10 |
||||
case 8000: |
||||
sampleRateIndex = 11 |
||||
case 7350: |
||||
sampleRateIndex = 12 |
||||
default: |
||||
return nil, fmt.Errorf("invalid sample rate: %v", pkt.SampleRate) |
||||
} |
||||
|
||||
header := make([]byte, 7) |
||||
header[0] = 0xFF |
||||
header[1] = 0xF1 |
||||
header[2] = (sampleRateIndex << 2) | ((channelConf >> 2) & 0x01) |
||||
header[3] = (channelConf&0x03)<<6 | uint8((frameLen>>11)&0x03) |
||||
header[4] = uint8((frameLen >> 3) & 0xFF) |
||||
header[5] = uint8((frameLen&0x07)<<5 | ((fullness >> 6) & 0x1F)) |
||||
header[6] = uint8((fullness & 0x3F) << 2) |
||||
ret = append(ret, header...) |
||||
|
||||
ret = append(ret, pkt.Frame...) |
||||
} |
||||
|
||||
return ret, nil |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
package aac |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var casesADTS = []struct { |
||||
name string |
||||
byts []byte |
||||
pkts []*ADTSPacket |
||||
}{ |
||||
{ |
||||
"single", |
||||
[]byte{0xff, 0xf1, 0xc, 0x80, 0x1, 0x3c, 0x20, 0xaa, 0xbb}, |
||||
[]*ADTSPacket{ |
||||
{ |
||||
SampleRate: 48000, |
||||
ChannelCount: 2, |
||||
Frame: []byte{0xaa, 0xbb}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"multiple", |
||||
[]byte{0xff, 0xf1, 0x10, 0x40, 0x1, 0x3c, 0x20, 0xaa, 0xbb, 0xff, 0xf1, 0xc, 0x80, 0x1, 0x3c, 0x20, 0xcc, 0xdd}, |
||||
[]*ADTSPacket{ |
||||
{ |
||||
SampleRate: 44100, |
||||
ChannelCount: 1, |
||||
Frame: []byte{0xaa, 0xbb}, |
||||
}, |
||||
{ |
||||
SampleRate: 48000, |
||||
ChannelCount: 2, |
||||
Frame: []byte{0xcc, 0xdd}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
func TestDecodeADTS(t *testing.T) { |
||||
for _, ca := range casesADTS { |
||||
t.Run(ca.name, func(t *testing.T) { |
||||
pkts, err := DecodeADTS(ca.byts) |
||||
require.NoError(t, err) |
||||
require.Equal(t, ca.pkts, pkts) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestEncodeADTS(t *testing.T) { |
||||
for _, ca := range casesADTS { |
||||
t.Run(ca.name, func(t *testing.T) { |
||||
byts, err := EncodeADTS(ca.pkts) |
||||
require.NoError(t, err) |
||||
require.Equal(t, ca.byts, byts) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,600 @@
@@ -0,0 +1,600 @@
|
||||
package clienthls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/headers" |
||||
"github.com/aler9/gortsplib/pkg/ringbuffer" |
||||
"github.com/aler9/gortsplib/pkg/rtpaac" |
||||
"github.com/aler9/gortsplib/pkg/rtph264" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/client" |
||||
"github.com/aler9/rtsp-simple-server/internal/h264" |
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
"github.com/aler9/rtsp-simple-server/internal/serverhls" |
||||
"github.com/aler9/rtsp-simple-server/internal/stats" |
||||
) |
||||
|
||||
const ( |
||||
// an offset is needed to
|
||||
// - avoid negative PTS values
|
||||
// - avoid PTS < DTS during startup
|
||||
ptsOffset = 2 * time.Second |
||||
|
||||
segmentMinAUCount = 100 |
||||
closeCheckPeriod = 1 * time.Second |
||||
closeAfterInactivity = 60 * time.Second |
||||
) |
||||
|
||||
const index = `<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<style> |
||||
#video { |
||||
width: 600px; |
||||
height: 600px; |
||||
background: black; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.0.0"></script> |
||||
<video id="video" muted controls></video> |
||||
<script> |
||||
|
||||
const create = () => { |
||||
const video = document.getElementById('video'); |
||||
|
||||
const hls = new Hls({ |
||||
progressive: false, |
||||
}); |
||||
|
||||
hls.on(Hls.Events.ERROR, (evt, data) => { |
||||
if (data.fatal) { |
||||
hls.destroy(); |
||||
|
||||
setTimeout(() => { |
||||
create(); |
||||
}, 2000); |
||||
} |
||||
}); |
||||
|
||||
hls.loadSource('stream.m3u8'); |
||||
hls.attachMedia(video); |
||||
|
||||
video.play(); |
||||
} |
||||
create(); |
||||
|
||||
</script> |
||||
|
||||
</body> |
||||
</html> |
||||
` |
||||
|
||||
func ipEqualOrInRange(ip net.IP, ips []interface{}) bool { |
||||
for _, item := range ips { |
||||
switch titem := item.(type) { |
||||
case net.IP: |
||||
if titem.Equal(ip) { |
||||
return true |
||||
} |
||||
|
||||
case *net.IPNet: |
||||
if titem.Contains(ip) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
type trackIDPayloadPair struct { |
||||
trackID int |
||||
buf []byte |
||||
} |
||||
|
||||
// PathMan is implemented by pathman.PathMan.
|
||||
type PathMan interface { |
||||
OnClientSetupPlay(client.SetupPlayReq) |
||||
} |
||||
|
||||
// Parent is implemented by clientman.ClientMan.
|
||||
type Parent interface { |
||||
Log(logger.Level, string, ...interface{}) |
||||
OnClientClose(client.Client) |
||||
} |
||||
|
||||
// Client is a HLS client.
|
||||
type Client struct { |
||||
hlsSegmentCount int |
||||
hlsSegmentDuration time.Duration |
||||
readBufferCount int |
||||
wg *sync.WaitGroup |
||||
stats *stats.Stats |
||||
pathName string |
||||
pathMan PathMan |
||||
parent Parent |
||||
|
||||
path client.Path |
||||
ringBuffer *ringbuffer.RingBuffer |
||||
tsQueue []*tsFile |
||||
tsByName map[string]*tsFile |
||||
tsDeleteCount int |
||||
tsMutex sync.Mutex |
||||
lastRequestTime int64 |
||||
|
||||
// in
|
||||
request chan serverhls.Request |
||||
terminate chan struct{} |
||||
} |
||||
|
||||
// New allocates a Client.
|
||||
func New( |
||||
hlsSegmentCount int, |
||||
hlsSegmentDuration time.Duration, |
||||
readBufferCount int, |
||||
wg *sync.WaitGroup, |
||||
stats *stats.Stats, |
||||
pathName string, |
||||
pathMan PathMan, |
||||
parent Parent) *Client { |
||||
|
||||
c := &Client{ |
||||
hlsSegmentCount: hlsSegmentCount, |
||||
hlsSegmentDuration: hlsSegmentDuration, |
||||
readBufferCount: readBufferCount, |
||||
wg: wg, |
||||
stats: stats, |
||||
pathName: pathName, |
||||
pathMan: pathMan, |
||||
parent: parent, |
||||
lastRequestTime: time.Now().Unix(), |
||||
tsByName: make(map[string]*tsFile), |
||||
request: make(chan serverhls.Request), |
||||
terminate: make(chan struct{}), |
||||
} |
||||
|
||||
atomic.AddInt64(c.stats.CountClients, 1) |
||||
c.log(logger.Info, "connected (HLS)") |
||||
|
||||
c.wg.Add(1) |
||||
go c.run() |
||||
|
||||
return c |
||||
} |
||||
|
||||
// Close closes a Client.
|
||||
func (c *Client) Close() { |
||||
atomic.AddInt64(c.stats.CountClients, -1) |
||||
close(c.terminate) |
||||
} |
||||
|
||||
// IsClient implements client.Client.
|
||||
func (c *Client) IsClient() {} |
||||
|
||||
// IsSource implements path.source.
|
||||
func (c *Client) IsSource() {} |
||||
|
||||
func (c *Client) log(level logger.Level, format string, args ...interface{}) { |
||||
c.parent.Log(level, "[client hls/%s] "+format, append([]interface{}{c.pathName}, args...)...) |
||||
} |
||||
|
||||
// PathName returns the path name of the client.
|
||||
func (c *Client) PathName() string { |
||||
return c.pathName |
||||
} |
||||
|
||||
func (c *Client) run() { |
||||
defer c.wg.Done() |
||||
defer c.log(logger.Info, "disconnected") |
||||
|
||||
var videoTrack *gortsplib.Track |
||||
var h264SPS []byte |
||||
var h264PPS []byte |
||||
var h264Decoder *rtph264.Decoder |
||||
var audioTrack *gortsplib.Track |
||||
var aacConfig rtpaac.MPEG4AudioConfig |
||||
var aacDecoder *rtpaac.Decoder |
||||
|
||||
err := func() error { |
||||
pres := make(chan client.SetupPlayRes) |
||||
c.pathMan.OnClientSetupPlay(client.SetupPlayReq{c, c.pathName, nil, pres}) //nolint:govet
|
||||
res := <-pres |
||||
|
||||
if res.Err != nil { |
||||
return res.Err |
||||
} |
||||
|
||||
c.path = res.Path |
||||
|
||||
for i, t := range res.Tracks { |
||||
if t.IsH264() { |
||||
if videoTrack != nil { |
||||
return fmt.Errorf("can't read track %d with HLS: too many tracks", i+1) |
||||
} |
||||
videoTrack = t |
||||
|
||||
var err error |
||||
h264SPS, h264PPS, err = t.ExtractDataH264() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
h264Decoder = rtph264.NewDecoder() |
||||
|
||||
} else if t.IsAAC() { |
||||
if audioTrack != nil { |
||||
return fmt.Errorf("can't read track %d with HLS: too many tracks", i+1) |
||||
} |
||||
audioTrack = t |
||||
|
||||
byts, err := t.ExtractDataAAC() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = aacConfig.Decode(byts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
aacDecoder = rtpaac.NewDecoder(aacConfig.SampleRate) |
||||
} |
||||
} |
||||
|
||||
if videoTrack == nil && audioTrack == nil { |
||||
return fmt.Errorf("unable to find a video or audio track") |
||||
} |
||||
|
||||
return nil |
||||
}() |
||||
if err != nil { |
||||
c.log(logger.Info, "ERR: %s", err) |
||||
|
||||
go func() { |
||||
for req := range c.request { |
||||
req.W.WriteHeader(http.StatusNotFound) |
||||
req.Res <- nil |
||||
} |
||||
}() |
||||
|
||||
if c.path != nil { |
||||
res := make(chan struct{}) |
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res |
||||
} |
||||
|
||||
c.parent.OnClientClose(c) |
||||
<-c.terminate |
||||
|
||||
close(c.request) |
||||
return |
||||
} |
||||
|
||||
curTSFile := newTSFile(videoTrack, audioTrack) |
||||
c.tsByName[curTSFile.Name()] = curTSFile |
||||
c.tsQueue = append(c.tsQueue, curTSFile) |
||||
|
||||
defer func() { |
||||
curTSFile.Close() |
||||
}() |
||||
|
||||
requestDone := make(chan struct{}) |
||||
go c.runRequestHandler(requestDone) |
||||
|
||||
defer func() { |
||||
close(c.request) |
||||
<-requestDone |
||||
}() |
||||
|
||||
c.ringBuffer = ringbuffer.New(uint64(c.readBufferCount)) |
||||
|
||||
resc := make(chan client.PlayRes) |
||||
c.path.OnClientPlay(client.PlayReq{c, resc}) //nolint:govet
|
||||
<-resc |
||||
|
||||
c.log(logger.Info, "is reading from path '%s'", c.pathName) |
||||
|
||||
writerDone := make(chan error) |
||||
go func() { |
||||
writerDone <- func() error { |
||||
startPCR := time.Now() |
||||
var videoBuf [][]byte |
||||
videoDTSEst := h264.NewDTSEstimator() |
||||
audioAUCount := 0 |
||||
|
||||
for { |
||||
data, ok := c.ringBuffer.Pull() |
||||
if !ok { |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
pair := data.(trackIDPayloadPair) |
||||
|
||||
if videoTrack != nil && pair.trackID == videoTrack.ID { |
||||
nalus, pts, err := h264Decoder.Decode(pair.buf) |
||||
if err != nil { |
||||
if err != rtph264.ErrMorePacketsNeeded { |
||||
c.log(logger.Warn, "unable to decode video track: %v", err) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
for _, nalu := range nalus { |
||||
// remove SPS, PPS, AUD
|
||||
typ := h264.NALUType(nalu[0] & 0x1F) |
||||
switch typ { |
||||
case h264.NALUTypeSPS, h264.NALUTypePPS, h264.NALUTypeAccessUnitDelimiter: |
||||
continue |
||||
} |
||||
|
||||
// add SPS and PPS before IDR
|
||||
if typ == h264.NALUTypeIDR { |
||||
videoBuf = append(videoBuf, h264SPS) |
||||
videoBuf = append(videoBuf, h264PPS) |
||||
} |
||||
|
||||
videoBuf = append(videoBuf, nalu) |
||||
} |
||||
|
||||
// RTP marker means that all the NALUs with the same PTS have been received.
|
||||
// send them together.
|
||||
marker := (pair.buf[1] >> 7 & 0x1) > 0 |
||||
if marker { |
||||
isIDR := func() bool { |
||||
for _, nalu := range videoBuf { |
||||
typ := h264.NALUType(nalu[0] & 0x1F) |
||||
if typ == h264.NALUTypeIDR { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
}() |
||||
|
||||
if isIDR { |
||||
if curTSFile.firstPacketWritten && |
||||
time.Since(curTSFile.firstPacketWrittenTime) >= c.hlsSegmentDuration { |
||||
if curTSFile != nil { |
||||
curTSFile.Close() |
||||
} |
||||
|
||||
curTSFile = newTSFile(videoTrack, audioTrack) |
||||
c.tsMutex.Lock() |
||||
c.tsByName[curTSFile.Name()] = curTSFile |
||||
c.tsQueue = append(c.tsQueue, curTSFile) |
||||
if len(c.tsQueue) > c.hlsSegmentCount { |
||||
delete(c.tsByName, c.tsQueue[0].Name()) |
||||
c.tsQueue = c.tsQueue[1:] |
||||
c.tsDeleteCount++ |
||||
} |
||||
c.tsMutex.Unlock() |
||||
} |
||||
|
||||
} else { |
||||
if !curTSFile.firstPacketWritten { |
||||
continue |
||||
} |
||||
} |
||||
|
||||
curTSFile.SetPCR(time.Since(startPCR)) |
||||
err := curTSFile.WriteH264( |
||||
videoDTSEst.Feed(pts+ptsOffset), |
||||
pts+ptsOffset, |
||||
isIDR, |
||||
videoBuf) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
videoBuf = nil |
||||
} |
||||
|
||||
} else if audioTrack != nil && pair.trackID == audioTrack.ID { |
||||
aus, pts, err := aacDecoder.Decode(pair.buf) |
||||
if err != nil { |
||||
if err != rtpaac.ErrMorePacketsNeeded { |
||||
c.log(logger.Warn, "unable to decode audio track: %v", err) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
if videoTrack == nil { |
||||
if curTSFile.firstPacketWritten && |
||||
(time.Since(curTSFile.firstPacketWrittenTime) >= c.hlsSegmentDuration && |
||||
audioAUCount >= segmentMinAUCount) { |
||||
|
||||
if curTSFile != nil { |
||||
curTSFile.Close() |
||||
} |
||||
|
||||
audioAUCount = 0 |
||||
curTSFile = newTSFile(videoTrack, audioTrack) |
||||
c.tsMutex.Lock() |
||||
c.tsByName[curTSFile.Name()] = curTSFile |
||||
c.tsQueue = append(c.tsQueue, curTSFile) |
||||
if len(c.tsQueue) > c.hlsSegmentCount { |
||||
delete(c.tsByName, c.tsQueue[0].Name()) |
||||
c.tsQueue = c.tsQueue[1:] |
||||
c.tsDeleteCount++ |
||||
} |
||||
c.tsMutex.Unlock() |
||||
} |
||||
} else { |
||||
if !curTSFile.firstPacketWritten { |
||||
continue |
||||
} |
||||
} |
||||
|
||||
for i, au := range aus { |
||||
auPTS := pts + time.Duration(i)*1000*time.Second/time.Duration(aacConfig.SampleRate) |
||||
|
||||
audioAUCount++ |
||||
curTSFile.SetPCR(time.Since(startPCR)) |
||||
err := curTSFile.WriteAAC( |
||||
aacConfig.SampleRate, |
||||
aacConfig.ChannelCount, |
||||
auPTS+ptsOffset, |
||||
au) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}() |
||||
}() |
||||
|
||||
closeCheckTicker := time.NewTicker(closeCheckPeriod) |
||||
defer closeCheckTicker.Stop() |
||||
|
||||
for { |
||||
select { |
||||
case <-closeCheckTicker.C: |
||||
t := time.Unix(atomic.LoadInt64(&c.lastRequestTime), 0) |
||||
if time.Since(t) >= closeAfterInactivity { |
||||
c.log(logger.Info, "closing due to inactivity") |
||||
|
||||
c.ringBuffer.Close() |
||||
<-writerDone |
||||
|
||||
res := make(chan struct{}) |
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res |
||||
|
||||
c.parent.OnClientClose(c) |
||||
<-c.terminate |
||||
return |
||||
} |
||||
|
||||
case err := <-writerDone: |
||||
c.log(logger.Info, "ERR: %s", err) |
||||
|
||||
res := make(chan struct{}) |
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res |
||||
|
||||
c.parent.OnClientClose(c) |
||||
<-c.terminate |
||||
return |
||||
|
||||
case <-c.terminate: |
||||
res := make(chan struct{}) |
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res |
||||
|
||||
c.ringBuffer.Close() |
||||
<-writerDone |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *Client) runRequestHandler(done chan struct{}) { |
||||
defer close(done) |
||||
|
||||
for preq := range c.request { |
||||
req := preq |
||||
|
||||
atomic.StoreInt64(&c.lastRequestTime, time.Now().Unix()) |
||||
|
||||
conf := c.path.Conf() |
||||
|
||||
if conf.ReadIpsParsed != nil { |
||||
tmp, _, _ := net.SplitHostPort(req.Req.RemoteAddr) |
||||
ip := net.ParseIP(tmp) |
||||
if !ipEqualOrInRange(ip, conf.ReadIpsParsed) { |
||||
c.log(logger.Info, "ERR: ip '%s' not allowed", ip) |
||||
req.W.WriteHeader(http.StatusUnauthorized) |
||||
req.Res <- nil |
||||
continue |
||||
} |
||||
} |
||||
|
||||
if conf.ReadUser != "" { |
||||
user, pass, ok := req.Req.BasicAuth() |
||||
if !ok || user != conf.ReadUser || pass != conf.ReadPass { |
||||
req.W.Header().Set("WWW-Authenticate", `Basic realm="rtsp-simple-server"`) |
||||
req.W.WriteHeader(http.StatusUnauthorized) |
||||
req.Res <- nil |
||||
continue |
||||
} |
||||
} |
||||
|
||||
switch { |
||||
case req.Subpath == "stream.m3u8": |
||||
func() { |
||||
c.tsMutex.Lock() |
||||
defer c.tsMutex.Unlock() |
||||
|
||||
if len(c.tsQueue) == 0 { |
||||
req.W.WriteHeader(http.StatusNotFound) |
||||
req.Res <- nil |
||||
return |
||||
} |
||||
|
||||
cnt := "#EXTM3U\n" |
||||
cnt += "#EXT-X-VERSION:3\n" |
||||
cnt += "#EXT-X-ALLOW-CACHE:NO\n" |
||||
cnt += "#EXT-X-TARGETDURATION:10\n" |
||||
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(c.tsDeleteCount), 10) + "\n" |
||||
for _, f := range c.tsQueue { |
||||
cnt += "#EXTINF:10,\n" |
||||
cnt += f.Name() + ".ts\n" |
||||
} |
||||
req.Res <- bytes.NewReader([]byte(cnt)) |
||||
}() |
||||
|
||||
case strings.HasSuffix(req.Subpath, ".ts"): |
||||
base := strings.TrimSuffix(req.Subpath, ".ts") |
||||
|
||||
c.tsMutex.Lock() |
||||
f, ok := c.tsByName[base] |
||||
c.tsMutex.Unlock() |
||||
|
||||
if !ok { |
||||
req.W.WriteHeader(http.StatusNotFound) |
||||
req.Res <- nil |
||||
continue |
||||
} |
||||
|
||||
req.Res <- f.buf.NewReader() |
||||
|
||||
case req.Subpath == "": |
||||
req.Res <- bytes.NewReader([]byte(index)) |
||||
|
||||
default: |
||||
req.W.WriteHeader(http.StatusNotFound) |
||||
req.Res <- nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
// OnRequest is called by clientman.ClientMan.
|
||||
func (c *Client) OnRequest(req serverhls.Request) { |
||||
c.request <- req |
||||
} |
||||
|
||||
// Authenticate performs an authentication.
|
||||
func (c *Client) Authenticate(authMethods []headers.AuthMethod, |
||||
pathName string, ips []interface{}, |
||||
user string, pass string, req interface{}) error { |
||||
return nil |
||||
} |
||||
|
||||
// OnFrame implements path.Reader.
|
||||
func (c *Client) OnFrame(trackID int, streamType gortsplib.StreamType, payload []byte) { |
||||
if streamType == gortsplib.StreamTypeRTP { |
||||
c.ringBuffer.Push(trackIDPayloadPair{trackID, payload}) |
||||
} |
||||
} |
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
package clienthls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"sync" |
||||
) |
||||
|
||||
type multiAccessBufferReader struct { |
||||
m *multiAccessBuffer |
||||
readPos int |
||||
} |
||||
|
||||
func (r *multiAccessBufferReader) Read(p []byte) (int, error) { |
||||
newReadPos := r.readPos + len(p) |
||||
|
||||
curBuf, err := func() ([]byte, error) { |
||||
r.m.mutex.Lock() |
||||
defer r.m.mutex.Unlock() |
||||
|
||||
if r.m.closed && r.readPos >= r.m.writePos { |
||||
return nil, io.EOF |
||||
} |
||||
|
||||
for !r.m.closed && newReadPos >= r.m.writePos { |
||||
r.m.cond.Wait() |
||||
} |
||||
|
||||
return r.m.buf.Bytes(), nil |
||||
}() |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
n := copy(p, curBuf[r.readPos:]) |
||||
r.readPos += n |
||||
|
||||
return n, nil |
||||
} |
||||
|
||||
type multiAccessBuffer struct { |
||||
buf bytes.Buffer |
||||
closed bool |
||||
writePos int |
||||
mutex sync.Mutex |
||||
cond *sync.Cond |
||||
} |
||||
|
||||
func newMultiAccessBuffer() *multiAccessBuffer { |
||||
m := &multiAccessBuffer{} |
||||
m.cond = sync.NewCond(&m.mutex) |
||||
return m |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) Close() error { |
||||
m.mutex.Lock() |
||||
m.closed = true |
||||
m.mutex.Unlock() |
||||
m.cond.Broadcast() |
||||
return nil |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) Write(p []byte) (int, error) { |
||||
m.mutex.Lock() |
||||
n, _ := m.buf.Write(p) |
||||
m.writePos += n |
||||
m.mutex.Unlock() |
||||
m.cond.Broadcast() |
||||
return n, nil |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) NewReader() *multiAccessBufferReader { |
||||
return &multiAccessBufferReader{ |
||||
m: m, |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package clienthls |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestMultiAccessBuffer(t *testing.T) { |
||||
m := newMultiAccessBuffer() |
||||
|
||||
m.Write([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}) |
||||
|
||||
r := m.NewReader() |
||||
|
||||
buf := make([]byte, 4) |
||||
n, err := r.Read(buf) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, buf[:n]) |
||||
|
||||
m.Close() |
||||
|
||||
buf = make([]byte, 10) |
||||
n, err = r.Read(buf) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []byte{0x05, 0x06, 0x07, 0x08}, buf[:n]) |
||||
} |
@ -0,0 +1,160 @@
@@ -0,0 +1,160 @@
|
||||
package clienthls |
||||
|
||||
import ( |
||||
"context" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/asticode/go-astits" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/aac" |
||||
"github.com/aler9/rtsp-simple-server/internal/h264" |
||||
) |
||||
|
||||
type tsFile struct { |
||||
name string |
||||
buf *multiAccessBuffer |
||||
mux *astits.Muxer |
||||
pcrTrackIsVideo bool |
||||
pcr time.Duration |
||||
firstPacketWritten bool |
||||
firstPacketWrittenTime time.Time |
||||
} |
||||
|
||||
func newTSFile(videoTrack *gortsplib.Track, audioTrack *gortsplib.Track) *tsFile { |
||||
t := &tsFile{ |
||||
buf: newMultiAccessBuffer(), |
||||
name: strconv.FormatInt(time.Now().Unix(), 10), |
||||
} |
||||
|
||||
t.mux = astits.NewMuxer(context.Background(), t.buf) |
||||
|
||||
if videoTrack != nil { |
||||
t.mux.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 256, |
||||
StreamType: astits.StreamTypeH264Video, |
||||
}) |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
t.mux.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 257, |
||||
StreamType: astits.StreamTypeAACAudio, |
||||
}) |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
t.pcrTrackIsVideo = true |
||||
t.mux.SetPCRPID(256) |
||||
} else { |
||||
t.pcrTrackIsVideo = false |
||||
t.mux.SetPCRPID(257) |
||||
} |
||||
|
||||
return t |
||||
} |
||||
|
||||
func (t *tsFile) Close() error { |
||||
return t.buf.Close() |
||||
} |
||||
|
||||
func (t *tsFile) Name() string { |
||||
return t.name |
||||
} |
||||
|
||||
func (t *tsFile) FirstPacketWritten() bool { |
||||
return t.firstPacketWritten |
||||
} |
||||
|
||||
func (t *tsFile) FirstPacketWrittenTime() time.Time { |
||||
return t.firstPacketWrittenTime |
||||
} |
||||
|
||||
func (t *tsFile) SetPCR(pcr time.Duration) { |
||||
t.pcr = pcr |
||||
} |
||||
|
||||
func (t *tsFile) WriteH264(dts time.Duration, pts time.Duration, isIDR bool, nalus [][]byte) error { |
||||
if !t.firstPacketWritten { |
||||
t.firstPacketWritten = true |
||||
t.firstPacketWrittenTime = time.Now() |
||||
} |
||||
|
||||
enc, err := h264.EncodeAnnexB(nalus) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
af := &astits.PacketAdaptationField{ |
||||
RandomAccessIndicator: isIDR, |
||||
} |
||||
|
||||
if t.pcrTrackIsVideo { |
||||
af.HasPCR = true |
||||
af.PCR = &astits.ClockReference{Base: int64(t.pcr.Seconds() * 90000)} |
||||
} |
||||
|
||||
_, err = t.mux.WriteData(&astits.MuxerData{ |
||||
PID: 256, |
||||
AdaptationField: af, |
||||
PES: &astits.PESData{ |
||||
Header: &astits.PESHeader{ |
||||
OptionalHeader: &astits.PESOptionalHeader{ |
||||
MarkerBits: 2, |
||||
PTSDTSIndicator: astits.PTSDTSIndicatorBothPresent, |
||||
DTS: &astits.ClockReference{Base: int64(dts.Seconds() * 90000)}, |
||||
PTS: &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}, |
||||
}, |
||||
StreamID: 224, // = video
|
||||
}, |
||||
Data: enc, |
||||
}, |
||||
}) |
||||
return err |
||||
} |
||||
|
||||
func (t *tsFile) WriteAAC(sampleRate int, channelCount int, pts time.Duration, au []byte) error { |
||||
if !t.firstPacketWritten { |
||||
t.firstPacketWritten = true |
||||
t.firstPacketWrittenTime = time.Now() |
||||
} |
||||
|
||||
adtsPkt, err := aac.EncodeADTS([]*aac.ADTSPacket{ |
||||
{ |
||||
SampleRate: sampleRate, |
||||
ChannelCount: channelCount, |
||||
Frame: au, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
af := &astits.PacketAdaptationField{ |
||||
RandomAccessIndicator: true, |
||||
} |
||||
|
||||
if !t.pcrTrackIsVideo { |
||||
af.HasPCR = true |
||||
af.PCR = &astits.ClockReference{Base: int64(t.pcr.Seconds() * 90000)} |
||||
} |
||||
|
||||
_, err = t.mux.WriteData(&astits.MuxerData{ |
||||
PID: 257, |
||||
AdaptationField: af, |
||||
PES: &astits.PESData{ |
||||
Header: &astits.PESHeader{ |
||||
OptionalHeader: &astits.PESOptionalHeader{ |
||||
MarkerBits: 2, |
||||
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS, |
||||
PTS: &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}, |
||||
}, |
||||
PacketLength: uint16(len(adtsPkt) + 8), |
||||
StreamID: 192, // = audio
|
||||
}, |
||||
Data: adtsPkt, |
||||
}, |
||||
}) |
||||
return err |
||||
} |
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
package serverhls |
||||
|
||||
import ( |
||||
"context" |
||||
"io" |
||||
"net" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
) |
||||
|
||||
// Request is an HTTP request received by the HLS server.
|
||||
type Request struct { |
||||
Path string |
||||
Subpath string |
||||
Req *http.Request |
||||
W http.ResponseWriter |
||||
Res chan io.Reader |
||||
} |
||||
|
||||
// Parent is implemented by program.
|
||||
type Parent interface { |
||||
Log(logger.Level, string, ...interface{}) |
||||
} |
||||
|
||||
// Server is an HLS server.
|
||||
type Server struct { |
||||
parent Parent |
||||
|
||||
ln net.Listener |
||||
s *http.Server |
||||
|
||||
// out
|
||||
request chan Request |
||||
} |
||||
|
||||
// New allocates a Server.
|
||||
func New( |
||||
listenIP string, |
||||
port int, |
||||
parent Parent, |
||||
) (*Server, error) { |
||||
address := listenIP + ":" + strconv.FormatInt(int64(port), 10) |
||||
ln, err := net.Listen("tcp", address) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
s := &Server{ |
||||
parent: parent, |
||||
ln: ln, |
||||
request: make(chan Request), |
||||
} |
||||
|
||||
s.s = &http.Server{ |
||||
Handler: s, |
||||
} |
||||
|
||||
s.log(logger.Info, "opened on "+address) |
||||
|
||||
go s.s.Serve(s.ln) |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
func (s *Server) log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[HLS listener] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
// Close closes all the server resources.
|
||||
func (s *Server) Close() { |
||||
go func() { |
||||
for req := range s.request { |
||||
req.Res <- nil |
||||
} |
||||
}() |
||||
s.s.Shutdown(context.Background()) |
||||
close(s.request) |
||||
} |
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
s.log(logger.Info, "%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) |
||||
|
||||
// remove leading prefix
|
||||
path := r.URL.Path[1:] |
||||
|
||||
if path == "" || path == "favicon.ico" { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
parts := strings.SplitN(path, "/", 2) |
||||
if len(parts) < 2 { |
||||
w.Header().Add("Location", parts[0]+"/") |
||||
w.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
|
||||
cres := make(chan io.Reader) |
||||
s.request <- Request{ |
||||
Path: parts[0], |
||||
Subpath: parts[1], |
||||
Req: r, |
||||
W: w, |
||||
Res: cres, |
||||
} |
||||
res := <-cres |
||||
|
||||
if res != nil { |
||||
io.Copy(w, res) |
||||
} |
||||
} |
||||
|
||||
// Request returns a channel to handle incoming HTTP requests.
|
||||
func (s *Server) Request() chan Request { |
||||
return s.request |
||||
} |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestClientHLSRead(t *testing.T) { |
||||
p, ok := testProgram("") |
||||
require.Equal(t, true, ok) |
||||
defer p.close() |
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{ |
||||
"-re", |
||||
"-stream_loop", "-1", |
||||
"-i", "emptyvideo.mkv", |
||||
"-c", "copy", |
||||
"-f", "rtsp", |
||||
"rtsp://" + ownDockerIP + ":8554/teststream", |
||||
}) |
||||
require.NoError(t, err) |
||||
defer cnt1.close() |
||||
|
||||
time.Sleep(1 * time.Second) |
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{ |
||||
"-i", "http://" + ownDockerIP + ":8888/teststream/stream.m3u8", |
||||
"-vframes", "1", |
||||
"-f", "image2", |
||||
"-y", "/dev/null", |
||||
}) |
||||
require.NoError(t, err) |
||||
defer cnt2.close() |
||||
require.Equal(t, 0, cnt2.wait()) |
||||
} |
||||
|
||||
func TestClientHLSReadAuth(t *testing.T) { |
||||
p, ok := testProgram( |
||||
"paths:\n" + |
||||
" all:\n" + |
||||
" readUser: testuser\n" + |
||||
" readPass: testpass\n" + |
||||
" readIps: [172.17.0.0/16]\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.close() |
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{ |
||||
"-re", |
||||
"-stream_loop", "-1", |
||||
"-i", "emptyvideo.mkv", |
||||
"-c", "copy", |
||||
"-f", "rtsp", |
||||
"rtsp://" + ownDockerIP + ":8554/teststream", |
||||
}) |
||||
require.NoError(t, err) |
||||
defer cnt1.close() |
||||
|
||||
time.Sleep(1 * time.Second) |
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{ |
||||
"-i", "http://testuser:testpass@" + ownDockerIP + ":8888/teststream/stream.m3u8", |
||||
"-vframes", "1", |
||||
"-f", "image2", |
||||
"-y", "/dev/null", |
||||
}) |
||||
require.NoError(t, err) |
||||
defer cnt2.close() |
||||
require.Equal(t, 0, cnt2.wait()) |
||||
} |
Loading…
Reference in new issue