mirror of
https://github.com/aler9/gortsplib
synced 2025-10-04 23:02:45 +08:00
Merge branch 'main' into v4
This commit is contained in:
@@ -18,10 +18,10 @@ type AV1 struct {
|
||||
Tier *int
|
||||
}
|
||||
|
||||
func (f *AV1) unmarshal(payloadType uint8, _ string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *AV1) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "level-idx":
|
||||
n, err := strconv.ParseUint(val, 10, 31)
|
||||
|
@@ -16,10 +16,19 @@ func getCodecAndClock(rtpMap string) (string, string) {
|
||||
return strings.ToLower(parts2[0]), parts2[1]
|
||||
}
|
||||
|
||||
type unmarshalContext struct {
|
||||
mediaType string
|
||||
payloadType uint8
|
||||
clock string
|
||||
codec string
|
||||
rtpMap string
|
||||
fmtp map[string]string
|
||||
}
|
||||
|
||||
// Format is a media format.
|
||||
// It defines the payload type of RTP packets and how to encode/decode them.
|
||||
type Format interface {
|
||||
unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error
|
||||
unmarshal(ctx *unmarshalContext) error
|
||||
|
||||
// Codec returns the codec name.
|
||||
Codec() string
|
||||
@@ -46,8 +55,8 @@ func Unmarshal(mediaType string, payloadType uint8, rtpMap string, fmtp map[stri
|
||||
|
||||
format := func() Format {
|
||||
switch {
|
||||
case mediaType == "video":
|
||||
switch {
|
||||
// video
|
||||
|
||||
case payloadType == 26:
|
||||
return &MJPEG{}
|
||||
|
||||
@@ -74,10 +83,9 @@ func Unmarshal(mediaType string, payloadType uint8, rtpMap string, fmtp map[stri
|
||||
|
||||
case codec == "av1" && clock == "90000":
|
||||
return &AV1{}
|
||||
}
|
||||
|
||||
case mediaType == "audio":
|
||||
switch {
|
||||
// audio
|
||||
|
||||
case payloadType == 0, payloadType == 8:
|
||||
return &G711{}
|
||||
|
||||
@@ -115,12 +123,18 @@ func Unmarshal(mediaType string, payloadType uint8, rtpMap string, fmtp map[stri
|
||||
case codec == "opus":
|
||||
return &Opus{}
|
||||
}
|
||||
}
|
||||
|
||||
return &Generic{}
|
||||
}()
|
||||
|
||||
err := format.unmarshal(payloadType, clock, codec, rtpMap, fmtp)
|
||||
err := format.unmarshal(&unmarshalContext{
|
||||
mediaType: mediaType,
|
||||
payloadType: payloadType,
|
||||
clock: clock,
|
||||
codec: codec,
|
||||
rtpMap: rtpMap,
|
||||
fmtp: fmtp,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -890,19 +890,6 @@ var casesFormat = []struct {
|
||||
"custom",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"application invalid rtpmap 2",
|
||||
"application",
|
||||
98,
|
||||
"custom/aaa",
|
||||
nil,
|
||||
&Generic{
|
||||
PayloadTyp: 98,
|
||||
RTPMa: "custom/aaa",
|
||||
},
|
||||
"custom/aaa",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
@@ -925,7 +912,13 @@ func TestMarshal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalMPEG4AudioGenericErrors(t *testing.T) {
|
||||
func TestUnmarshalErrors(t *testing.T) {
|
||||
t.Run("invalid video", func(t *testing.T) {
|
||||
_, err := Unmarshal("video", 96, "", map[string]string{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("mpeg-4 audio generic", func(t *testing.T) {
|
||||
_, err := Unmarshal("audio", 96, "MPEG4-generic/48000/2", map[string]string{
|
||||
"streamtype": "10",
|
||||
})
|
||||
@@ -981,9 +974,9 @@ func TestUnmarshalMPEG4AudioGenericErrors(t *testing.T) {
|
||||
"indexdeltalength": "3",
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
func TestUnmarshalMPEG4AudioLATMErrors(t *testing.T) {
|
||||
t.Run("mpeg-4 audio latm", func(t *testing.T) {
|
||||
_, err := Unmarshal("audio", 96, "MP4A-LATM/48000/2", map[string]string{
|
||||
"profile-level-id": "aaa",
|
||||
})
|
||||
@@ -1011,9 +1004,9 @@ func TestUnmarshalMPEG4AudioLATMErrors(t *testing.T) {
|
||||
"sbr-enabled": "1",
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
func TestUnmarshalAV1Errors(t *testing.T) {
|
||||
t.Run("av1", func(t *testing.T) {
|
||||
_, err := Unmarshal("video", 96, "AV1/90000", map[string]string{
|
||||
"level-idx": "aaa",
|
||||
})
|
||||
@@ -1028,6 +1021,7 @@ func TestUnmarshalAV1Errors(t *testing.T) {
|
||||
"tier": "aaa",
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzUnmarshalH264(f *testing.F) {
|
||||
|
@@ -13,8 +13,8 @@ type G711 struct {
|
||||
MULaw bool
|
||||
}
|
||||
|
||||
func (f *G711) unmarshal(payloadType uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
f.MULaw = (payloadType == 0)
|
||||
func (f *G711) unmarshal(ctx *unmarshalContext) error {
|
||||
f.MULaw = (ctx.payloadType == 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
// Specification: https://datatracker.ietf.org/doc/html/rfc3551
|
||||
type G722 struct{}
|
||||
|
||||
func (f *G722) unmarshal(_ uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
func (f *G722) unmarshal(_ *unmarshalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -15,19 +15,19 @@ type G726 struct {
|
||||
BigEndian bool
|
||||
}
|
||||
|
||||
func (f *G726) unmarshal(payloadType uint8, _ string, codec string, _ string, _ map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *G726) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
if strings.HasPrefix(codec, "aal2-") {
|
||||
if strings.HasPrefix(ctx.codec, "aal2-") {
|
||||
f.BigEndian = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(codec, "-16"):
|
||||
case strings.HasSuffix(ctx.codec, "-16"):
|
||||
f.BitRate = 16
|
||||
case strings.HasSuffix(codec, "-24"):
|
||||
case strings.HasSuffix(ctx.codec, "-24"):
|
||||
f.BitRate = 24
|
||||
case strings.HasSuffix(codec, "-32"):
|
||||
case strings.HasSuffix(ctx.codec, "-32"):
|
||||
f.BitRate = 32
|
||||
default:
|
||||
f.BitRate = 40
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func findClockRate(payloadType uint8, rtpMap string) (int, error) {
|
||||
func findClockRate(payloadType uint8, rtpMap string, isApplication bool) (int, error) {
|
||||
// get clock rate from payload type
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
switch payloadType {
|
||||
@@ -34,22 +34,24 @@ func findClockRate(payloadType uint8, rtpMap string) (int, error) {
|
||||
// get clock rate from rtpmap
|
||||
// https://tools.ietf.org/html/rfc4566
|
||||
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
||||
if rtpMap == "" {
|
||||
return 0, fmt.Errorf("attribute 'rtpmap' not found")
|
||||
}
|
||||
|
||||
tmp := strings.Split(rtpMap, "/")
|
||||
if len(tmp) != 2 && len(tmp) != 3 {
|
||||
return 0, fmt.Errorf("invalid rtpmap (%v)", rtpMap)
|
||||
}
|
||||
|
||||
if rtpMap != "" {
|
||||
if tmp := strings.Split(rtpMap, "/"); len(tmp) >= 2 {
|
||||
v, err := strconv.ParseUint(tmp[1], 10, 31)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
// application format without clock rate.
|
||||
// do not throw an error, but return zero, that disables RTCP sender and receiver reports.
|
||||
if isApplication || rtpMap != "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("clock rate not found")
|
||||
}
|
||||
|
||||
// Generic is a generic RTP format.
|
||||
type Generic struct {
|
||||
@@ -63,16 +65,19 @@ type Generic struct {
|
||||
|
||||
// Init computes the clock rate of the format. It is mandatory to call it.
|
||||
func (f *Generic) Init() error {
|
||||
f.ClockRat, _ = findClockRate(f.PayloadTyp, f.RTPMa)
|
||||
return nil
|
||||
var err error
|
||||
f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Generic) unmarshal(payloadType uint8, _ string, _ string, rtpmap string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
f.RTPMa = rtpmap
|
||||
f.FMT = fmtp
|
||||
func (f *Generic) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
f.RTPMa = ctx.rtpMap
|
||||
f.FMT = ctx.fmtp
|
||||
|
||||
return f.Init()
|
||||
var err error
|
||||
f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, ctx.mediaType == "application")
|
||||
return err
|
||||
}
|
||||
|
||||
// Codec implements Format.
|
||||
|
@@ -25,10 +25,10 @@ type H264 struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (f *H264) unmarshal(payloadType uint8, _ string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *H264) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "sprop-parameter-sets":
|
||||
tmp := strings.Split(val, ",")
|
||||
|
@@ -24,36 +24,36 @@ type H265 struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (f *H265) unmarshal(payloadType uint8, _ string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *H265) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "sprop-vps":
|
||||
var err error
|
||||
f.VPS, err = base64.StdEncoding.DecodeString(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprop-vps (%v)", fmtp)
|
||||
return fmt.Errorf("invalid sprop-vps (%v)", ctx.fmtp)
|
||||
}
|
||||
|
||||
case "sprop-sps":
|
||||
var err error
|
||||
f.SPS, err = base64.StdEncoding.DecodeString(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprop-sps (%v)", fmtp)
|
||||
return fmt.Errorf("invalid sprop-sps (%v)", ctx.fmtp)
|
||||
}
|
||||
|
||||
case "sprop-pps":
|
||||
var err error
|
||||
f.PPS, err = base64.StdEncoding.DecodeString(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprop-pps (%v)", fmtp)
|
||||
return fmt.Errorf("invalid sprop-pps (%v)", ctx.fmtp)
|
||||
}
|
||||
|
||||
case "sprop-max-don-diff":
|
||||
tmp, err := strconv.ParseUint(val, 10, 31)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp)
|
||||
return fmt.Errorf("invalid sprop-max-don-diff (%v)", ctx.fmtp)
|
||||
}
|
||||
f.MaxDONDiff = int(tmp)
|
||||
}
|
||||
|
@@ -18,10 +18,10 @@ type LPCM struct {
|
||||
ChannelCount int
|
||||
}
|
||||
|
||||
func (f *LPCM) unmarshal(payloadType uint8, clock string, codec string, _ string, _ map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *LPCM) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
switch codec {
|
||||
switch ctx.codec {
|
||||
case "l8":
|
||||
f.BitDepth = 8
|
||||
|
||||
@@ -32,7 +32,7 @@ func (f *LPCM) unmarshal(payloadType uint8, clock string, codec string, _ string
|
||||
f.BitDepth = 24
|
||||
}
|
||||
|
||||
tmp := strings.SplitN(clock, "/", 2)
|
||||
tmp := strings.SplitN(ctx.clock, "/", 2)
|
||||
|
||||
tmp1, err := strconv.ParseUint(tmp[0], 10, 31)
|
||||
if err != nil {
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
|
||||
type MJPEG struct{}
|
||||
|
||||
func (f *MJPEG) unmarshal(_ uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
func (f *MJPEG) unmarshal(_ *unmarshalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
|
||||
type MPEG1Audio struct{}
|
||||
|
||||
func (f *MPEG1Audio) unmarshal(_ uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
func (f *MPEG1Audio) unmarshal(_ *unmarshalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
|
||||
type MPEG1Video struct{}
|
||||
|
||||
func (f *MPEG1Video) unmarshal(_ uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
func (f *MPEG1Video) unmarshal(_ *unmarshalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -26,13 +26,10 @@ type MPEG4AudioGeneric struct {
|
||||
IndexDeltaLength int
|
||||
}
|
||||
|
||||
func (f *MPEG4AudioGeneric) unmarshal(
|
||||
payloadType uint8, _ string, _ string,
|
||||
_ string, fmtp map[string]string,
|
||||
) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *MPEG4AudioGeneric) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "streamtype":
|
||||
if val != "5" { // AudioStream in ISO 14496-1
|
||||
|
@@ -22,17 +22,14 @@ type MPEG4AudioLATM struct {
|
||||
SBREnabled *bool
|
||||
}
|
||||
|
||||
func (f *MPEG4AudioLATM) unmarshal(
|
||||
payloadType uint8, _ string, _ string,
|
||||
_ string, fmtp map[string]string,
|
||||
) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *MPEG4AudioLATM) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
// default value set by specification
|
||||
f.ProfileLevelID = 30
|
||||
f.CPresent = true
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "profile-level-id":
|
||||
tmp, err := strconv.ParseUint(val, 10, 31)
|
||||
|
@@ -22,14 +22,11 @@ type MPEG4VideoES struct {
|
||||
Config []byte
|
||||
}
|
||||
|
||||
func (f *MPEG4VideoES) unmarshal(
|
||||
payloadType uint8, _ string, _ string,
|
||||
_ string, fmtp map[string]string,
|
||||
) error {
|
||||
f.PayloadTyp = payloadType
|
||||
f.ProfileLevelID = 1 // default value defined by specification
|
||||
func (f *MPEG4VideoES) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
f.ProfileLevelID = 1 // default value imposed by specification
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "profile-level-id":
|
||||
tmp, err := strconv.ParseUint(val, 10, 31)
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
|
||||
type MPEGTS struct{}
|
||||
|
||||
func (f *MPEGTS) unmarshal(_ uint8, _ string, _ string, _ string, _ map[string]string) error {
|
||||
func (f *MPEGTS) unmarshal(_ *unmarshalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -17,12 +17,12 @@ type Opus struct {
|
||||
IsStereo bool
|
||||
}
|
||||
|
||||
func (f *Opus) unmarshal(payloadType uint8, clock string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *Opus) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
tmp := strings.SplitN(clock, "/", 2)
|
||||
tmp := strings.SplitN(ctx.clock, "/", 2)
|
||||
if len(tmp) != 2 {
|
||||
return fmt.Errorf("invalid clock (%v)", clock)
|
||||
return fmt.Errorf("invalid clock (%v)", ctx.clock)
|
||||
}
|
||||
|
||||
sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
|
||||
@@ -35,7 +35,7 @@ func (f *Opus) unmarshal(payloadType uint8, clock string, _ string, _ string, fm
|
||||
return fmt.Errorf("invalid channel count: %d", channelCount)
|
||||
}
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
if key == "sprop-stereo" {
|
||||
f.IsStereo = (val == "1")
|
||||
}
|
||||
|
@@ -15,16 +15,16 @@ type Speex struct {
|
||||
VBR *bool
|
||||
}
|
||||
|
||||
func (f *Speex) unmarshal(payloadType uint8, clock string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *Speex) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
sampleRate, err := strconv.ParseUint(clock, 10, 31)
|
||||
sampleRate, err := strconv.ParseUint(ctx.clock, 10, 31)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SampleRate = int(sampleRate)
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
if key == "vbr" {
|
||||
if val != "on" && val != "off" {
|
||||
return fmt.Errorf("invalid vbr value: %v", val)
|
||||
|
@@ -18,12 +18,12 @@ type Vorbis struct {
|
||||
Configuration []byte
|
||||
}
|
||||
|
||||
func (f *Vorbis) unmarshal(payloadType uint8, clock string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *Vorbis) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
tmp := strings.SplitN(clock, "/", 2)
|
||||
tmp := strings.SplitN(ctx.clock, "/", 2)
|
||||
if len(tmp) != 2 {
|
||||
return fmt.Errorf("invalid clock (%v)", clock)
|
||||
return fmt.Errorf("invalid clock (%v)", ctx.clock)
|
||||
}
|
||||
|
||||
sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
|
||||
@@ -38,7 +38,7 @@ func (f *Vorbis) unmarshal(payloadType uint8, clock string, _ string, _ string,
|
||||
}
|
||||
f.ChannelCount = int(channelCount)
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
if key == "configuration" {
|
||||
conf, err := base64.StdEncoding.DecodeString(val)
|
||||
if err != nil {
|
||||
|
@@ -17,10 +17,10 @@ type VP8 struct {
|
||||
MaxFS *int
|
||||
}
|
||||
|
||||
func (f *VP8) unmarshal(payloadType uint8, _ string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *VP8) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "max-fr":
|
||||
n, err := strconv.ParseUint(val, 10, 31)
|
||||
|
@@ -18,10 +18,10 @@ type VP9 struct {
|
||||
ProfileID *int
|
||||
}
|
||||
|
||||
func (f *VP9) unmarshal(payloadType uint8, _ string, _ string, _ string, fmtp map[string]string) error {
|
||||
f.PayloadTyp = payloadType
|
||||
func (f *VP9) unmarshal(ctx *unmarshalContext) error {
|
||||
f.PayloadTyp = ctx.payloadType
|
||||
|
||||
for key, val := range fmtp {
|
||||
for key, val := range ctx.fmtp {
|
||||
switch key {
|
||||
case "max-fr":
|
||||
n, err := strconv.ParseUint(val, 10, 31)
|
||||
|
Reference in New Issue
Block a user