409 lines
10 KiB
Go
409 lines
10 KiB
Go
package hls
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bluenviron/gohlslib/v2"
|
|
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
|
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
|
"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 {
|
|
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
|
|
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
|
}
|
|
|
|
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
return pm.findPathConf(req)
|
|
}
|
|
|
|
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
return pm.addReader(req)
|
|
}
|
|
|
|
func TestPreflightRequest(t *testing.T) {
|
|
s := &Server{
|
|
Address: "127.0.0.1:8888",
|
|
AllowOrigin: "*",
|
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
Parent: test.NilLogger,
|
|
}
|
|
err := s.Initialize()
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
tr := &http.Transport{}
|
|
defer tr.CloseIdleConnections()
|
|
hc := &http.Client{Transport: tr}
|
|
|
|
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8888", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Add("Access-Control-Request-Method", "GET")
|
|
|
|
res, err := hc.Do(req)
|
|
require.NoError(t, err)
|
|
defer res.Body.Close()
|
|
|
|
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
|
|
|
byts, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
|
|
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
|
|
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
|
|
require.Equal(t, "Authorization, Range", res.Header.Get("Access-Control-Allow-Headers"))
|
|
require.Equal(t, byts, []byte{})
|
|
}
|
|
|
|
func TestServerNotFound(t *testing.T) {
|
|
for _, ca := range []string{
|
|
"always remux off",
|
|
"always remux on",
|
|
} {
|
|
t.Run(ca, func(t *testing.T) {
|
|
pm := &dummyPathManager{
|
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
|
return &conf.Path{}, nil
|
|
},
|
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
|
return nil, nil, fmt.Errorf("not found")
|
|
},
|
|
}
|
|
|
|
s := &Server{
|
|
Address: "127.0.0.1:8888",
|
|
Encryption: false,
|
|
ServerKey: "",
|
|
ServerCert: "",
|
|
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.IPNetworks{},
|
|
Directory: "",
|
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
PathManager: pm,
|
|
Parent: test.NilLogger,
|
|
}
|
|
err := s.Initialize()
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
tr := &http.Transport{}
|
|
defer tr.CloseIdleConnections()
|
|
hc := &http.Client{Transport: tr}
|
|
|
|
func() {
|
|
req, err := http.NewRequest(http.MethodGet, "http://myuser:mypass@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://myuser:mypass@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) {
|
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
|
|
|
str, err := stream.New(
|
|
512,
|
|
1460,
|
|
desc,
|
|
true,
|
|
test.NilLogger,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
pm := &dummyPathManager{
|
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
return &conf.Path{}, nil
|
|
},
|
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
return &dummyPath{}, str, nil
|
|
},
|
|
}
|
|
|
|
s := &Server{
|
|
Address: "127.0.0.1:8888",
|
|
Encryption: false,
|
|
ServerKey: "",
|
|
ServerCert: "",
|
|
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.IPNetworks{},
|
|
Directory: "",
|
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
PathManager: pm,
|
|
Parent: test.NilLogger,
|
|
}
|
|
err = s.Initialize()
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
c := &gohlslib.Client{
|
|
URI: "http://myuser:mypass@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{},
|
|
ClockRate: 90000,
|
|
}}, tracks)
|
|
|
|
c.OnDataH26x(tracks[0], func(pts, dts int64, au [][]byte) {
|
|
require.Equal(t, int64(0), pts)
|
|
require.Equal(t, int64(0), dts)
|
|
require.Equal(t, [][]byte{
|
|
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++ {
|
|
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
|
Base: unit.Base{
|
|
NTP: time.Time{},
|
|
PTS: int64(i) * 90000,
|
|
},
|
|
AU: [][]byte{
|
|
{5, 1}, // IDR
|
|
},
|
|
})
|
|
}
|
|
}()
|
|
|
|
<-recv
|
|
})
|
|
|
|
t.Run("always remux on", func(t *testing.T) {
|
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
|
|
|
str, err := stream.New(
|
|
512,
|
|
1460,
|
|
desc,
|
|
true,
|
|
test.NilLogger,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
pm := &dummyPathManager{
|
|
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
return &conf.Path{}, nil
|
|
},
|
|
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
return &dummyPath{}, str, nil
|
|
},
|
|
}
|
|
|
|
s := &Server{
|
|
Address: "127.0.0.1:8888",
|
|
Encryption: false,
|
|
ServerKey: "",
|
|
ServerCert: "",
|
|
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.IPNetworks{},
|
|
Directory: "",
|
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
PathManager: pm,
|
|
Parent: test.NilLogger,
|
|
}
|
|
err = s.Initialize()
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
s.PathReady(&dummyPath{})
|
|
|
|
str.WaitRunningReader()
|
|
|
|
for i := 0; i < 4; i++ {
|
|
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
|
Base: unit.Base{
|
|
NTP: time.Time{},
|
|
PTS: int64(i) * 90000,
|
|
},
|
|
AU: [][]byte{
|
|
{5, 1}, // IDR
|
|
},
|
|
})
|
|
}
|
|
|
|
c := &gohlslib.Client{
|
|
URI: "http://myuser:mypass@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{},
|
|
ClockRate: 90000,
|
|
}}, tracks)
|
|
|
|
c.OnDataH26x(tracks[0], func(pts, dts int64, au [][]byte) {
|
|
require.Equal(t, int64(0), pts)
|
|
require.Equal(t, int64(0), dts)
|
|
require.Equal(t, [][]byte{
|
|
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
|
|
})
|
|
}
|
|
|
|
func TestDirectory(t *testing.T) {
|
|
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(dir)
|
|
|
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
|
|
|
str, err := stream.New(
|
|
512,
|
|
1460,
|
|
desc,
|
|
true,
|
|
test.NilLogger,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
pm := &dummyPathManager{
|
|
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
return &dummyPath{}, str, nil
|
|
},
|
|
}
|
|
|
|
s := &Server{
|
|
Address: "127.0.0.1:8888",
|
|
Encryption: false,
|
|
ServerKey: "",
|
|
ServerCert: "",
|
|
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.IPNetworks{},
|
|
Directory: filepath.Join(dir, "mydir"),
|
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
PathManager: pm,
|
|
Parent: test.NilLogger,
|
|
}
|
|
err = s.Initialize()
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
s.PathReady(&dummyPath{})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_, err = os.Stat(filepath.Join(dir, "mydir", "mystream"))
|
|
require.NoError(t, err)
|
|
}
|