直播:后台 JWT 推流、前台画中画;WebRTC 服务与 Nginx WebSocket 代理

Made-with: Cursor
This commit is contained in:
whm
2026-03-25 15:00:14 +08:00
parent b83ec91b1a
commit 7811adca66
1050 changed files with 146524 additions and 37 deletions

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
}