mirror of
				https://github.com/aler9/rtsp-simple-server
				synced 2025-10-31 19:13:22 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			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)
 | |
| }
 | 
