Browse Source

support proxying HLS

pull/591/head
aler9 4 years ago committed by Alessandro Ros
parent
commit
df4c268813
  1. 39
      README.md
  2. 1
      go.mod
  3. 2
      go.sum
  4. 187
      internal/aac/adts.go
  5. 17
      internal/aac/adts_test.go
  6. 23
      internal/conf/path.go
  7. 166
      internal/core/hls_source.go
  8. 160
      internal/core/hls_source_test.go
  9. 18
      internal/core/path.go
  10. 53
      internal/core/rtmp_source_test.go
  11. 892
      internal/hls/client.go
  12. 128
      internal/hls/client_test.go
  13. 2
      internal/hls/segment.go
  14. 3
      rtsp-simple-server.yml

39
README.md

@ -9,19 +9,20 @@ @@ -9,19 +9,20 @@
[![Release](https://img.shields.io/github/v/release/aler9/rtsp-simple-server)](https://github.com/aler9/rtsp-simple-server/releases)
[![Docker Hub](https://img.shields.io/badge/docker-aler9/rtsp--simple--server-blue)](https://hub.docker.com/r/aler9/rtsp-simple-server)
_rtsp-simple-server_ is a ready-to-use and zero-dependency RTSP / RTMP / HLS server and proxy, a software that allows users to publish, read and proxy live video and audio streams. RTSP, RTMP and HLS are independent protocols that allows to perform these operations with the help of a server, that is contacted by both publishers and readers and relays the publisher's streams to the readers; in particular:
_rtsp-simple-server_ is a ready-to-use and zero-dependency server and proxy that allows users to publish, read and proxy live video and audio streams through various protocols:
* RTSP is the fastest way to publish and read streams
* RTMP allows to interact with legacy servers or software
* HLS allows to view streams from a web page
|protocol|description|publish|read|proxy|
|--------|-----------|-------|----|-----|
|RTSP|fastest way to publish and read streams|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|RTMP|allows to interact with legacy software|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|HLS|allows to embed streams into a web page|:x:|:heavy_check_mark:|:heavy_check_mark:|
Features:
* Publish live streams with RTSP (UDP, TCP or TLS mode) or RTMP
* Read live streams with RTSP (UDP, UDP-multicast, TCP or TLS mode), RTMP or HLS
* Pull and serve streams from other RTSP or RTMP servers or cameras, always or on-demand (RTSP proxy)
* Publish and read live streams
* Act as a proxy and serve streams from other servers or cameras, always or on-demand
* Each stream can have multiple video and audio tracks, encoded with any codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG
* Streams are automatically converted from a protocol to another. For instance, it's possible to publish with RTSP and read with HLS
* Streams are automatically converted from a protocol to another. For instance, it's possible to publish a stream with RTSP and read it with HLS
Plus:
@ -127,18 +128,20 @@ docker run --rm -it -e RTSP_PROTOCOLS=tcp -p 8554:8554 -p 1935:1935 aler9/rtsp-s @@ -127,18 +128,20 @@ docker run --rm -it -e RTSP_PROTOCOLS=tcp -p 8554:8554 -p 1935:1935 aler9/rtsp-s
All the configuration parameters are listed and commented in the [configuration file](rtsp-simple-server.yml).
There are two ways to change the configuration:
There are 3 ways to change the configuration:
* By editing the `rtsp-simple-server.yml` file, that is
1. By editing the `rtsp-simple-server.yml` file, that is
* included into the release bundle
* available in the root folder of the Docker image (`/rtsp-simple-server.yml`); it can be overridden in this way:
* included into the release bundle
* available in the root folder of the Docker image (`/rtsp-simple-server.yml`); it can be overridden in this way:
```
docker run --rm -it --network=host -v $PWD/rtsp-simple-server.yml:/rtsp-simple-server.yml aler9/rtsp-simple-server
```
```
docker run --rm -it --network=host -v $PWD/rtsp-simple-server.yml:/rtsp-simple-server.yml aler9/rtsp-simple-server
```
* By overriding configuration parameters with environment variables, in the format `RTSP_PARAMNAME`, where `PARAMNAME` is the uppercase name of a parameter. For instance, the `rtspAddress` parameter can be overridden in the following way:
The configuration can be changed dinamically when the server is running (hot reloading) by writing to the configuration file. Changes are detected and applied without disconnecting existing clients, whenever it's possible.
2. By overriding configuration parameters with environment variables, in the format `RTSP_PARAMNAME`, where `PARAMNAME` is the uppercase name of a parameter. For instance, the `rtspAddress` parameter can be overridden in the following way:
```
RTSP_RTSPADDRESS="127.0.0.1:8554" ./rtsp-simple-server
@ -156,7 +159,7 @@ There are two ways to change the configuration: @@ -156,7 +159,7 @@ There are two ways to change the configuration:
docker run --rm -it --network=host -e RTSP_PATHS_TEST_SOURCE=rtsp://myurl aler9/rtsp-simple-server
```
The configuration can be changed dinamically when the server is running (hot reloading) by writing to the configuration file. Changes are detected and applied without disconnecting existing clients, whenever it's possible.
3. By using the [HTTP API](#http-api).
### Encryption
@ -258,7 +261,7 @@ RTSP_CONFKEY=mykey ./rtsp-simple-server @@ -258,7 +261,7 @@ RTSP_CONFKEY=mykey ./rtsp-simple-server
### Proxy mode
_rtsp-simple-server_ is also a RTSP and RTMP proxy, that is usually deployed in one of these scenarios:
_rtsp-simple-server_ is also a proxy, that is usually deployed in one of these scenarios:
* when there are multiple users that are receiving a stream and the bandwidth is limited; the proxy is used to receive the stream once. Users can then connect to the proxy instead of the original source.
* when there's a NAT / firewall between a stream and the users; the proxy is installed on the NAT and makes the stream available to the outside world.

1
go.mod

@ -10,6 +10,7 @@ require ( @@ -10,6 +10,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.7.2
github.com/gookit/color v1.4.2
github.com/grafov/m3u8 v0.11.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/notedit/rtmp v0.0.2
github.com/pion/rtp v1.6.2

2
go.sum

@ -32,6 +32,8 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW @@ -32,6 +32,8 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=

187
internal/aac/adts.go

@ -4,11 +4,42 @@ import ( @@ -4,11 +4,42 @@ import (
"fmt"
)
const (
mpegAudioTypeAACLLC = 2
)
var sampleRates = []int{
96000,
88200,
64000,
48000,
44100,
32000,
24000,
22050,
16000,
12000,
11025,
8000,
7350,
}
var channelCounts = []int{
1,
2,
3,
4,
5,
6,
8,
}
// ADTSPacket is an ADTS packet
type ADTSPacket struct {
Type int
SampleRate int
ChannelCount int
Frame []byte
AU []byte
}
// DecodeADTS decodes an ADTS stream into ADTS packets.
@ -25,64 +56,36 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) { @@ -25,64 +56,36 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) {
protectionAbsent := byts[1] & 0x01
if protectionAbsent != 1 {
return nil, fmt.Errorf("ADTS with CRC is not supported")
return nil, fmt.Errorf("CRC is not supported")
}
pkt := &ADTSPacket{}
profile := (byts[2] >> 6)
if profile != 0 {
return nil, fmt.Errorf("only AAC-LC is supported")
pkt.Type = int((byts[2] >> 6) + 1)
switch pkt.Type {
case mpegAudioTypeAACLLC:
default:
return nil, fmt.Errorf("unsupported object type: %d", pkt.Type)
}
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
switch {
case sampleRateIndex <= 12:
pkt.SampleRate = sampleRates[sampleRateIndex]
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
switch {
case channelConfig >= 1 && channelConfig <= 7:
pkt.ChannelCount = channelCounts[channelConfig-1]
default:
return nil, fmt.Errorf("invalid channel configuration: %d", channelConfig)
}
@ -91,10 +94,7 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) { @@ -91,10 +94,7 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) {
(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")
}
// fullness := ((uint16(byts[5]) & 0x1F) << 6) | ((uint16(byts[6]) >> 2) & 0x3F)
frameCount := byts[6] & 0x03
if frameCount != 0 {
@ -105,7 +105,7 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) { @@ -105,7 +105,7 @@ func DecodeADTS(byts []byte) ([]*ADTSPacket, error) {
return nil, fmt.Errorf("invalid frame length")
}
pkt.Frame = byts[7 : 7+frameLen]
pkt.AU = byts[7 : 7+frameLen]
byts = byts[7+frameLen:]
ret = append(ret, pkt)
@ -119,72 +119,47 @@ func EncodeADTS(pkts []*ADTSPacket) ([]byte, error) { @@ -119,72 +119,47 @@ 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)
sampleRateIndex := func() int {
for i, s := range sampleRates {
if s == pkt.SampleRate {
return i
}
}
return -1
}()
if sampleRateIndex == -1 {
return nil, fmt.Errorf("invalid sample rate: %d", pkt.SampleRate)
}
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)
channelConfig := func() int {
for i, co := range channelCounts {
if co == pkt.ChannelCount {
return i + 1
}
}
return -1
}()
if channelConfig == -1 {
return nil, fmt.Errorf("invalid channel count: %d", pkt.ChannelCount)
}
frameLen := len(pkt.AU) + 7
fullness := 0x07FF // like ffmpeg does
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[2] = uint8(((pkt.Type - 1) << 6) | (sampleRateIndex << 2) | ((channelConfig >> 2) & 0x01))
header[3] = uint8((channelConfig&0x03)<<6 | (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...)
ret = append(ret, pkt.AU...)
}
return ret, nil

17
internal/aac/adts_test.go

@ -13,28 +13,35 @@ var casesADTS = []struct { @@ -13,28 +13,35 @@ var casesADTS = []struct {
}{
{
"single",
[]byte{0xff, 0xf1, 0xc, 0x80, 0x1, 0x3c, 0x20, 0xaa, 0xbb},
[]byte{0xff, 0xf1, 0x4c, 0x80, 0x1, 0x3f, 0xfc, 0xaa, 0xbb},
[]*ADTSPacket{
{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
Frame: []byte{0xaa, 0xbb},
AU: []byte{0xaa, 0xbb},
},
},
},
{
"multiple",
[]byte{0xff, 0xf1, 0x10, 0x40, 0x1, 0x3c, 0x20, 0xaa, 0xbb, 0xff, 0xf1, 0xc, 0x80, 0x1, 0x3c, 0x20, 0xcc, 0xdd},
[]byte{
0xff, 0xf1, 0x50, 0x40, 0x1, 0x3f, 0xfc, 0xaa,
0xbb, 0xff, 0xf1, 0x4c, 0x80, 0x1, 0x3f, 0xfc,
0xcc, 0xdd,
},
[]*ADTSPacket{
{
Type: 2,
SampleRate: 44100,
ChannelCount: 1,
Frame: []byte{0xaa, 0xbb},
AU: []byte{0xaa, 0xbb},
},
{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
Frame: []byte{0xcc, 0xdd},
AU: []byte{0xcc, 0xdd},
},
},
},

23
internal/conf/path.go

@ -192,6 +192,29 @@ func (pconf *PathConf) checkAndFillMissing(name string) error { @@ -192,6 +192,29 @@ func (pconf *PathConf) checkAndFillMissing(name string) error {
}
}
case strings.HasPrefix(pconf.Source, "http://") ||
strings.HasPrefix(pconf.Source, "https://"):
if pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source; use another path")
}
u, err := url.Parse(pconf.Source)
if err != nil {
return fmt.Errorf("'%s' is not a valid HLS URL", pconf.Source)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("'%s' is not a valid HLS URL", pconf.Source)
}
if u.User != nil {
pass, _ := u.User.Password()
user := u.User.Username()
if user != "" && pass == "" ||
user == "" && pass != "" {
return fmt.Errorf("username and password must be both provided")
}
}
case pconf.Source == "redirect":
if pconf.SourceRedirect == "" {
return fmt.Errorf("source redirect must be filled")

166
internal/core/hls_source.go

@ -0,0 +1,166 @@ @@ -0,0 +1,166 @@
package core
import (
"context"
"sync"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/rtsp-simple-server/internal/hls"
"github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/aler9/rtsp-simple-server/internal/rtcpsenderset"
)
const (
hlsSourceRetryPause = 5 * time.Second
)
type hlsSourceParent interface {
Log(logger.Level, string, ...interface{})
OnSourceStaticSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes
OnSourceStaticSetNotReady(req pathSourceStaticSetNotReadyReq)
}
type hlsSource struct {
ur string
wg *sync.WaitGroup
parent hlsSourceParent
ctx context.Context
ctxCancel func()
}
func newHLSSource(
parentCtx context.Context,
ur string,
wg *sync.WaitGroup,
parent hlsSourceParent) *hlsSource {
ctx, ctxCancel := context.WithCancel(parentCtx)
s := &hlsSource{
ur: ur,
wg: wg,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
}
s.Log(logger.Info, "started")
s.wg.Add(1)
go s.run()
return s
}
func (s *hlsSource) Close() {
s.Log(logger.Info, "stopped")
s.ctxCancel()
}
func (s *hlsSource) Log(level logger.Level, format string, args ...interface{}) {
s.parent.Log(level, "[hls source] "+format, args...)
}
func (s *hlsSource) run() {
defer s.wg.Done()
outer:
for {
ok := s.runInner()
if !ok {
break outer
}
select {
case <-time.After(hlsSourceRetryPause):
case <-s.ctx.Done():
break outer
}
}
s.ctxCancel()
}
func (s *hlsSource) runInner() bool {
var stream *stream
var rtcpSenders *rtcpsenderset.RTCPSenderSet
var videoTrackID int
var audioTrackID int
defer func() {
if stream != nil {
s.parent.OnSourceStaticSetNotReady(pathSourceStaticSetNotReadyReq{Source: s})
rtcpSenders.Close()
}
}()
onTracks := func(videoTrack *gortsplib.Track, audioTrack *gortsplib.Track) error {
var tracks gortsplib.Tracks
if videoTrack != nil {
videoTrackID = len(tracks)
tracks = append(tracks, videoTrack)
}
if audioTrack != nil {
audioTrackID = len(tracks)
tracks = append(tracks, audioTrack)
}
res := s.parent.OnSourceStaticSetReady(pathSourceStaticSetReadyReq{
Source: s,
Tracks: tracks,
})
if res.Err != nil {
return res.Err
}
s.Log(logger.Info, "ready")
stream = res.Stream
rtcpSenders = rtcpsenderset.New(tracks, stream.onFrame)
return nil
}
onFrame := func(isVideo bool, payload []byte) {
var trackID int
if isVideo {
trackID = videoTrackID
} else {
trackID = audioTrackID
}
if stream != nil {
rtcpSenders.OnFrame(trackID, gortsplib.StreamTypeRTP, payload)
stream.onFrame(trackID, gortsplib.StreamTypeRTP, payload)
}
}
c := hls.NewClient(
s.ur,
onTracks,
onFrame,
s,
)
select {
case err := <-c.Wait():
s.Log(logger.Info, "ERR: %v", err)
return true
case <-s.ctx.Done():
c.Close()
<-c.Wait()
return false
}
}
// OnSourceAPIDescribe implements source.
func (*hlsSource) OnSourceAPIDescribe() interface{} {
return struct {
Type string `json:"type"`
}{"hlsSource"}
}

160
internal/core/hls_source_test.go

@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
package core
import (
"bytes"
"context"
"io"
"net"
"net/http"
"sync/atomic"
"testing"
"time"
"github.com/aler9/gortsplib"
"github.com/asticode/go-astits"
"github.com/gin-gonic/gin"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
"github.com/aler9/rtsp-simple-server/internal/h264"
)
type testHLSServer struct {
s *http.Server
}
func newTestHLSServer() (*testHLSServer, error) {
ln, err := net.Listen("tcp", "localhost:5780")
if err != nil {
return nil, err
}
ts := &testHLSServer{}
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.GET("/stream.m3u8", ts.onPlaylist)
router.GET("/segment.ts", ts.onSegment)
ts.s = &http.Server{Handler: router}
go ts.s.Serve(ln)
return ts, nil
}
func (ts *testHLSServer) close() {
ts.s.Shutdown(context.Background())
}
func (ts *testHLSServer) onPlaylist(ctx *gin.Context) {
cnt := `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2,
segment.ts
`
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`)
io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt)))
}
func (ts *testHLSServer) onSegment(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
mux := astits.NewMuxer(context.Background(), ctx.Writer)
mux.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
mux.SetPCRPID(256)
mux.WriteTables()
enc, _ := h264.EncodeAnnexB([][]byte{
{7, 1, 2, 3}, // SPS
{8}, // PPS
})
mux.WriteData(&astits.MuxerData{
PID: 256,
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: &astits.PESOptionalHeader{
MarkerBits: 2,
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS,
PTS: &astits.ClockReference{Base: int64(1 * 90000)},
},
StreamID: 224, // = video
},
Data: enc,
},
})
ctx.Writer.(http.Flusher).Flush()
time.Sleep(1 * time.Second)
enc, _ = h264.EncodeAnnexB([][]byte{
{5}, // IDR
})
mux.WriteData(&astits.MuxerData{
PID: 256,
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: &astits.PESOptionalHeader{
MarkerBits: 2,
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS,
PTS: &astits.ClockReference{Base: int64(2 * 90000)},
},
StreamID: 224, // = video
},
Data: enc,
},
})
}
func TestHLSSource(t *testing.T) {
ts, err := newTestHLSServer()
require.NoError(t, err)
defer ts.close()
p, ok := newInstance("hlsDisable: yes\n" +
"rtmpDisable: yes\n" +
"paths:\n" +
" proxied:\n" +
" source: http://localhost:5780/stream.m3u8\n" +
" sourceOnDemand: yes\n")
require.Equal(t, true, ok)
defer p.close()
time.Sleep(1 * time.Second)
dest, err := gortsplib.DialRead("rtsp://localhost:8554/proxied")
require.NoError(t, err)
rtcpRecv := int64(0)
readDone := make(chan struct{})
frameRecv := make(chan struct{})
go func() {
defer close(readDone)
dest.ReadFrames(func(trackID int, streamType gortsplib.StreamType, payload []byte) {
if atomic.SwapInt64(&rtcpRecv, 1) == 0 {
} else {
require.Equal(t, gortsplib.StreamTypeRTP, streamType)
var pkt rtp.Packet
err := pkt.Unmarshal(payload)
require.NoError(t, err)
require.Equal(t, []byte{0x05}, pkt.Payload)
close(frameRecv)
}
})
}()
<-frameRecv
dest.Close()
<-readDone
}

18
internal/core/path.go

@ -465,7 +465,9 @@ outer: @@ -465,7 +465,9 @@ outer:
func (pa *path) hasStaticSource() bool {
return strings.HasPrefix(pa.conf.Source, "rtsp://") ||
strings.HasPrefix(pa.conf.Source, "rtsps://") ||
strings.HasPrefix(pa.conf.Source, "rtmp://")
strings.HasPrefix(pa.conf.Source, "rtmp://") ||
strings.HasPrefix(pa.conf.Source, "http://") ||
strings.HasPrefix(pa.conf.Source, "https://")
}
func (pa *path) isOnDemand() bool {
@ -578,8 +580,9 @@ func (pa *path) sourceSetNotReady() { @@ -578,8 +580,9 @@ func (pa *path) sourceSetNotReady() {
}
func (pa *path) staticSourceCreate() {
if strings.HasPrefix(pa.conf.Source, "rtsp://") ||
strings.HasPrefix(pa.conf.Source, "rtsps://") {
switch {
case strings.HasPrefix(pa.conf.Source, "rtsp://") ||
strings.HasPrefix(pa.conf.Source, "rtsps://"):
pa.source = newRTSPSource(
pa.ctx,
pa.conf.Source,
@ -592,7 +595,7 @@ func (pa *path) staticSourceCreate() { @@ -592,7 +595,7 @@ func (pa *path) staticSourceCreate() {
pa.readBufferSize,
&pa.sourceStaticWg,
pa)
} else if strings.HasPrefix(pa.conf.Source, "rtmp://") {
case strings.HasPrefix(pa.conf.Source, "rtmp://"):
pa.source = newRTMPSource(
pa.ctx,
pa.conf.Source,
@ -600,6 +603,13 @@ func (pa *path) staticSourceCreate() { @@ -600,6 +603,13 @@ func (pa *path) staticSourceCreate() {
pa.writeTimeout,
&pa.sourceStaticWg,
pa)
case strings.HasPrefix(pa.conf.Source, "http://") ||
strings.HasPrefix(pa.conf.Source, "https://"):
pa.source = newHLSSource(
pa.ctx,
pa.conf.Source,
&pa.sourceStaticWg,
pa)
}
}

53
internal/core/rtmp_source_test.go

@ -13,34 +13,31 @@ func TestRTMPSource(t *testing.T) { @@ -13,34 +13,31 @@ func TestRTMPSource(t *testing.T) {
"video",
} {
t.Run(source, func(t *testing.T) {
switch source {
case "videoaudio", "video":
cnt1, err := newContainer("nginx-rtmp", "rtmpserver", []string{})
require.NoError(t, err)
defer cnt1.close()
cnt2, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "empty" + source + ".mkv",
"-c", "copy",
"-f", "flv",
"rtmp://" + cnt1.ip() + "/stream/test",
})
require.NoError(t, err)
defer cnt2.close()
time.Sleep(1 * time.Second)
p, ok := newInstance("hlsDisable: yes\n" +
"rtmpDisable: yes\n" +
"paths:\n" +
" proxied:\n" +
" source: rtmp://localhost/stream/test\n" +
" sourceOnDemand: yes\n")
require.Equal(t, true, ok)
defer p.close()
}
cnt1, err := newContainer("nginx-rtmp", "rtmpserver", []string{})
require.NoError(t, err)
defer cnt1.close()
cnt2, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "empty" + source + ".mkv",
"-c", "copy",
"-f", "flv",
"rtmp://" + cnt1.ip() + "/stream/test",
})
require.NoError(t, err)
defer cnt2.close()
time.Sleep(1 * time.Second)
p, ok := newInstance("hlsDisable: yes\n" +
"rtmpDisable: yes\n" +
"paths:\n" +
" proxied:\n" +
" source: rtmp://localhost/stream/test\n" +
" sourceOnDemand: yes\n")
require.Equal(t, true, ok)
defer p.close()
time.Sleep(1 * time.Second)

892
internal/hls/client.go

@ -0,0 +1,892 @@ @@ -0,0 +1,892 @@
package hls
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
gopath "path"
"strings"
"sync"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/rtpaac"
"github.com/aler9/gortsplib/pkg/rtph264"
"github.com/asticode/go-astits"
"github.com/grafov/m3u8"
"github.com/aler9/rtsp-simple-server/internal/aac"
"github.com/aler9/rtsp-simple-server/internal/h264"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
const (
clientMinDownloadPause = 5 * time.Second
clientQueueSize = 100
clientMinSegmentsBeforeDownloading = 2
)
func clientURLAbsolute(base *url.URL, relative string) (*url.URL, error) {
u, err := url.Parse(relative)
if err != nil {
return nil, err
}
if !u.IsAbs() {
u = &url.URL{
Scheme: base.Scheme,
User: base.User,
Host: base.Host,
Path: gopath.Join(gopath.Dir(base.Path), relative),
}
}
return u, nil
}
type clientSegmentQueue struct {
mutex sync.Mutex
queue [][]byte
didPush chan struct{}
didPull chan struct{}
}
func newClientSegmentQueue() *clientSegmentQueue {
return &clientSegmentQueue{
didPush: make(chan struct{}),
didPull: make(chan struct{}),
}
}
func (q *clientSegmentQueue) push(seg []byte) {
q.mutex.Lock()
queueWasEmpty := (len(q.queue) == 0)
q.queue = append(q.queue, seg)
if queueWasEmpty {
close(q.didPush)
q.didPush = make(chan struct{})
}
q.mutex.Unlock()
}
func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) {
q.mutex.Lock()
for len(q.queue) > n {
q.mutex.Unlock()
select {
case <-q.didPull:
case <-ctx.Done():
return
}
q.mutex.Lock()
}
q.mutex.Unlock()
}
func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) {
q.mutex.Lock()
for len(q.queue) == 0 {
q.mutex.Unlock()
select {
case <-q.didPush:
case <-ctx.Done():
return nil, fmt.Errorf("terminated")
}
q.mutex.Lock()
}
var seg []byte
seg, q.queue = q.queue[0], q.queue[1:]
close(q.didPull)
q.didPull = make(chan struct{})
q.mutex.Unlock()
return seg, nil
}
type clientAllocateProcsReq struct {
res chan struct{}
}
type clientVideoProcessorData struct {
data []byte
pts time.Duration
dts time.Duration
}
type clientVideoProcessor struct {
ctx context.Context
onTrack func(*gortsplib.Track) error
onFrame func([]byte)
queue chan clientVideoProcessorData
sps []byte
pps []byte
encoder *rtph264.Encoder
clockStartRTC time.Time
}
func newClientVideoProcessor(
ctx context.Context,
onTrack func(*gortsplib.Track) error,
onFrame func([]byte),
) *clientVideoProcessor {
p := &clientVideoProcessor{
ctx: ctx,
onTrack: onTrack,
onFrame: onFrame,
queue: make(chan clientVideoProcessorData, clientQueueSize),
}
return p
}
func (p *clientVideoProcessor) run() error {
for {
select {
case item := <-p.queue:
err := p.doProcess(item.data, item.pts, item.dts)
if err != nil {
return err
}
case <-p.ctx.Done():
return nil
}
}
}
func (p *clientVideoProcessor) doProcess(
data []byte,
pts time.Duration,
dts time.Duration) error {
elapsed := time.Since(p.clockStartRTC)
if dts > elapsed {
select {
case <-p.ctx.Done():
return fmt.Errorf("terminated")
case <-time.After(dts - elapsed):
}
}
nalus, err := h264.DecodeAnnexB(data)
if err != nil {
return err
}
outNALUs := make([][]byte, 0, len(nalus))
for _, nalu := range nalus {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
if p.sps == nil {
p.sps = append([]byte(nil), nalu...)
if p.encoder == nil && p.pps != nil {
err := p.initializeTrack()
if err != nil {
return err
}
}
}
// remove since it's not needed
continue
case h264.NALUTypePPS:
if p.pps == nil {
p.pps = append([]byte(nil), nalu...)
if p.encoder == nil && p.sps != nil {
err := p.initializeTrack()
if err != nil {
return err
}
}
}
// remove since it's not needed
continue
case h264.NALUTypeAccessUnitDelimiter:
// remove since it's not needed
continue
}
outNALUs = append(outNALUs, nalu)
}
if len(outNALUs) == 0 {
return nil
}
if p.encoder == nil {
return nil
}
pkts, err := p.encoder.Encode(outNALUs, pts)
if err != nil {
return fmt.Errorf("error while encoding H264: %v", err)
}
for _, pkt := range pkts {
p.onFrame(pkt)
}
return nil
}
func (p *clientVideoProcessor) process(
data []byte,
pts time.Duration,
dts time.Duration) {
p.queue <- clientVideoProcessorData{data, pts, dts}
}
func (p *clientVideoProcessor) initializeTrack() error {
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{SPS: p.sps, PPS: p.pps})
if err != nil {
return err
}
p.encoder = rtph264.NewEncoder(96, nil, nil, nil)
return p.onTrack(track)
}
type clientAudioProcessorData struct {
data []byte
pts time.Duration
}
type clientAudioProcessor struct {
ctx context.Context
onTrack func(*gortsplib.Track) error
onFrame func([]byte)
queue chan clientAudioProcessorData
conf *gortsplib.TrackConfigAAC
encoder *rtpaac.Encoder
clockStartRTC time.Time
}
func newClientAudioProcessor(
ctx context.Context,
onTrack func(*gortsplib.Track) error,
onFrame func([]byte),
) *clientAudioProcessor {
p := &clientAudioProcessor{
ctx: ctx,
onTrack: onTrack,
onFrame: onFrame,
queue: make(chan clientAudioProcessorData, clientQueueSize),
}
return p
}
func (p *clientAudioProcessor) run() error {
for {
select {
case item := <-p.queue:
err := p.doProcess(item.data, item.pts)
if err != nil {
return err
}
case <-p.ctx.Done():
return nil
}
}
}
func (p *clientAudioProcessor) doProcess(
data []byte,
pts time.Duration) error {
adtsPkts, err := aac.DecodeADTS(data)
if err != nil {
return err
}
aus := make([][]byte, 0, len(adtsPkts))
pktPts := pts
now := time.Now()
for _, pkt := range adtsPkts {
elapsed := now.Sub(p.clockStartRTC)
if pktPts > elapsed {
select {
case <-p.ctx.Done():
return fmt.Errorf("terminated")
case <-time.After(pktPts - elapsed):
}
}
if p.conf == nil {
p.conf = &gortsplib.TrackConfigAAC{
Type: pkt.Type,
SampleRate: pkt.SampleRate,
ChannelCount: pkt.ChannelCount,
}
if p.encoder == nil {
err := p.initializeTrack()
if err != nil {
return err
}
}
}
aus = append(aus, pkt.AU)
pktPts += 1000 * time.Second / time.Duration(pkt.SampleRate)
}
if p.encoder == nil {
return nil
}
pkts, err := p.encoder.Encode(aus, pts)
if err != nil {
return fmt.Errorf("error while encoding AAC: %v", err)
}
for _, pkt := range pkts {
p.onFrame(pkt)
}
return nil
}
func (p *clientAudioProcessor) process(
data []byte,
pts time.Duration) {
select {
case p.queue <- clientAudioProcessorData{data, pts}:
case <-p.ctx.Done():
}
}
func (p *clientAudioProcessor) initializeTrack() error {
track, err := gortsplib.NewTrackAAC(97, p.conf)
if err != nil {
return err
}
p.encoder = rtpaac.NewEncoder(97, p.conf.SampleRate, nil, nil, nil)
return p.onTrack(track)
}
// ClientParent is the parent of a Client.
type ClientParent interface {
Log(level logger.Level, format string, args ...interface{})
}
// Client is a HLS client.
type Client struct {
ur string
onTracks func(*gortsplib.Track, *gortsplib.Track) error
onFrame func(bool, []byte)
parent ClientParent
ctx context.Context
ctxCancel func()
urlParsed *url.URL
lastDownloadTime time.Time
downloadedSegmentURIs []string
segmentQueue *clientSegmentQueue
pmtDownloaded bool
clockInitialized bool
clockStartPTS time.Duration
videoPID *uint16
audioPID *uint16
videoProc *clientVideoProcessor
audioProc *clientAudioProcessor
tracksMutex sync.RWMutex
videoTrack *gortsplib.Track
audioTrack *gortsplib.Track
// in
allocateProcs chan clientAllocateProcsReq
// out
outErr chan error
}
// NewClient allocates a Client.
func NewClient(
ur string,
onTracks func(*gortsplib.Track, *gortsplib.Track) error,
onFrame func(bool, []byte),
parent ClientParent,
) *Client {
ctx, ctxCancel := context.WithCancel(context.Background())
c := &Client{
ur: ur,
onTracks: onTracks,
onFrame: onFrame,
parent: parent,
ctx: ctx,
ctxCancel: ctxCancel,
segmentQueue: newClientSegmentQueue(),
allocateProcs: make(chan clientAllocateProcsReq),
outErr: make(chan error, 1),
}
go c.run()
return c
}
func (c *Client) log(level logger.Level, format string, args ...interface{}) {
c.parent.Log(level, format, args...)
}
// Close closes all the Client resources.
func (c *Client) Close() {
c.ctxCancel()
}
// Wait waits for any error of the Client.
func (c *Client) Wait() chan error {
return c.outErr
}
func (c *Client) run() {
c.outErr <- c.runInner()
}
func (c *Client) runInner() error {
innerCtx, innerCtxCancel := context.WithCancel(context.Background())
errChan := make(chan error)
go func() { errChan <- c.runDownloader(innerCtx) }()
go func() { errChan <- c.runProcessor(innerCtx) }()
for {
select {
case req := <-c.allocateProcs:
if c.videoPID != nil {
c.videoProc = newClientVideoProcessor(
innerCtx,
c.onVideoTrack,
c.onVideoFrame)
go func() { errChan <- c.videoProc.run() }()
}
if c.audioPID != nil {
c.audioProc = newClientAudioProcessor(
innerCtx,
c.onAudioTrack,
c.onAudioFrame)
go func() { errChan <- c.audioProc.run() }()
}
close(req.res)
case err := <-errChan:
innerCtxCancel()
<-errChan
if c.videoProc != nil {
<-errChan
}
if c.audioProc != nil {
<-errChan
}
return err
case <-c.ctx.Done():
innerCtxCancel()
<-errChan
<-errChan
if c.videoProc != nil {
<-errChan
}
if c.audioProc != nil {
<-errChan
}
return fmt.Errorf("terminated")
}
}
}
func (c *Client) runDownloader(innerCtx context.Context) error {
for {
c.segmentQueue.waitUntilSizeIsBelow(innerCtx, clientMinSegmentsBeforeDownloading)
_, err := c.fillSegmentQueue(innerCtx)
if err != nil {
return err
}
}
}
func (c *Client) fillSegmentQueue(innerCtx context.Context) (bool, error) {
minTime := c.lastDownloadTime.Add(clientMinDownloadPause)
now := time.Now()
if now.Before(minTime) {
select {
case <-time.After(minTime.Sub(now)):
case <-innerCtx.Done():
return false, fmt.Errorf("terminated")
}
}
c.lastDownloadTime = now
pl, err := func() (*m3u8.MediaPlaylist, error) {
if c.urlParsed == nil {
return c.downloadPrimaryPlaylist(innerCtx)
}
return c.downloadStreamPlaylist(innerCtx)
}()
if err != nil {
return false, err
}
added := false
for _, seg := range pl.Segments {
if seg == nil {
break
}
if !c.segmentWasDownloaded(seg.URI) {
c.downloadedSegmentURIs = append(c.downloadedSegmentURIs, seg.URI)
byts, err := c.downloadSegment(innerCtx, seg.URI)
if err != nil {
return false, err
}
c.segmentQueue.push(byts)
added = true
}
}
return added, nil
}
func (c *Client) segmentWasDownloaded(ur string) bool {
for _, q := range c.downloadedSegmentURIs {
if q == ur {
return true
}
}
return false
}
func (c *Client) downloadPrimaryPlaylist(innerCtx context.Context) (*m3u8.MediaPlaylist, error) {
c.log(logger.Debug, "downloading primary playlist %s", c.ur)
var err error
c.urlParsed, err = url.Parse(c.ur)
if err != nil {
return nil, err
}
pl, err := c.downloadPlaylist(innerCtx)
if err != nil {
return nil, err
}
switch plt := pl.(type) {
case *m3u8.MediaPlaylist:
return plt, nil
case *m3u8.MasterPlaylist:
// choose the variant with the highest bandwidth
var chosenVariant *m3u8.Variant
for _, v := range plt.Variants {
if chosenVariant == nil ||
v.VariantParams.Bandwidth > chosenVariant.VariantParams.Bandwidth {
chosenVariant = v
}
}
if chosenVariant == nil {
return nil, fmt.Errorf("no variants found")
}
u, err := clientURLAbsolute(c.urlParsed, chosenVariant.URI)
if err != nil {
return nil, err
}
c.urlParsed = u
return c.downloadStreamPlaylist(innerCtx)
default:
return nil, fmt.Errorf("invalid playlist")
}
}
func (c *Client) downloadStreamPlaylist(innerCtx context.Context) (*m3u8.MediaPlaylist, error) {
c.log(logger.Debug, "downloading stream playlist %s", c.urlParsed.String())
pl, err := c.downloadPlaylist(innerCtx)
if err != nil {
return nil, err
}
plt, ok := pl.(*m3u8.MediaPlaylist)
if !ok {
return nil, fmt.Errorf("invalid playlist")
}
return plt, nil
}
func (c *Client) downloadPlaylist(innerCtx context.Context) (m3u8.Playlist, error) {
req, err := http.NewRequestWithContext(innerCtx, http.MethodGet, c.urlParsed.String(), nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
pl, _, err := m3u8.DecodeFrom(res.Body, true)
if err != nil {
return nil, err
}
return pl, nil
}
func (c *Client) downloadSegment(innerCtx context.Context, segmentURI string) ([]byte, error) {
u, err := clientURLAbsolute(c.urlParsed, segmentURI)
if err != nil {
return nil, err
}
c.log(logger.Debug, "downloading segment %s", u)
req, err := http.NewRequestWithContext(innerCtx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
byts, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return byts, nil
}
func (c *Client) runProcessor(innerCtx context.Context) error {
for {
seg, err := c.segmentQueue.waitAndPull(innerCtx)
if err != nil {
return err
}
err = c.processSegment(innerCtx, seg)
if err != nil {
return err
}
}
}
func (c *Client) processSegment(innerCtx context.Context, byts []byte) error {
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts))
// parse PMT
if !c.pmtDownloaded {
for {
data, err := dem.NextData()
if err != nil {
if err == astits.ErrNoMorePackets {
return nil
}
return err
}
if data.PMT != nil {
c.pmtDownloaded = true
for _, e := range data.PMT.ElementaryStreams {
switch e.StreamType {
case astits.StreamTypeH264Video:
if c.videoPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
c.videoPID = &v
case astits.StreamTypeAACAudio:
if c.audioPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
c.audioPID = &v
}
}
break
}
}
if c.videoPID == nil && c.audioPID == nil {
return fmt.Errorf("stream doesn't contain tracks with supported codecs (H264 or AAC)")
}
res := make(chan struct{})
select {
case c.allocateProcs <- clientAllocateProcsReq{res}:
<-res
case <-innerCtx.Done():
return nil
}
}
// process PES packets
for {
data, err := dem.NextData()
if err != nil {
if err == astits.ErrNoMorePackets {
return nil
}
if strings.HasPrefix(err.Error(), "astits: parsing PES data failed") {
continue
}
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")
}
pts := time.Duration(float64(data.PES.Header.OptionalHeader.PTS.Base) * float64(time.Second) / 90000)
if !c.clockInitialized {
c.clockInitialized = true
c.clockStartPTS = pts
now := time.Now()
if c.videoPID != nil {
c.videoProc.clockStartRTC = now
}
if c.audioPID != nil {
c.audioProc.clockStartRTC = now
}
}
if c.videoPID != nil && data.PID == *c.videoPID {
var dts time.Duration
if data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent {
dts = time.Duration(float64(data.PES.Header.OptionalHeader.DTS.Base) * float64(time.Second) / 90000)
} else {
dts = pts
}
pts -= c.clockStartPTS
dts -= c.clockStartPTS
c.videoProc.process(data.PES.Data, pts, dts)
} else if c.audioPID != nil && data.PID == *c.audioPID {
pts -= c.clockStartPTS
c.audioProc.process(data.PES.Data, pts)
}
}
}
func (c *Client) onVideoTrack(track *gortsplib.Track) error {
c.tracksMutex.Lock()
defer c.tracksMutex.Unlock()
c.videoTrack = track
if c.audioPID == nil || c.audioTrack != nil {
return c.initializeTracks()
}
return nil
}
func (c *Client) onAudioTrack(track *gortsplib.Track) error {
c.tracksMutex.Lock()
defer c.tracksMutex.Unlock()
c.audioTrack = track
if c.videoPID == nil || c.videoTrack != nil {
return c.initializeTracks()
}
return nil
}
func (c *Client) initializeTracks() error {
return c.onTracks(c.videoTrack, c.audioTrack)
}
func (c *Client) onVideoFrame(payload []byte) {
c.tracksMutex.RLock()
defer c.tracksMutex.RUnlock()
c.onFrame(true, payload)
}
func (c *Client) onAudioFrame(payload []byte) {
c.tracksMutex.RLock()
defer c.tracksMutex.RUnlock()
c.onFrame(false, payload)
}

128
internal/hls/client_test.go

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
package hls
import (
"bytes"
"context"
"io"
"net"
"net/http"
"testing"
"github.com/aler9/gortsplib"
"github.com/asticode/go-astits"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/aler9/rtsp-simple-server/internal/h264"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
type testHLSServer struct {
s *http.Server
}
func newTestHLSServer() (*testHLSServer, error) {
ln, err := net.Listen("tcp", "localhost:5780")
if err != nil {
return nil, err
}
ts := &testHLSServer{}
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.GET("/stream.m3u8", ts.onPlaylist)
router.GET("/segment.ts", ts.onSegment)
ts.s = &http.Server{Handler: router}
go ts.s.Serve(ln)
return ts, nil
}
func (ts *testHLSServer) close() {
ts.s.Shutdown(context.Background())
}
func (ts *testHLSServer) onPlaylist(ctx *gin.Context) {
cnt := `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2,
segment.ts
`
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`)
io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt)))
}
func (ts *testHLSServer) onSegment(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
mux := astits.NewMuxer(context.Background(), ctx.Writer)
mux.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
mux.SetPCRPID(256)
mux.WriteTables()
enc, _ := h264.EncodeAnnexB([][]byte{
{7, 1, 2, 3}, // SPS
{8}, // PPS
{5}, // IDR
})
mux.WriteData(&astits.MuxerData{
PID: 256,
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: &astits.PESOptionalHeader{
MarkerBits: 2,
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS,
PTS: &astits.ClockReference{Base: int64(1 * 90000)},
},
StreamID: 224, // = video
},
Data: enc,
},
})
}
type testClientParent struct{}
func (testClientParent) Log(level logger.Level, format string, args ...interface{}) {}
func TestClient(t *testing.T) {
ts, err := newTestHLSServer()
require.NoError(t, err)
defer ts.close()
onTracks := func(*gortsplib.Track, *gortsplib.Track) error {
return nil
}
frameRecv := make(chan struct{})
onFrame := func(isVideo bool, byts []byte) {
require.Equal(t, true, isVideo)
require.Equal(t, byte(0x05), byts[12])
close(frameRecv)
}
c := NewClient(
"http://localhost:5780/stream.m3u8",
onTracks,
onFrame,
testClientParent{},
)
<-frameRecv
c.Close()
c.Wait()
}

2
internal/hls/segment.go

@ -198,7 +198,7 @@ func (t *segment) writeAAC( @@ -198,7 +198,7 @@ func (t *segment) writeAAC(
{
SampleRate: t.aacConf.SampleRate,
ChannelCount: t.aacConf.ChannelCount,
Frame: au,
AU: au,
},
})
if err != nil {

3
rtsp-simple-server.yml

@ -128,7 +128,8 @@ paths: @@ -128,7 +128,8 @@ paths:
# * publisher -> the stream is published by a RTSP or RTMP client
# * rtsp://existing-url -> the stream is pulled from another RTSP server
# * rtsps://existing-url -> the stream is pulled from another RTSP server, with RTSPS
# * rtmp://existing-url -> the stream is pulled from a RTMP server
# * rtmp://existing-url -> the stream is pulled from another RTMP server
# * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server
# * redirect -> the stream is provided by another path or server
source: publisher

Loading…
Cancel
Save