直播:后台 JWT 推流、前台画中画;WebRTC 服务与 Nginx WebSocket 代理
Made-with: Cursor
This commit is contained in:
41
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/av1.go
generated
vendored
Normal file
41
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/av1.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
type av1FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *av1FMTP) MimeType() string {
|
||||
return "video/av1"
|
||||
}
|
||||
|
||||
func (h *av1FMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*av1FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// RTP Payload Format For AV1 (v1.0)
|
||||
// https://aomediacodec.github.io/av1-rtp-spec/
|
||||
// If the profile parameter is not present, it MUST be inferred to be 0 (“Main” profile).
|
||||
hProfile, ok := h.parameters["profile"]
|
||||
if !ok {
|
||||
hProfile = "0"
|
||||
}
|
||||
cProfile, ok := c.parameters["profile"]
|
||||
if !ok {
|
||||
cProfile = "0"
|
||||
}
|
||||
if hProfile != cProfile {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *av1FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
return v, ok
|
||||
}
|
||||
112
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/fmtp.go
generated
vendored
Normal file
112
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/fmtp.go
generated
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package fmtp implements per codec parsing of fmtp lines
|
||||
package fmtp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseParameters(line string) map[string]string {
|
||||
parameters := make(map[string]string)
|
||||
|
||||
for _, p := range strings.Split(line, ";") {
|
||||
pp := strings.SplitN(strings.TrimSpace(p), "=", 2)
|
||||
key := strings.ToLower(pp[0])
|
||||
var value string
|
||||
if len(pp) > 1 {
|
||||
value = pp[1]
|
||||
}
|
||||
parameters[key] = value
|
||||
}
|
||||
|
||||
return parameters
|
||||
}
|
||||
|
||||
// FMTP interface for implementing custom
|
||||
// FMTP parsers based on MimeType
|
||||
type FMTP interface {
|
||||
// MimeType returns the MimeType associated with
|
||||
// the fmtp
|
||||
MimeType() string
|
||||
// Match compares two fmtp descriptions for
|
||||
// compatibility based on the MimeType
|
||||
Match(f FMTP) bool
|
||||
// Parameter returns a value for the associated key
|
||||
// if contained in the parsed fmtp string
|
||||
Parameter(key string) (string, bool)
|
||||
}
|
||||
|
||||
// Parse parses an fmtp string based on the MimeType
|
||||
func Parse(mimeType, line string) FMTP {
|
||||
var f FMTP
|
||||
|
||||
parameters := parseParameters(line)
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(mimeType, "video/h264"):
|
||||
f = &h264FMTP{
|
||||
parameters: parameters,
|
||||
}
|
||||
|
||||
case strings.EqualFold(mimeType, "video/vp9"):
|
||||
f = &vp9FMTP{
|
||||
parameters: parameters,
|
||||
}
|
||||
|
||||
case strings.EqualFold(mimeType, "video/av1"):
|
||||
f = &av1FMTP{
|
||||
parameters: parameters,
|
||||
}
|
||||
|
||||
default:
|
||||
f = &genericFMTP{
|
||||
mimeType: mimeType,
|
||||
parameters: parameters,
|
||||
}
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
type genericFMTP struct {
|
||||
mimeType string
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (g *genericFMTP) MimeType() string {
|
||||
return g.mimeType
|
||||
}
|
||||
|
||||
// Match returns true if g and b are compatible fmtp descriptions
|
||||
// The generic implementation is used for MimeTypes that are not defined
|
||||
func (g *genericFMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*genericFMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.EqualFold(g.mimeType, c.MimeType()) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, v := range g.parameters {
|
||||
if vb, ok := c.parameters[k]; ok && !strings.EqualFold(vb, v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range c.parameters {
|
||||
if va, ok := g.parameters[k]; ok && !strings.EqualFold(va, v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *genericFMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := g.parameters[key]
|
||||
return v, ok
|
||||
}
|
||||
84
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/h264.go
generated
vendored
Normal file
84
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/h264.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func profileLevelIDMatches(a, b string) bool {
|
||||
aa, err := hex.DecodeString(a)
|
||||
if err != nil || len(aa) < 2 {
|
||||
return false
|
||||
}
|
||||
bb, err := hex.DecodeString(b)
|
||||
if err != nil || len(bb) < 2 {
|
||||
return false
|
||||
}
|
||||
return aa[0] == bb[0] && aa[1] == bb[1]
|
||||
}
|
||||
|
||||
type h264FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *h264FMTP) MimeType() string {
|
||||
return "video/h264"
|
||||
}
|
||||
|
||||
// Match returns true if h and b are compatible fmtp descriptions
|
||||
// Based on RFC6184 Section 8.2.2:
|
||||
//
|
||||
// The parameters identifying a media format configuration for H.264
|
||||
// are profile-level-id and packetization-mode. These media format
|
||||
// configuration parameters (except for the level part of profile-
|
||||
// level-id) MUST be used symmetrically; that is, the answerer MUST
|
||||
// either maintain all configuration parameters or remove the media
|
||||
// format (payload type) completely if one or more of the parameter
|
||||
// values are not supported.
|
||||
// Informative note: The requirement for symmetric use does not
|
||||
// apply for the level part of profile-level-id and does not apply
|
||||
// for the other stream properties and capability parameters.
|
||||
func (h *h264FMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*h264FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// test packetization-mode
|
||||
hpmode, hok := h.parameters["packetization-mode"]
|
||||
if !hok {
|
||||
return false
|
||||
}
|
||||
cpmode, cok := c.parameters["packetization-mode"]
|
||||
if !cok {
|
||||
return false
|
||||
}
|
||||
|
||||
if hpmode != cpmode {
|
||||
return false
|
||||
}
|
||||
|
||||
// test profile-level-id
|
||||
hplid, hok := h.parameters["profile-level-id"]
|
||||
if !hok {
|
||||
return false
|
||||
}
|
||||
|
||||
cplid, cok := c.parameters["profile-level-id"]
|
||||
if !cok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !profileLevelIDMatches(hplid, cplid) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *h264FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
return v, ok
|
||||
}
|
||||
41
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/vp9.go
generated
vendored
Normal file
41
server/vendor/github.com/pion/webrtc/v3/internal/fmtp/vp9.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
type vp9FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) MimeType() string {
|
||||
return "video/vp9"
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*vp9FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// RTP Payload Format for VP9 Video - draft-ietf-payload-vp9-16
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16
|
||||
// If no profile-id is present, Profile 0 MUST be inferred
|
||||
hProfileID, ok := h.parameters["profile-id"]
|
||||
if !ok {
|
||||
hProfileID = "0"
|
||||
}
|
||||
cProfileID, ok := c.parameters["profile-id"]
|
||||
if !ok {
|
||||
cProfileID = "0"
|
||||
}
|
||||
if hProfileID != cProfileID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
return v, ok
|
||||
}
|
||||
78
server/vendor/github.com/pion/webrtc/v3/internal/mux/endpoint.go
generated
vendored
Normal file
78
server/vendor/github.com/pion/webrtc/v3/internal/mux/endpoint.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/transport/v2/packetio"
|
||||
)
|
||||
|
||||
// Endpoint implements net.Conn. It is used to read muxed packets.
|
||||
type Endpoint struct {
|
||||
mux *Mux
|
||||
buffer *packetio.Buffer
|
||||
}
|
||||
|
||||
// Close unregisters the endpoint from the Mux
|
||||
func (e *Endpoint) Close() (err error) {
|
||||
err = e.close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.mux.RemoveEndpoint(e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Endpoint) close() error {
|
||||
return e.buffer.Close()
|
||||
}
|
||||
|
||||
// Read reads a packet of len(p) bytes from the underlying conn
|
||||
// that are matched by the associated MuxFunc
|
||||
func (e *Endpoint) Read(p []byte) (int, error) {
|
||||
return e.buffer.Read(p)
|
||||
}
|
||||
|
||||
// Write writes len(p) bytes to the underlying conn
|
||||
func (e *Endpoint) Write(p []byte) (int, error) {
|
||||
n, err := e.mux.nextConn.Write(p)
|
||||
if errors.Is(err, ice.ErrNoCandidatePairs) {
|
||||
return 0, nil
|
||||
} else if errors.Is(err, ice.ErrClosed) {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// LocalAddr is a stub
|
||||
func (e *Endpoint) LocalAddr() net.Addr {
|
||||
return e.mux.nextConn.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr is a stub
|
||||
func (e *Endpoint) RemoteAddr() net.Addr {
|
||||
return e.mux.nextConn.RemoteAddr()
|
||||
}
|
||||
|
||||
// SetDeadline is a stub
|
||||
func (e *Endpoint) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetReadDeadline is a stub
|
||||
func (e *Endpoint) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline is a stub
|
||||
func (e *Endpoint) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
163
server/vendor/github.com/pion/webrtc/v3/internal/mux/mux.go
generated
vendored
Normal file
163
server/vendor/github.com/pion/webrtc/v3/internal/mux/mux.go
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package mux multiplexes packets on a single socket (RFC7983)
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/transport/v2/packetio"
|
||||
)
|
||||
|
||||
// The maximum amount of data that can be buffered before returning errors.
|
||||
const maxBufferSize = 1000 * 1000 // 1MB
|
||||
|
||||
// Config collects the arguments to mux.Mux construction into
|
||||
// a single structure
|
||||
type Config struct {
|
||||
Conn net.Conn
|
||||
BufferSize int
|
||||
LoggerFactory logging.LoggerFactory
|
||||
}
|
||||
|
||||
// Mux allows multiplexing
|
||||
type Mux struct {
|
||||
lock sync.RWMutex
|
||||
nextConn net.Conn
|
||||
endpoints map[*Endpoint]MatchFunc
|
||||
bufferSize int
|
||||
closedCh chan struct{}
|
||||
|
||||
log logging.LeveledLogger
|
||||
}
|
||||
|
||||
// NewMux creates a new Mux
|
||||
func NewMux(config Config) *Mux {
|
||||
m := &Mux{
|
||||
nextConn: config.Conn,
|
||||
endpoints: make(map[*Endpoint]MatchFunc),
|
||||
bufferSize: config.BufferSize,
|
||||
closedCh: make(chan struct{}),
|
||||
log: config.LoggerFactory.NewLogger("mux"),
|
||||
}
|
||||
|
||||
go m.readLoop()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// NewEndpoint creates a new Endpoint
|
||||
func (m *Mux) NewEndpoint(f MatchFunc) *Endpoint {
|
||||
e := &Endpoint{
|
||||
mux: m,
|
||||
buffer: packetio.NewBuffer(),
|
||||
}
|
||||
|
||||
// Set a maximum size of the buffer in bytes.
|
||||
e.buffer.SetLimitSize(maxBufferSize)
|
||||
|
||||
m.lock.Lock()
|
||||
m.endpoints[e] = f
|
||||
m.lock.Unlock()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// RemoveEndpoint removes an endpoint from the Mux
|
||||
func (m *Mux) RemoveEndpoint(e *Endpoint) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
delete(m.endpoints, e)
|
||||
}
|
||||
|
||||
// Close closes the Mux and all associated Endpoints.
|
||||
func (m *Mux) Close() error {
|
||||
m.lock.Lock()
|
||||
for e := range m.endpoints {
|
||||
if err := e.close(); err != nil {
|
||||
m.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
delete(m.endpoints, e)
|
||||
}
|
||||
m.lock.Unlock()
|
||||
|
||||
err := m.nextConn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for readLoop to end
|
||||
<-m.closedCh
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mux) readLoop() {
|
||||
defer func() {
|
||||
close(m.closedCh)
|
||||
}()
|
||||
|
||||
buf := make([]byte, m.bufferSize)
|
||||
for {
|
||||
n, err := m.nextConn.Read(buf)
|
||||
switch {
|
||||
case errors.Is(err, io.EOF), errors.Is(err, ice.ErrClosed):
|
||||
return
|
||||
case errors.Is(err, io.ErrShortBuffer), errors.Is(err, packetio.ErrTimeout):
|
||||
m.log.Errorf("mux: failed to read from packetio.Buffer %s", err.Error())
|
||||
continue
|
||||
case err != nil:
|
||||
m.log.Errorf("mux: ending readLoop packetio.Buffer error %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = m.dispatch(buf[:n]); err != nil {
|
||||
if errors.Is(err, io.ErrClosedPipe) {
|
||||
// if the buffer was closed, that's not an error we care to report
|
||||
return
|
||||
}
|
||||
m.log.Errorf("mux: ending readLoop dispatch error %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mux) dispatch(buf []byte) error {
|
||||
var endpoint *Endpoint
|
||||
|
||||
m.lock.Lock()
|
||||
for e, f := range m.endpoints {
|
||||
if f(buf) {
|
||||
endpoint = e
|
||||
break
|
||||
}
|
||||
}
|
||||
m.lock.Unlock()
|
||||
|
||||
if endpoint == nil {
|
||||
if len(buf) > 0 {
|
||||
m.log.Warnf("Warning: mux: no endpoint for packet starting with %d", buf[0])
|
||||
} else {
|
||||
m.log.Warnf("Warning: mux: no endpoint for zero length packet")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := endpoint.buffer.Write(buf)
|
||||
|
||||
// Expected when bytes are received faster than the endpoint can process them (#2152, #2180)
|
||||
if errors.Is(err, packetio.ErrFull) {
|
||||
m.log.Infof("mux: endpoint buffer is full, dropping packet")
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
65
server/vendor/github.com/pion/webrtc/v3/internal/mux/muxfunc.go
generated
vendored
Normal file
65
server/vendor/github.com/pion/webrtc/v3/internal/mux/muxfunc.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mux
|
||||
|
||||
// MatchFunc allows custom logic for mapping packets to an Endpoint
|
||||
type MatchFunc func([]byte) bool
|
||||
|
||||
// MatchAll always returns true
|
||||
func MatchAll([]byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MatchRange returns true if the first byte of buf is in [lower..upper]
|
||||
func MatchRange(lower, upper byte, buf []byte) bool {
|
||||
if len(buf) < 1 {
|
||||
return false
|
||||
}
|
||||
b := buf[0]
|
||||
return b >= lower && b <= upper
|
||||
}
|
||||
|
||||
// MatchFuncs as described in RFC7983
|
||||
// https://tools.ietf.org/html/rfc7983
|
||||
// +----------------+
|
||||
// | [0..3] -+--> forward to STUN
|
||||
// | |
|
||||
// | [16..19] -+--> forward to ZRTP
|
||||
// | |
|
||||
// packet --> | [20..63] -+--> forward to DTLS
|
||||
// | |
|
||||
// | [64..79] -+--> forward to TURN Channel
|
||||
// | |
|
||||
// | [128..191] -+--> forward to RTP/RTCP
|
||||
// +----------------+
|
||||
|
||||
// MatchDTLS is a MatchFunc that accepts packets with the first byte in [20..63]
|
||||
// as defied in RFC7983
|
||||
func MatchDTLS(b []byte) bool {
|
||||
return MatchRange(20, 63, b)
|
||||
}
|
||||
|
||||
// MatchSRTPOrSRTCP is a MatchFunc that accepts packets with the first byte in [128..191]
|
||||
// as defied in RFC7983
|
||||
func MatchSRTPOrSRTCP(b []byte) bool {
|
||||
return MatchRange(128, 191, b)
|
||||
}
|
||||
|
||||
func isRTCP(buf []byte) bool {
|
||||
// Not long enough to determine RTP/RTCP
|
||||
if len(buf) < 4 {
|
||||
return false
|
||||
}
|
||||
return buf[1] >= 192 && buf[1] <= 223
|
||||
}
|
||||
|
||||
// MatchSRTP is a MatchFunc that only matches SRTP and not SRTCP
|
||||
func MatchSRTP(buf []byte) bool {
|
||||
return MatchSRTPOrSRTCP(buf) && !isRTCP(buf)
|
||||
}
|
||||
|
||||
// MatchSRTCP is a MatchFunc that only matches SRTCP and not SRTP
|
||||
func MatchSRTCP(buf []byte) bool {
|
||||
return MatchSRTPOrSRTCP(buf) && isRTCP(buf)
|
||||
}
|
||||
75
server/vendor/github.com/pion/webrtc/v3/internal/util/util.go
generated
vendored
Normal file
75
server/vendor/github.com/pion/webrtc/v3/internal/util/util.go
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package util provides auxiliary functions internally used in webrtc package
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/randutil"
|
||||
)
|
||||
|
||||
const (
|
||||
runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
)
|
||||
|
||||
// Use global random generator to properly seed by crypto grade random.
|
||||
var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals
|
||||
|
||||
// MathRandAlpha generates a mathmatical random alphabet sequence of the requested length.
|
||||
func MathRandAlpha(n int) string {
|
||||
return globalMathRandomGenerator.GenerateString(n, runesAlpha)
|
||||
}
|
||||
|
||||
// RandUint32 generates a mathmatical random uint32.
|
||||
func RandUint32() uint32 {
|
||||
return globalMathRandomGenerator.Uint32()
|
||||
}
|
||||
|
||||
// FlattenErrs flattens multiple errors into one
|
||||
func FlattenErrs(errs []error) error {
|
||||
errs2 := []error{}
|
||||
for _, e := range errs {
|
||||
if e != nil {
|
||||
errs2 = append(errs2, e)
|
||||
}
|
||||
}
|
||||
if len(errs2) == 0 {
|
||||
return nil
|
||||
}
|
||||
return multiError(errs2)
|
||||
}
|
||||
|
||||
type multiError []error //nolint:errname
|
||||
|
||||
func (me multiError) Error() string {
|
||||
var errstrings []string
|
||||
|
||||
for _, err := range me {
|
||||
if err != nil {
|
||||
errstrings = append(errstrings, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errstrings) == 0 {
|
||||
return "multiError must contain multiple error but is empty"
|
||||
}
|
||||
|
||||
return strings.Join(errstrings, "\n")
|
||||
}
|
||||
|
||||
func (me multiError) Is(err error) bool {
|
||||
for _, e := range me {
|
||||
if errors.Is(e, err) {
|
||||
return true
|
||||
}
|
||||
if me2, ok := e.(multiError); ok { //nolint:errorlint
|
||||
if me2.Is(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user