28 changed files with 983 additions and 890 deletions
@ -1,222 +0,0 @@
@@ -1,222 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"io" |
||||
"net" |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v4" |
||||
"github.com/bluenviron/gortsplib/v4/pkg/description" |
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/pion/rtp" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
type testHTTPAuthenticator struct { |
||||
*http.Server |
||||
} |
||||
|
||||
func newTestHTTPAuthenticator(t *testing.T, protocol string, action string) *testHTTPAuthenticator { |
||||
firstReceived := false |
||||
|
||||
ts := &testHTTPAuthenticator{} |
||||
|
||||
ts.Server = &http.Server{ |
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
require.Equal(t, http.MethodPost, r.Method) |
||||
require.Equal(t, "/auth", r.URL.Path) |
||||
|
||||
var in struct { |
||||
IP string `json:"ip"` |
||||
User string `json:"user"` |
||||
Password string `json:"password"` |
||||
Path string `json:"path"` |
||||
Protocol string `json:"protocol"` |
||||
ID string `json:"id"` |
||||
Action string `json:"action"` |
||||
Query string `json:"query"` |
||||
} |
||||
err := json.NewDecoder(r.Body).Decode(&in) |
||||
require.NoError(t, err) |
||||
|
||||
var user string |
||||
if action == "publish" { |
||||
user = "testpublisher" |
||||
} else { |
||||
user = "testreader" |
||||
} |
||||
|
||||
if in.IP != "127.0.0.1" || |
||||
in.User != user || |
||||
in.Password != "testpass" || |
||||
in.Path != "teststream" || |
||||
in.Protocol != protocol || |
||||
(firstReceived && in.ID == "") || |
||||
in.Action != action || |
||||
(in.Query != "user=testreader&pass=testpass¶m=value" && |
||||
in.Query != "user=testpublisher&pass=testpass¶m=value" && |
||||
in.Query != "param=value") { |
||||
w.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
firstReceived = true |
||||
}), |
||||
} |
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:9120") |
||||
require.NoError(t, err) |
||||
|
||||
go ts.Server.Serve(ln) |
||||
|
||||
return ts |
||||
} |
||||
|
||||
func (ts *testHTTPAuthenticator) close() { |
||||
ts.Server.Shutdown(context.Background()) |
||||
} |
||||
|
||||
func httpPullFile(t *testing.T, hc *http.Client, u string) []byte { |
||||
res, err := hc.Get(u) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
|
||||
if res.StatusCode != http.StatusOK { |
||||
t.Errorf("bad status code: %v", res.StatusCode) |
||||
} |
||||
|
||||
byts, err := io.ReadAll(res.Body) |
||||
require.NoError(t, err) |
||||
|
||||
return byts |
||||
} |
||||
|
||||
func TestHLSReadNotFound(t *testing.T) { |
||||
p, ok := newInstance("") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/stream/", nil) |
||||
require.NoError(t, err) |
||||
|
||||
hc := &http.Client{Transport: &http.Transport{}} |
||||
|
||||
res, err := hc.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
require.Equal(t, http.StatusNotFound, res.StatusCode) |
||||
} |
||||
|
||||
func TestHLSRead(t *testing.T) { |
||||
p, ok := newInstance("hlsAlwaysRemux: yes\n" + |
||||
"paths:\n" + |
||||
" all_others:\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
medi := &description.Media{ |
||||
Type: description.MediaTypeVideo, |
||||
Formats: []format.Format{&format.H264{ |
||||
PayloadTyp: 96, |
||||
PacketizationMode: 1, |
||||
SPS: []byte{ // 1920x1080 baseline
|
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, |
||||
}, |
||||
PPS: []byte{0x08, 0x06, 0x07, 0x08}, |
||||
}}, |
||||
} |
||||
|
||||
v := gortsplib.TransportTCP |
||||
source := gortsplib.Client{ |
||||
Transport: &v, |
||||
} |
||||
err := source.StartRecording("rtsp://localhost:8554/stream", |
||||
&description.Session{Medias: []*description.Media{medi}}) |
||||
require.NoError(t, err) |
||||
defer source.Close() |
||||
|
||||
time.Sleep(500 * time.Millisecond) |
||||
|
||||
for i := 0; i < 2; i++ { |
||||
err = source.WritePacketRTP(medi, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 123 + uint16(i), |
||||
Timestamp: 45343 + uint32(i*90000), |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{ |
||||
0x05, 0x02, 0x03, 0x04, // IDR
|
||||
}, |
||||
}) |
||||
require.NoError(t, err) |
||||
} |
||||
|
||||
hc := &http.Client{Transport: &http.Transport{}} |
||||
|
||||
cnt := httpPullFile(t, hc, "http://localhost:8888/stream/index.m3u8") |
||||
require.Equal(t, "#EXTM3U\n"+ |
||||
"#EXT-X-VERSION:9\n"+ |
||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
||||
"\n"+ |
||||
"#EXT-X-STREAM-INF:BANDWIDTH=1192,AVERAGE-BANDWIDTH=1192,"+ |
||||
"CODECS=\"avc1.42c028\",RESOLUTION=1920x1080,FRAME-RATE=30.000\n"+ |
||||
"stream.m3u8\n", string(cnt)) |
||||
|
||||
cnt = httpPullFile(t, hc, "http://localhost:8888/stream/stream.m3u8") |
||||
require.Regexp(t, "#EXTM3U\n"+ |
||||
"#EXT-X-VERSION:9\n"+ |
||||
"#EXT-X-TARGETDURATION:1\n"+ |
||||
"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=2\\.50000,CAN-SKIP-UNTIL=6\\.00000\n"+ |
||||
"#EXT-X-PART-INF:PART-TARGET=1\\.00000\n"+ |
||||
"#EXT-X-MEDIA-SEQUENCE:1\n"+ |
||||
"#EXT-X-MAP:URI=\".*?_init.mp4\"\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-GAP\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
"gap.mp4\n"+ |
||||
"#EXT-X-PROGRAM-DATE-TIME:.+?\n"+ |
||||
"#EXT-X-PART:DURATION=1\\.00000,URI=\".*?_part0.mp4\",INDEPENDENT=YES\n"+ |
||||
"#EXTINF:1\\.00000,\n"+ |
||||
".*?_seg7.mp4\n"+ |
||||
"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\".*?_part1.mp4\"\n", string(cnt)) |
||||
|
||||
/*trak := <-c.track |
||||
|
||||
pkt, _, err := trak.ReadRTP() |
||||
require.NoError(t, err) |
||||
require.Equal(t, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 102, |
||||
SequenceNumber: pkt.SequenceNumber, |
||||
Timestamp: pkt.Timestamp, |
||||
SSRC: pkt.SSRC, |
||||
CSRC: []uint32{}, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}, pkt)*/ |
||||
} |
||||
@ -1,9 +1,14 @@
@@ -1,9 +1,14 @@
|
||||
package defs |
||||
|
||||
import ( |
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
) |
||||
|
||||
// PathManager is a path manager.
|
||||
type PathManager interface { |
||||
FindPathConf(req PathFindPathConfReq) PathFindPathConfRes |
||||
FindPathConf(req PathFindPathConfReq) (*conf.Path, error) |
||||
Describe(req PathDescribeReq) PathDescribeRes |
||||
AddPublisher(req PathAddPublisherReq) PathAddPublisherRes |
||||
AddReader(req PathAddReaderReq) PathAddReaderRes |
||||
AddPublisher(req PathAddPublisherReq) (Path, error) |
||||
AddReader(req PathAddReaderReq) (Path, *stream.Stream, error) |
||||
} |
||||
|
||||
@ -0,0 +1,278 @@
@@ -0,0 +1,278 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gohlslib" |
||||
"github.com/bluenviron/gohlslib/pkg/codecs" |
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter" |
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/defs" |
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
"github.com/bluenviron/mediamtx/internal/unit" |
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
type muxerInstance struct { |
||||
variant conf.HLSVariant |
||||
segmentCount int |
||||
segmentDuration conf.StringDuration |
||||
partDuration conf.StringDuration |
||||
segmentMaxSize conf.StringSize |
||||
directory string |
||||
writeQueueSize int |
||||
pathName string |
||||
stream *stream.Stream |
||||
bytesSent *uint64 |
||||
parent logger.Writer |
||||
|
||||
writer *asyncwriter.Writer |
||||
hmuxer *gohlslib.Muxer |
||||
} |
||||
|
||||
func (mi *muxerInstance) initialize() error { |
||||
mi.writer = asyncwriter.New(mi.writeQueueSize, mi) |
||||
|
||||
videoTrack := mi.createVideoTrack() |
||||
audioTrack := mi.createAudioTrack() |
||||
|
||||
if videoTrack == nil && audioTrack == nil { |
||||
mi.stream.RemoveReader(mi.writer) |
||||
return fmt.Errorf( |
||||
"the stream doesn't contain any supported codec, which are currently H265, H264, Opus, MPEG-4 Audio") |
||||
} |
||||
|
||||
var muxerDirectory string |
||||
if mi.directory != "" { |
||||
muxerDirectory = filepath.Join(mi.directory, mi.pathName) |
||||
os.MkdirAll(muxerDirectory, 0o755) |
||||
defer os.Remove(muxerDirectory) |
||||
} |
||||
|
||||
mi.hmuxer = &gohlslib.Muxer{ |
||||
Variant: gohlslib.MuxerVariant(mi.variant), |
||||
SegmentCount: mi.segmentCount, |
||||
SegmentDuration: time.Duration(mi.segmentDuration), |
||||
PartDuration: time.Duration(mi.partDuration), |
||||
SegmentMaxSize: uint64(mi.segmentMaxSize), |
||||
VideoTrack: videoTrack, |
||||
AudioTrack: audioTrack, |
||||
Directory: muxerDirectory, |
||||
} |
||||
|
||||
err := mi.hmuxer.Start() |
||||
if err != nil { |
||||
mi.stream.RemoveReader(mi.writer) |
||||
return err |
||||
} |
||||
|
||||
mi.Log(logger.Info, "is converting into HLS, %s", |
||||
defs.FormatsInfo(mi.stream.FormatsForReader(mi.writer))) |
||||
|
||||
mi.writer.Start() |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Log implements logger.Writer.
|
||||
func (mi *muxerInstance) Log(level logger.Level, format string, args ...interface{}) { |
||||
mi.parent.Log(level, format, args...) |
||||
} |
||||
|
||||
func (mi *muxerInstance) close() { |
||||
mi.writer.Stop() |
||||
mi.hmuxer.Close() |
||||
mi.stream.RemoveReader(mi.writer) |
||||
} |
||||
|
||||
func (mi *muxerInstance) createVideoTrack() *gohlslib.Track { |
||||
var videoFormatAV1 *format.AV1 |
||||
videoMedia := mi.stream.Desc().FindFormat(&videoFormatAV1) |
||||
|
||||
if videoFormatAV1 != nil { |
||||
mi.stream.AddReader(mi.writer, videoMedia, videoFormatAV1, func(u unit.Unit) error { |
||||
tunit := u.(*unit.AV1) |
||||
|
||||
if tunit.TU == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := mi.hmuxer.WriteAV1(tunit.NTP, tunit.PTS, tunit.TU) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.AV1{}, |
||||
} |
||||
} |
||||
|
||||
var videoFormatVP9 *format.VP9 |
||||
videoMedia = mi.stream.Desc().FindFormat(&videoFormatVP9) |
||||
|
||||
if videoFormatVP9 != nil { |
||||
mi.stream.AddReader(mi.writer, videoMedia, videoFormatVP9, func(u unit.Unit) error { |
||||
tunit := u.(*unit.VP9) |
||||
|
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := mi.hmuxer.WriteVP9(tunit.NTP, tunit.PTS, tunit.Frame) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.VP9{}, |
||||
} |
||||
} |
||||
|
||||
var videoFormatH265 *format.H265 |
||||
videoMedia = mi.stream.Desc().FindFormat(&videoFormatH265) |
||||
|
||||
if videoFormatH265 != nil { |
||||
mi.stream.AddReader(mi.writer, videoMedia, videoFormatH265, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H265) |
||||
|
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
vps, sps, pps := videoFormatH265.SafeParams() |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.H265{ |
||||
VPS: vps, |
||||
SPS: sps, |
||||
PPS: pps, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
var videoFormatH264 *format.H264 |
||||
videoMedia = mi.stream.Desc().FindFormat(&videoFormatH264) |
||||
|
||||
if videoFormatH264 != nil { |
||||
mi.stream.AddReader(mi.writer, videoMedia, videoFormatH264, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H264) |
||||
|
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
sps, pps := videoFormatH264.SafeParams() |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.H264{ |
||||
SPS: sps, |
||||
PPS: pps, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (mi *muxerInstance) createAudioTrack() *gohlslib.Track { |
||||
var audioFormatOpus *format.Opus |
||||
audioMedia := mi.stream.Desc().FindFormat(&audioFormatOpus) |
||||
|
||||
if audioMedia != nil { |
||||
mi.stream.AddReader(mi.writer, audioMedia, audioFormatOpus, func(u unit.Unit) error { |
||||
tunit := u.(*unit.Opus) |
||||
|
||||
err := mi.hmuxer.WriteOpus( |
||||
tunit.NTP, |
||||
tunit.PTS, |
||||
tunit.Packets) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.Opus{ |
||||
ChannelCount: func() int { |
||||
if audioFormatOpus.IsStereo { |
||||
return 2 |
||||
} |
||||
return 1 |
||||
}(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
var audioFormatMPEG4Audio *format.MPEG4Audio |
||||
audioMedia = mi.stream.Desc().FindFormat(&audioFormatMPEG4Audio) |
||||
|
||||
if audioMedia != nil { |
||||
mi.stream.AddReader(mi.writer, audioMedia, audioFormatMPEG4Audio, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG4Audio) |
||||
|
||||
if tunit.AUs == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := mi.hmuxer.WriteMPEG4Audio( |
||||
tunit.NTP, |
||||
tunit.PTS, |
||||
tunit.AUs) |
||||
if err != nil { |
||||
return fmt.Errorf("muxer error: %w", err) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
return &gohlslib.Track{ |
||||
Codec: &codecs.MPEG4Audio{ |
||||
Config: *audioFormatMPEG4Audio.GetConfig(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (mi *muxerInstance) errorChan() chan error { |
||||
return mi.writer.Error() |
||||
} |
||||
|
||||
func (mi *muxerInstance) handleRequest(ctx *gin.Context) { |
||||
w := &responseWriterWithCounter{ |
||||
ResponseWriter: ctx.Writer, |
||||
bytesSent: mi.bytesSent, |
||||
} |
||||
|
||||
mi.hmuxer.Handle(w, ctx.Request) |
||||
} |
||||
@ -0,0 +1,303 @@
@@ -0,0 +1,303 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gohlslib" |
||||
"github.com/bluenviron/gohlslib/pkg/codecs" |
||||
"github.com/bluenviron/gortsplib/v4/pkg/description" |
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264" |
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/defs" |
||||
"github.com/bluenviron/mediamtx/internal/externalcmd" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
"github.com/bluenviron/mediamtx/internal/test" |
||||
"github.com/bluenviron/mediamtx/internal/unit" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
type dummyPath struct{} |
||||
|
||||
func (pa *dummyPath) Name() string { |
||||
return "mystream" |
||||
} |
||||
|
||||
func (pa *dummyPath) SafeConf() *conf.Path { |
||||
return &conf.Path{} |
||||
} |
||||
|
||||
func (pa *dummyPath) ExternalCmdEnv() externalcmd.Environment { |
||||
return nil |
||||
} |
||||
|
||||
func (pa *dummyPath) StartPublisher(_ defs.PathStartPublisherReq) (*stream.Stream, error) { |
||||
return nil, fmt.Errorf("unimplemented") |
||||
} |
||||
|
||||
func (pa *dummyPath) StopPublisher(_ defs.PathStopPublisherReq) { |
||||
} |
||||
|
||||
func (pa *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) { |
||||
} |
||||
|
||||
func (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) { |
||||
} |
||||
|
||||
type dummyPathManager struct { |
||||
stream *stream.Stream |
||||
} |
||||
|
||||
func (pm *dummyPathManager) FindPathConf(_ defs.PathFindPathConfReq) (*conf.Path, error) { |
||||
return &conf.Path{}, nil |
||||
} |
||||
|
||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { |
||||
if req.AccessRequest.Name == "nonexisting" { |
||||
return nil, nil, fmt.Errorf("not found") |
||||
} |
||||
return &dummyPath{}, pm.stream, nil |
||||
} |
||||
|
||||
func TestServerNotFound(t *testing.T) { |
||||
for _, ca := range []string{ |
||||
"always remux off", |
||||
"always remux on", |
||||
} { |
||||
t.Run(ca, func(t *testing.T) { |
||||
s := &Server{ |
||||
Address: "127.0.0.1:8888", |
||||
Encryption: false, |
||||
ServerKey: "", |
||||
ServerCert: "", |
||||
ExternalAuthenticationURL: "", |
||||
AlwaysRemux: ca == "always remux on", |
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS), |
||||
SegmentCount: 7, |
||||
SegmentDuration: conf.StringDuration(1 * time.Second), |
||||
PartDuration: conf.StringDuration(200 * time.Millisecond), |
||||
SegmentMaxSize: 50 * 1024 * 1024, |
||||
AllowOrigin: "", |
||||
TrustedProxies: conf.IPsOrCIDRs{}, |
||||
Directory: "", |
||||
ReadTimeout: conf.StringDuration(10 * time.Second), |
||||
WriteQueueSize: 512, |
||||
PathManager: &dummyPathManager{}, |
||||
Parent: &test.NilLogger{}, |
||||
} |
||||
err := s.Initialize() |
||||
require.NoError(t, err) |
||||
defer s.Close() |
||||
|
||||
hc := &http.Client{Transport: &http.Transport{}} |
||||
|
||||
func() { |
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/nonexisting/", nil) |
||||
require.NoError(t, err) |
||||
|
||||
res, err := hc.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
require.Equal(t, http.StatusOK, res.StatusCode) |
||||
}() |
||||
|
||||
func() { |
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/nonexisting/index.m3u8", nil) |
||||
require.NoError(t, err) |
||||
|
||||
res, err := hc.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
require.Equal(t, http.StatusNotFound, res.StatusCode) |
||||
}() |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestServerRead(t *testing.T) { |
||||
t.Run("always remux off", func(t *testing.T) { |
||||
testMediaH264 := &description.Media{ |
||||
Type: description.MediaTypeVideo, |
||||
Formats: []format.Format{test.FormatH264}, |
||||
} |
||||
|
||||
desc := &description.Session{Medias: []*description.Media{testMediaH264}} |
||||
|
||||
stream, err := stream.New( |
||||
1460, |
||||
desc, |
||||
true, |
||||
test.NilLogger{}, |
||||
) |
||||
require.NoError(t, err) |
||||
|
||||
pathManager := &dummyPathManager{stream: stream} |
||||
|
||||
s := &Server{ |
||||
Address: "127.0.0.1:8888", |
||||
Encryption: false, |
||||
ServerKey: "", |
||||
ServerCert: "", |
||||
ExternalAuthenticationURL: "", |
||||
AlwaysRemux: false, |
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS), |
||||
SegmentCount: 7, |
||||
SegmentDuration: conf.StringDuration(1 * time.Second), |
||||
PartDuration: conf.StringDuration(200 * time.Millisecond), |
||||
SegmentMaxSize: 50 * 1024 * 1024, |
||||
AllowOrigin: "", |
||||
TrustedProxies: conf.IPsOrCIDRs{}, |
||||
Directory: "", |
||||
ReadTimeout: conf.StringDuration(10 * time.Second), |
||||
WriteQueueSize: 512, |
||||
PathManager: pathManager, |
||||
Parent: &test.NilLogger{}, |
||||
} |
||||
err = s.Initialize() |
||||
require.NoError(t, err) |
||||
defer s.Close() |
||||
|
||||
c := &gohlslib.Client{ |
||||
URI: "http://127.0.0.1:8888/mystream/index.m3u8", |
||||
} |
||||
|
||||
recv := make(chan struct{}) |
||||
|
||||
c.OnTracks = func(tracks []*gohlslib.Track) error { |
||||
require.Equal(t, []*gohlslib.Track{{ |
||||
Codec: &codecs.H264{}, |
||||
}}, tracks) |
||||
|
||||
c.OnDataH26x(tracks[0], func(pts, dts time.Duration, au [][]byte) { |
||||
require.Equal(t, time.Duration(0), pts) |
||||
require.Equal(t, time.Duration(0), dts) |
||||
require.Equal(t, [][]byte{ |
||||
{byte(h264.NALUTypeAccessUnitDelimiter), 0xf0}, |
||||
test.FormatH264.SPS, |
||||
test.FormatH264.PPS, |
||||
{5, 1}, |
||||
}, au) |
||||
close(recv) |
||||
}) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
err = c.Start() |
||||
require.NoError(t, err) |
||||
defer func() { <-c.Wait() }() |
||||
defer c.Close() |
||||
|
||||
go func() { |
||||
time.Sleep(100 * time.Millisecond) |
||||
for i := 0; i < 4; i++ { |
||||
stream.WriteUnit(testMediaH264, test.FormatH264, &unit.H264{ |
||||
Base: unit.Base{ |
||||
NTP: time.Time{}, |
||||
PTS: time.Duration(i) * time.Second, |
||||
}, |
||||
AU: [][]byte{ |
||||
{5, 1}, // IDR
|
||||
}, |
||||
}) |
||||
} |
||||
}() |
||||
|
||||
<-recv |
||||
}) |
||||
|
||||
t.Run("always remux on", func(t *testing.T) { |
||||
testMediaH264 := &description.Media{ |
||||
Type: description.MediaTypeVideo, |
||||
Formats: []format.Format{test.FormatH264}, |
||||
} |
||||
|
||||
desc := &description.Session{Medias: []*description.Media{testMediaH264}} |
||||
|
||||
stream, err := stream.New( |
||||
1460, |
||||
desc, |
||||
true, |
||||
test.NilLogger{}, |
||||
) |
||||
require.NoError(t, err) |
||||
|
||||
pathManager := &dummyPathManager{stream: stream} |
||||
|
||||
s := &Server{ |
||||
Address: "127.0.0.1:8888", |
||||
Encryption: false, |
||||
ServerKey: "", |
||||
ServerCert: "", |
||||
ExternalAuthenticationURL: "", |
||||
AlwaysRemux: true, |
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS), |
||||
SegmentCount: 7, |
||||
SegmentDuration: conf.StringDuration(1 * time.Second), |
||||
PartDuration: conf.StringDuration(200 * time.Millisecond), |
||||
SegmentMaxSize: 50 * 1024 * 1024, |
||||
AllowOrigin: "", |
||||
TrustedProxies: conf.IPsOrCIDRs{}, |
||||
Directory: "", |
||||
ReadTimeout: conf.StringDuration(10 * time.Second), |
||||
WriteQueueSize: 512, |
||||
PathManager: pathManager, |
||||
Parent: &test.NilLogger{}, |
||||
} |
||||
err = s.Initialize() |
||||
require.NoError(t, err) |
||||
defer s.Close() |
||||
|
||||
s.PathReady(&dummyPath{}) |
||||
|
||||
time.Sleep(100 * time.Millisecond) |
||||
|
||||
for i := 0; i < 4; i++ { |
||||
stream.WriteUnit(testMediaH264, test.FormatH264, &unit.H264{ |
||||
Base: unit.Base{ |
||||
NTP: time.Time{}, |
||||
PTS: time.Duration(i) * time.Second, |
||||
}, |
||||
AU: [][]byte{ |
||||
{5, 1}, // IDR
|
||||
}, |
||||
}) |
||||
} |
||||
|
||||
c := &gohlslib.Client{ |
||||
URI: "http://127.0.0.1:8888/mystream/index.m3u8", |
||||
} |
||||
|
||||
recv := make(chan struct{}) |
||||
|
||||
c.OnTracks = func(tracks []*gohlslib.Track) error { |
||||
require.Equal(t, []*gohlslib.Track{{ |
||||
Codec: &codecs.H264{}, |
||||
}}, tracks) |
||||
|
||||
c.OnDataH26x(tracks[0], func(pts, dts time.Duration, au [][]byte) { |
||||
require.Equal(t, time.Duration(0), pts) |
||||
require.Equal(t, time.Duration(0), dts) |
||||
require.Equal(t, [][]byte{ |
||||
{0x09, 0xf0}, |
||||
test.FormatH264.SPS, |
||||
test.FormatH264.PPS, |
||||
{5, 1}, |
||||
}, au) |
||||
close(recv) |
||||
}) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
err = c.Start() |
||||
require.NoError(t, err) |
||||
defer func() { <-c.Wait() }() |
||||
defer c.Close() |
||||
|
||||
<-recv |
||||
}) |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
package test |
||||
|
||||
import ( |
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" |
||||
) |
||||
|
||||
// FormatH264 is a test H264 format.
|
||||
var FormatH264 = &format.H264{ |
||||
PayloadTyp: 96, |
||||
SPS: []byte{ // 1920x1080 baseline
|
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, |
||||
}, |
||||
PPS: []byte{0x08, 0x06, 0x07, 0x08}, |
||||
PacketizationMode: 1, |
||||
} |
||||
|
||||
// FormatMPEG4Audio is a test MPEG-4 audio format.
|
||||
var FormatMPEG4Audio = &format.MPEG4Audio{ |
||||
PayloadTyp: 96, |
||||
Config: &mpeg4audio.Config{ |
||||
Type: 2, |
||||
SampleRate: 44100, |
||||
ChannelCount: 2, |
||||
}, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
} |
||||
Loading…
Reference in new issue