Browse Source

Allow sending Range header to RTSP sources (#1780)

* Enable Range headers using path config

* Use enum instead of strings

* Comments added to new code

* Wrong comment format

* Made CreateRangeHeader func private

* reorder configuration

* handle errors inside createRangeHeader()

* add tests

* update API docs

---------

Co-authored-by: Jordy Boezaard <jordy@boezaard.com>
Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
pull/1798/head
Jordy84 2 years ago committed by GitHub
parent
commit
596765c14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 61
      apidocs/openapi.yaml
  2. 90
      internal/conf/path.go
  3. 73
      internal/conf/rtsprangetype.go
  4. 53
      internal/core/rtsp_source.go
  5. 96
      internal/core/rtsp_source_test.go
  6. 98
      mediamtx.yml

61
apidocs/openapi.yaml

@ -171,13 +171,10 @@ components: @@ -171,13 +171,10 @@ components:
PathConf:
type: object
properties:
# source
source:
type: string
sourceProtocol:
type: string
sourceAnyPortEnable:
type: boolean
# general
sourceFingerprint:
type: string
sourceOnDemand:
@ -186,12 +183,46 @@ components: @@ -186,12 +183,46 @@ components:
type: string
sourceOnDemandCloseAfter:
type: string
sourceRedirect:
# authentication
publishUser:
type: string
publishPass:
type: string
publishIPs:
type: array
items:
type: string
readUser:
type: string
readPass:
type: string
readIPs:
type: array
items:
type: string
# publisher
disablePublisherOverride:
type: boolean
fallback:
type: string
# rtsp
sourceProtocol:
type: string
sourceAnyPortEnable:
type: boolean
rtspRangeType:
type: string
rtspRangeStart:
type: string
# redirect
sourceRedirect:
type: string
# raspberry pi camera
rpiCameraCamID:
type: integer
rpiCameraWidth:
@ -255,24 +286,6 @@ components: @@ -255,24 +286,6 @@ components:
rpiCameraTextOverlay:
type: string
# authentication
publishUser:
type: string
publishPass:
type: string
publishIPs:
type: array
items:
type: string
readUser:
type: string
readPass:
type: string
readIPs:
type: array
items:
type: string
# external commands
runOnInit:
type: string

90
internal/conf/path.go

@ -43,47 +43,13 @@ type PathConf struct { @@ -43,47 +43,13 @@ type PathConf struct {
Regexp *regexp.Regexp `json:"-"`
// source
Source string `json:"source"`
SourceProtocol SourceProtocol `json:"sourceProtocol"`
SourceAnyPortEnable bool `json:"sourceAnyPortEnable"`
Source string `json:"source"`
// general
SourceFingerprint string `json:"sourceFingerprint"`
SourceOnDemand bool `json:"sourceOnDemand"`
SourceOnDemandStartTimeout StringDuration `json:"sourceOnDemandStartTimeout"`
SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"`
SourceRedirect string `json:"sourceRedirect"`
DisablePublisherOverride bool `json:"disablePublisherOverride"`
Fallback string `json:"fallback"`
RPICameraCamID int `json:"rpiCameraCamID"`
RPICameraWidth int `json:"rpiCameraWidth"`
RPICameraHeight int `json:"rpiCameraHeight"`
RPICameraHFlip bool `json:"rpiCameraHFlip"`
RPICameraVFlip bool `json:"rpiCameraVFlip"`
RPICameraBrightness float64 `json:"rpiCameraBrightness"`
RPICameraContrast float64 `json:"rpiCameraContrast"`
RPICameraSaturation float64 `json:"rpiCameraSaturation"`
RPICameraSharpness float64 `json:"rpiCameraSharpness"`
RPICameraExposure string `json:"rpiCameraExposure"`
RPICameraAWB string `json:"rpiCameraAWB"`
RPICameraDenoise string `json:"rpiCameraDenoise"`
RPICameraShutter int `json:"rpiCameraShutter"`
RPICameraMetering string `json:"rpiCameraMetering"`
RPICameraGain float64 `json:"rpiCameraGain"`
RPICameraEV float64 `json:"rpiCameraEV"`
RPICameraROI string `json:"rpiCameraROI"`
RPICameraTuningFile string `json:"rpiCameraTuningFile"`
RPICameraMode string `json:"rpiCameraMode"`
RPICameraFPS int `json:"rpiCameraFPS"`
RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"`
RPICameraBitrate int `json:"rpiCameraBitrate"`
RPICameraProfile string `json:"rpiCameraProfile"`
RPICameraLevel string `json:"rpiCameraLevel"`
RPICameraAfMode string `json:"rpiCameraAfMode"`
RPICameraAfRange string `json:"rpiCameraAfRange"`
RPICameraAfSpeed string `json:"rpiCameraAfSpeed"`
RPICameraLensPosition float64 `json:"rpiCameraLensPosition"`
RPICameraAfWindow string `json:"rpiCameraAfWindow"`
RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"`
RPICameraTextOverlay string `json:"rpiCameraTextOverlay"`
// authentication
PublishUser Credential `json:"publishUser"`
@ -93,6 +59,52 @@ type PathConf struct { @@ -93,6 +59,52 @@ type PathConf struct {
ReadPass Credential `json:"readPass"`
ReadIPs IPsOrCIDRs `json:"readIPs"`
// publisher
DisablePublisherOverride bool `json:"disablePublisherOverride"`
Fallback string `json:"fallback"`
// rtsp
SourceProtocol SourceProtocol `json:"sourceProtocol"`
SourceAnyPortEnable bool `json:"sourceAnyPortEnable"`
RtspRangeType RtspRangeType `json:"rtspRangeType"`
RtspRangeStart string `json:"rtspRangeStart"`
// redirect
SourceRedirect string `json:"sourceRedirect"`
// raspberry pi camera
RPICameraCamID int `json:"rpiCameraCamID"`
RPICameraWidth int `json:"rpiCameraWidth"`
RPICameraHeight int `json:"rpiCameraHeight"`
RPICameraHFlip bool `json:"rpiCameraHFlip"`
RPICameraVFlip bool `json:"rpiCameraVFlip"`
RPICameraBrightness float64 `json:"rpiCameraBrightness"`
RPICameraContrast float64 `json:"rpiCameraContrast"`
RPICameraSaturation float64 `json:"rpiCameraSaturation"`
RPICameraSharpness float64 `json:"rpiCameraSharpness"`
RPICameraExposure string `json:"rpiCameraExposure"`
RPICameraAWB string `json:"rpiCameraAWB"`
RPICameraDenoise string `json:"rpiCameraDenoise"`
RPICameraShutter int `json:"rpiCameraShutter"`
RPICameraMetering string `json:"rpiCameraMetering"`
RPICameraGain float64 `json:"rpiCameraGain"`
RPICameraEV float64 `json:"rpiCameraEV"`
RPICameraROI string `json:"rpiCameraROI"`
RPICameraTuningFile string `json:"rpiCameraTuningFile"`
RPICameraMode string `json:"rpiCameraMode"`
RPICameraFPS int `json:"rpiCameraFPS"`
RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"`
RPICameraBitrate int `json:"rpiCameraBitrate"`
RPICameraProfile string `json:"rpiCameraProfile"`
RPICameraLevel string `json:"rpiCameraLevel"`
RPICameraAfMode string `json:"rpiCameraAfMode"`
RPICameraAfRange string `json:"rpiCameraAfRange"`
RPICameraAfSpeed string `json:"rpiCameraAfSpeed"`
RPICameraLensPosition float64 `json:"rpiCameraLensPosition"`
RPICameraAfWindow string `json:"rpiCameraAfWindow"`
RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"`
RPICameraTextOverlay string `json:"rpiCameraTextOverlay"`
// external commands
RunOnInit string `json:"runOnInit"`
RunOnInitRestart bool `json:"runOnInitRestart"`
@ -339,8 +351,12 @@ func (pconf PathConf) HasOnDemandPublisher() bool { @@ -339,8 +351,12 @@ func (pconf PathConf) HasOnDemandPublisher() bool {
func (pconf *PathConf) UnmarshalJSON(b []byte) error {
// source
pconf.Source = "publisher"
// general
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
// raspberry pi camera
pconf.RPICameraWidth = 1920
pconf.RPICameraHeight = 1080
pconf.RPICameraContrast = 1

73
internal/conf/rtsprangetype.go

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
package conf
import (
"encoding/json"
"fmt"
)
// RtspRangeType is the type used in the Range header.
type RtspRangeType int
// supported rtsp range types.
const (
RtspRangeTypeUndefined RtspRangeType = iota
RtspRangeTypeClock
RtspRangeTypeNPT
RtspRangeTypeSMPTE
)
// MarshalJSON implements json.Marshaler.
func (d RtspRangeType) MarshalJSON() ([]byte, error) {
var out string
switch d {
case RtspRangeTypeClock:
out = "clock"
case RtspRangeTypeNPT:
out = "npt"
case RtspRangeTypeSMPTE:
out = "smpte"
case RtspRangeTypeUndefined:
out = ""
default:
return nil, fmt.Errorf("invalid rtsp range type: %v", d)
}
return json.Marshal(out)
}
// UnmarshalJSON implements json.Unmarshaler.
func (d *RtspRangeType) UnmarshalJSON(b []byte) error {
var in string
if err := json.Unmarshal(b, &in); err != nil {
return err
}
switch in {
case "clock":
*d = RtspRangeTypeClock
case "npt":
*d = RtspRangeTypeNPT
case "smpte":
*d = RtspRangeTypeSMPTE
case "":
*d = RtspRangeTypeUndefined
default:
return fmt.Errorf("invalid rtsp range type: '%s'", in)
}
return nil
}
// UnmarshalEnv implements envUnmarshaler.
func (d *RtspRangeType) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

53
internal/core/rtsp_source.go

@ -11,6 +11,7 @@ import ( @@ -11,6 +11,7 @@ import (
"github.com/bluenviron/gortsplib/v3"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/headers"
"github.com/pion/rtp"
"github.com/aler9/mediamtx/internal/conf"
@ -49,6 +50,51 @@ func (s *rtspSource) Log(level logger.Level, format string, args ...interface{}) @@ -49,6 +50,51 @@ func (s *rtspSource) Log(level logger.Level, format string, args ...interface{})
s.parent.Log(level, "[rtsp source] "+format, args...)
}
func createRangeHeader(cnf *conf.PathConf) (*headers.Range, error) {
switch cnf.RtspRangeType {
case conf.RtspRangeTypeClock:
start, err := time.Parse("20060102T150405Z", cnf.RtspRangeStart)
if err != nil {
return nil, err
}
return &headers.Range{
Value: &headers.RangeUTC{
Start: start,
},
}, nil
case conf.RtspRangeTypeNPT:
start, err := time.ParseDuration(cnf.RtspRangeStart)
if err != nil {
return nil, err
}
return &headers.Range{
Value: &headers.RangeNPT{
Start: start,
},
}, nil
case conf.RtspRangeTypeSMPTE:
start, err := time.ParseDuration(cnf.RtspRangeStart)
if err != nil {
return nil, err
}
return &headers.Range{
Value: &headers.RangeSMPTE{
Start: headers.RangeSMPTETime{
Time: start,
},
},
}, nil
default:
return nil, nil
}
}
// run implements sourceStaticImpl.
func (s *rtspSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
s.Log(logger.Debug, "connecting")
@ -144,7 +190,12 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf cha @@ -144,7 +190,12 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf cha
}
}
_, err = c.Play(nil)
rangeHeader, err := createRangeHeader(cnf)
if err != nil {
return err
}
_, err = c.Play(rangeHeader)
if err != nil {
return err
}

96
internal/core/rtsp_source_test.go

@ -43,23 +43,18 @@ func TestRTSPSource(t *testing.T) { @@ -43,23 +43,18 @@ func TestRTSPSource(t *testing.T) {
t.Run(source, func(t *testing.T) {
medi := testMediaH264
stream := gortsplib.NewServerStream(media.Medias{medi})
var authValidator *auth.Validator
nonce := auth.GenerateNonce()
s := gortsplib.Server{
Handler: &testServer{
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
if authValidator == nil {
authValidator = auth.NewValidator("testuser", "testpass", nil)
}
err := authValidator.ValidateRequest(ctx.Request, nil)
err := auth.Validate(ctx.Request, "testuser", "testpass", nil, nil, "IPCAM", nonce)
if err != nil {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": authValidator.Header(),
"WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
},
}, nil, nil
}
@ -171,25 +166,19 @@ func TestRTSPSource(t *testing.T) { @@ -171,25 +166,19 @@ func TestRTSPSource(t *testing.T) {
}
func TestRTSPSourceNoPassword(t *testing.T) {
medi := testMediaH264
stream := gortsplib.NewServerStream(media.Medias{medi})
var authValidator *auth.Validator
stream := gortsplib.NewServerStream(media.Medias{testMediaH264})
nonce := auth.GenerateNonce()
done := make(chan struct{})
s := gortsplib.Server{
Handler: &testServer{
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
if authValidator == nil {
authValidator = auth.NewValidator("testuser", "", nil)
}
err := authValidator.ValidateRequest(ctx.Request, nil)
err := auth.Validate(ctx.Request, "testuser", "", nil, nil, "IPCAM", nonce)
if err != nil {
return &base.Response{
StatusCode: base.StatusUnauthorized,
Header: base.Header{
"WWW-Authenticate": authValidator.Header(),
"WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
},
}, nil, nil
}
@ -229,3 +218,74 @@ func TestRTSPSourceNoPassword(t *testing.T) { @@ -229,3 +218,74 @@ func TestRTSPSourceNoPassword(t *testing.T) {
<-done
}
func TestRTSPSourceRange(t *testing.T) {
for _, ca := range []string{"clock", "npt", "smpte"} {
t.Run(ca, func(t *testing.T) {
stream := gortsplib.NewServerStream(media.Medias{testMediaH264})
done := make(chan struct{})
s := gortsplib.Server{
Handler: &testServer{
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
return &base.Response{
StatusCode: base.StatusOK,
}, stream, nil
},
onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
return &base.Response{
StatusCode: base.StatusOK,
}, stream, nil
},
onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
switch ca {
case "clock":
require.Equal(t, base.HeaderValue{"clock=20230812T120000Z-"}, ctx.Request.Header["Range"])
case "npt":
require.Equal(t, base.HeaderValue{"npt=0.35-"}, ctx.Request.Header["Range"])
case "smpte":
require.Equal(t, base.HeaderValue{"smpte=0:02:10-"}, ctx.Request.Header["Range"])
}
close(done)
return &base.Response{
StatusCode: base.StatusOK,
}, nil
},
},
RTSPAddress: "127.0.0.1:8555",
}
err := s.Start()
require.NoError(t, err)
defer s.Wait()
defer s.Close()
var addConf string
switch ca {
case "clock":
addConf += " rtspRangeType: clock\n" +
" rtspRangeStart: 20230812T120000Z\n"
case "npt":
addConf += " rtspRangeType: npt\n" +
" rtspRangeStart: 350ms\n"
case "smpte":
addConf += " rtspRangeType: smpte\n" +
" rtspRangeStart: 130s\n"
}
p, ok := newInstance("rtmpDisable: yes\n" +
"hlsDisable: yes\n" +
"webrtcDisable: yes\n" +
"paths:\n" +
" proxied:\n" +
" source: rtsp://testuser:@127.0.0.1:8555/teststream\n" + addConf)
require.Equal(t, true, ok)
defer p.Close()
<-done
})
}
}

98
mediamtx.yml

@ -244,23 +244,16 @@ paths: @@ -244,23 +244,16 @@ paths:
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
source: publisher
# If the source is an RTSP or RTSPS URL, this is the protocol that will be used to
# pull the stream. available values are "automatic", "udp", "multicast", "tcp".
sourceProtocol: automatic
# Tf the source is an RTSP or RTSPS URL, this allows to support sources that
# don't provide server ports or use random server ports. This is a security issue
# and must be used only when interacting with sources that require it.
sourceAnyPortEnable: no
###############################################
# General path parameters
# If the source is a RTSPS, RTMPS or HTTPS URL, and the source certificate is self-signed
# If the source is a URL, and the source certificate is self-signed
# or invalid, you can provide the fingerprint of the certificate in order to
# validate it anyway. It can be obtained by running:
# openssl s_client -connect source_ip:source_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
sourceFingerprint:
# If the source is an RTSP or RTMP URL, it will be pulled only when at least
# If the source is a URL, it will be pulled only when at least
# one reader is connected, saving bandwidth.
sourceOnDemand: no
# If sourceOnDemand is "yes", readers will be put on hold until the source is
@ -270,19 +263,65 @@ paths: @@ -270,19 +263,65 @@ paths:
# readers connected and this amount of time has passed.
sourceOnDemandCloseAfter: 10s
# If the source is "redirect", this is the RTSP URL which clients will be
# redirected to.
sourceRedirect:
###############################################
# Authentication path parameters
# If the source is "publisher" and a client is publishing, do not allow another
# client to disconnect the former and publish in its place.
disablePublisherOverride: no
# Username required to publish.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
publishUser:
# Password required to publish.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
publishPass:
# IPs or networks (x.x.x.x/24) allowed to publish.
publishIPs: []
# Username required to read.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
readUser:
# password required to read.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
readPass:
# IPs or networks (x.x.x.x/24) allowed to read.
readIPs: []
###############################################
# Publisher path parameters (when source is "publisher")
# If the source is "publisher" and no one is publishing, redirect readers to this
# path. It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL.
# do not allow another client to disconnect the current publisher and publish in its place.
disablePublisherOverride: no
# if no one is publishing, redirect readers to this path.
# It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL.
fallback:
# If the source is "rpiCamera", these are the Raspberry Pi Camera parameters.
###############################################
# RTSP path parameters (when source is a RTSP or a RTSPS URL)
# protocol used to pull the stream. available values are "automatic", "udp", "multicast", "tcp".
sourceProtocol: automatic
# support sources that don't provide server ports or use random server ports. This is a security issue
# and must be used only when interacting with sources that require it.
sourceAnyPortEnable: no
# range header to send to the source, in order to start streaming from the specified offset.
# available values:
# * clock: Absolute time
# * npt: Normal Play Time
# * smpte: SMPTE timestamps relative to the start of the recording
rtspRangeType:
# available values:
# * clock: UTC ISO 8601 combined date and time string, e.g. 20230812T120000Z
# * npt: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
# * smpte: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
rtspRangeStart:
###############################################
# Redirect path parameters (when source is "redirect")
# RTSP URL which clients will be redirected to.
sourceRedirect:
###############################################
# Raspberry Pi Camera path parameters (when source is "rpiCamera")
# ID of the camera
rpiCameraCamID: 0
# width of frames
@ -360,23 +399,8 @@ paths: @@ -360,23 +399,8 @@ paths:
# format is the one of the strftime() function.
rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX'
# Username required to publish.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
publishUser:
# Password required to publish.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
publishPass:
# IPs or networks (x.x.x.x/24) allowed to publish.
publishIPs: []
# Username required to read.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
readUser:
# password required to read.
# SHA256-hashed values can be inserted with the "sha256:" prefix.
readPass:
# IPs or networks (x.x.x.x/24) allowed to read.
readIPs: []
###############################################
# external commands path parameters
# Command to run when this path is initialized.
# This can be used to publish a stream and keep it always opened.

Loading…
Cancel
Save