├── .gitignore
├── 320x240.h264
├── config.json
├── go.mod
├── README.md
├── main.go
├── pkg
├── server
│ ├── server.go
│ └── handlers.go
├── webrtc
│ ├── client.go
│ └── signalling.go
├── stream
│ └── stream.go
└── h264
│ └── payloader.go
├── src
└── html
│ └── index.html
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | ffmpeg-webrtc
2 | *.swp
3 | *.swo
4 |
--------------------------------------------------------------------------------
/320x240.h264:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwoja22/ffmpeg-webrtc/HEAD/320x240.h264
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "app":"ffmpeg",
3 | "args":[
4 | "-re",
5 | "-i",
6 | "320x240.h264",
7 | "-f",
8 | "h264",
9 | "-c:v",
10 | "copy",
11 | "pipe:pipe1"
12 | ],
13 | "from_file":true,
14 | "pipe_name":"pipe1"
15 | }
16 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module ffmpeg-webrtc
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/google/uuid v1.3.1
7 | github.com/gorilla/mux v1.8.0
8 | github.com/gorilla/websocket v1.5.0
9 | github.com/pion/interceptor v0.1.19
10 | github.com/pion/mdns v0.0.9 // indirect
11 | github.com/pion/rtcp v1.2.10
12 | github.com/pion/rtp v1.8.1
13 | github.com/pion/sctp v1.8.9 // indirect
14 | github.com/pion/transport/v2 v2.2.4 // indirect
15 | github.com/pion/turn/v2 v2.1.4 // indirect
16 | github.com/pion/webrtc/v3 v3.2.21
17 | golang.org/x/sys v0.12.0
18 | )
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ffmpeg-webrtc
2 | ffmpeg-webrtc is an example app that demonstrates how to stream a h264 capable web cam via Pion WebRTC on linux based systems
3 |
4 | ## Dependencies
5 | * ffmpeg
6 | * v4l2
7 | * h264 capable usb cam
8 |
9 | ## Instructions
10 | Install v4l-utils
11 | ```
12 | sudo apt-get install v4l-utils
13 | ```
14 | Install ffmpeg
15 | ```
16 | sudo apt-get install ffmpeg
17 | ```
18 | Build
19 | ```
20 | git clone https://github.com/dwoja22/ffmpeg-webrtc.git
21 | cd ffmpeg-webrtc
22 | go build
23 | ```
24 | Run it
25 | ```
26 | ./ffmpeg-webrtc
27 | ```
28 | * open Firefox or Google Chrome and navigate to localhost:7000
29 | * click play
30 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "ffmpeg-webrtc/pkg/stream"
5 | "log"
6 | "os"
7 | "os/signal"
8 | "runtime/pprof"
9 | "syscall"
10 | )
11 |
12 | func main() {
13 | //cpu profiling
14 | f, err := os.Create("cpu.prof")
15 | if err != nil {
16 | log.Fatal(err)
17 | }
18 |
19 | if err := pprof.StartCPUProfile(f); err != nil {
20 | log.Fatal(err)
21 | }
22 |
23 | defer pprof.StopCPUProfile()
24 |
25 | stream, err := stream.NewStream()
26 | if err != nil {
27 | log.Fatal(err)
28 | }
29 |
30 | if err := stream.Start(); err != nil {
31 | log.Fatal(err)
32 | }
33 |
34 | osSignals := make(chan os.Signal, 1)
35 | signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM)
36 | <-osSignals
37 | stream.Stop()
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "ffmpeg-webrtc/pkg/webrtc"
6 | "fmt"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | type Server struct {
14 | room *webrtc.Room
15 | done chan bool
16 | }
17 |
18 | func NewServer(room *webrtc.Room, done chan bool) *Server {
19 | return &Server{
20 | room: room,
21 | done: done,
22 | }
23 | }
24 |
25 | func (s *Server) Start() {
26 | //create a server instance
27 | server := &http.Server{
28 | Addr: ":7000",
29 | ReadTimeout: 5 * time.Second,
30 | WriteTimeout: 5 * time.Second,
31 | Handler: nil,
32 | }
33 |
34 | router := mux.NewRouter()
35 | server.Handler = router
36 |
37 | registerHandlers(router, s.room)
38 |
39 | ctx, cancel := context.WithCancel(context.Background())
40 |
41 | serverErrors := make(chan error, 1)
42 |
43 | go func() {
44 | <-s.done
45 | cancel()
46 | }()
47 |
48 | go func() {
49 | fmt.Println("server is ready to handle requests at", server.Addr)
50 | serverErrors <- server.ListenAndServe()
51 | }()
52 |
53 | select {
54 | case err := <-serverErrors:
55 | fmt.Println(err)
56 | case <-ctx.Done():
57 | fmt.Println("shutting down the server")
58 | server.Shutdown(ctx)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/server/handlers.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "ffmpeg-webrtc/pkg/webrtc"
5 | "log"
6 | "net/http"
7 | "text/template"
8 |
9 | "github.com/gorilla/mux"
10 | "github.com/gorilla/websocket"
11 | )
12 |
13 | func indexHandler(t *template.Template) http.HandlerFunc {
14 | return func(w http.ResponseWriter, r *http.Request) {
15 | templ := t.Lookup("index.html")
16 |
17 | if templ == nil {
18 | http.Error(w, "Could not find template", http.StatusInternalServerError)
19 | return
20 | }
21 |
22 | host := r.Host
23 |
24 | if err := templ.Execute(w, host); err != nil {
25 | http.Error(w, "Could not execute template", http.StatusInternalServerError)
26 | }
27 | }
28 | }
29 |
30 | func wsHandler(room *webrtc.Room) http.HandlerFunc {
31 | return func(w http.ResponseWriter, r *http.Request) {
32 | upgrader := websocket.Upgrader{}
33 |
34 | conn, err := upgrader.Upgrade(w, r, nil)
35 | if err != nil {
36 | log.Println("could not upgrade connection to websocket.", err)
37 | return
38 | }
39 |
40 | clientID := r.URL.Query().Get("clientID")
41 | if clientID == "" {
42 | log.Println("clientID is required")
43 | return
44 | }
45 |
46 | client := webrtc.NewClient(conn, clientID, room)
47 | room.Register <- client
48 |
49 | go client.Read()
50 | go client.Write()
51 | }
52 | }
53 |
54 | func registerHandlers(mux *mux.Router, room *webrtc.Room) {
55 | indexTemplate := template.Must(template.ParseFiles("src/html/index.html"))
56 | mux.HandleFunc("/", indexHandler(indexTemplate))
57 | mux.HandleFunc("/ws", wsHandler(room))
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/webrtc/client.go:
--------------------------------------------------------------------------------
1 | package webrtc
2 |
3 | import (
4 | "ffmpeg-webrtc/pkg/h264"
5 | "fmt"
6 | "log"
7 | "time"
8 |
9 | "github.com/gorilla/websocket"
10 | "github.com/pion/interceptor/pkg/cc"
11 | "github.com/pion/rtcp"
12 | "github.com/pion/rtp"
13 | "github.com/pion/webrtc/v3"
14 | )
15 |
16 | type Client struct {
17 | id string
18 | conn *websocket.Conn
19 | send chan []byte
20 | room *Room
21 | Track *webrtc.TrackLocalStaticRTP
22 | SSRC webrtc.SSRC
23 | RTPSender *webrtc.RTPSender
24 | PC *webrtc.PeerConnection
25 | Estimator cc.BandwidthEstimator
26 | Packets chan *rtp.Packet
27 | Frames chan []byte
28 | done chan bool
29 | }
30 |
31 | func NewClient(conn *websocket.Conn, clientID string, room *Room) *Client {
32 | client := Client{
33 | id: clientID,
34 | conn: conn,
35 | send: make(chan []byte, 1),
36 | room: room,
37 | Packets: make(chan *rtp.Packet, 240),
38 | Frames: make(chan []byte, 240),
39 | done: make(chan bool, 1),
40 | }
41 |
42 | return &client
43 | }
44 |
45 | func (c *Client) Read() {
46 | for {
47 | _, msg, err := c.conn.ReadMessage()
48 | if err != nil {
49 | log.Println(err)
50 | return
51 | }
52 | c.room.Broadcast <- msg
53 | }
54 | }
55 |
56 | func (c *Client) Write() {
57 | for {
58 | msg := <-c.send
59 | err := c.conn.WriteMessage(websocket.TextMessage, msg)
60 | if err != nil {
61 | return
62 | }
63 | }
64 | }
65 |
66 | func (c *Client) WriteRTP() {
67 | payloader := h264.NewPayloader()
68 | packetizer := rtp.NewPacketizer(1460, 96, uint32(c.SSRC), payloader, rtp.NewRandomSequencer(), 90000)
69 |
70 | for {
71 | select {
72 | case packet := <-c.Packets:
73 | c.Track.WriteRTP(packet)
74 | case frame := <-c.Frames:
75 | packets := packetizer.Packetize(frame, uint32(160))
76 | for _, packet := range packets {
77 | c.Track.WriteRTP(packet)
78 | }
79 | case <-c.done:
80 | return
81 | }
82 | }
83 | }
84 |
85 | func (c *Client) ReadRTCP() {
86 | for {
87 | select {
88 | case <-c.done:
89 | return
90 | default:
91 | rtcpPackets, _, err := c.RTPSender.ReadRTCP()
92 | if err != nil {
93 | fmt.Println("could not read rtcp:", err)
94 | return
95 | }
96 |
97 | for _, packet := range rtcpPackets {
98 | switch packet.(type) {
99 | case *rtcp.PictureLossIndication:
100 | fmt.Println("received pli")
101 | case *rtcp.TransportLayerNack:
102 | fmt.Println("received nack")
103 | fmt.Println(packet.(*rtcp.TransportLayerNack).Nacks)
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
110 | func (c *Client) BandwidthEstimator() {
111 | ticker := time.NewTicker(100 * time.Millisecond)
112 |
113 | // Keep a table of powers to units for fast conversion.
114 | bitUnits := []string{"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"}
115 |
116 | // Do some unit conversions because b/s is far too difficult to read.
117 | powers := 0
118 |
119 | for {
120 | select {
121 | case <-ticker.C:
122 | bitrate := float64(c.Estimator.GetTargetBitrate())
123 | // Keep dividing the bitrate until it's under 1000
124 | for bitrate >= 1000.0 && powers < len(bitUnits) {
125 | bitrate /= 1000.0
126 | powers++
127 | }
128 |
129 | unit := bitUnits[powers]
130 | powers = 0
131 |
132 | fmt.Printf("client %v estimated available bandwidth: %.2f %s/s\n", c.id, bitrate, unit)
133 | case <-c.done:
134 | return
135 | }
136 | }
137 | }
138 |
139 | func (c *Client) Send(msg []byte) {
140 | c.send <- msg
141 | }
142 |
143 | func (c *Client) Stop() {
144 | if c.conn != nil {
145 | c.conn.Close()
146 | }
147 |
148 | select {
149 | case <-c.done:
150 | return
151 | default:
152 | close(c.done)
153 | }
154 | }
155 |
156 | func (c *Client) Room() *Room {
157 | return c.room
158 | }
159 |
160 | func (c *Client) Conn() *websocket.Conn {
161 | return c.conn
162 | }
163 |
--------------------------------------------------------------------------------
/src/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebRTC
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
151 |
--------------------------------------------------------------------------------
/pkg/stream/stream.go:
--------------------------------------------------------------------------------
1 | package stream
2 |
3 | import (
4 | "encoding/json"
5 | "ffmpeg-webrtc/pkg/server"
6 | wbrtc "ffmpeg-webrtc/pkg/webrtc"
7 | "fmt"
8 | "io/ioutil"
9 | "os"
10 | "os/exec"
11 | "syscall"
12 | "time"
13 |
14 | "github.com/pion/webrtc/v3"
15 | "golang.org/x/sys/unix"
16 | )
17 |
18 | const H264FRAMEDURATION = time.Millisecond * 33
19 |
20 | type Stream struct {
21 | App string `json:"app"`
22 | Args []string `json:"args"`
23 | Type string `json:"type"`
24 | PipeName string `json:"pipe_name"`
25 | FromFile bool `json:"from_file"`
26 | room *wbrtc.Room
27 | server *server.Server
28 | cmd *exec.Cmd
29 | done chan bool
30 | pipe *os.File
31 | logger *os.File
32 | }
33 |
34 | func NewStream() (*Stream, error) {
35 | var stream Stream
36 |
37 | config, err := ioutil.ReadFile("config.json")
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | if err = json.Unmarshal(config, &stream); err != nil {
43 | return nil, err
44 | }
45 |
46 | if _, err := exec.LookPath(stream.App); err != nil {
47 | return nil, fmt.Errorf("app %s does not exist", stream.App)
48 | }
49 |
50 | if len(stream.Args) == 0 {
51 | return nil, fmt.Errorf("args cannot be empty")
52 | }
53 |
54 | if stream.PipeName == "" {
55 | return nil, fmt.Errorf("pipe_name must not be empty")
56 | }
57 |
58 | done := make(chan bool, 1)
59 | room := wbrtc.NewRoom(done)
60 | server := server.NewServer(room, done)
61 |
62 | stream.server = server
63 | stream.room = room
64 | stream.done = done
65 |
66 | return &stream, nil
67 | }
68 |
69 | func (s *Stream) Start() error {
70 | cmd := exec.Command(s.App, s.Args...)
71 | s.cmd = cmd
72 |
73 | fmt.Println(cmd.Args)
74 |
75 | if err := s.initIO(cmd); err != nil {
76 | return err
77 | }
78 |
79 | go s.server.Start()
80 | go s.room.Start()
81 |
82 | if s.FromFile {
83 | if err := s.streamFromFile(); err != nil {
84 | return err
85 | }
86 | } else {
87 | if err := s.streamFromDevice(); err != nil {
88 | return err
89 | }
90 | }
91 |
92 | return nil
93 | }
94 |
95 | func (s *Stream) Stop() error {
96 | //stop the ffmpeg process
97 | s.cmd.Process.Signal(syscall.SIGTERM)
98 |
99 | //close the pipe
100 | if err := s.pipe.Close(); err != nil {
101 | return fmt.Errorf("error closing pipe: %v", err)
102 | }
103 |
104 | s.logger.Close()
105 |
106 | close(s.done)
107 |
108 | return nil
109 | }
110 |
111 | func (s *Stream) initIO(cmd *exec.Cmd) error {
112 | if _, err := os.Stat(s.PipeName); os.IsNotExist(err) {
113 | if err := syscall.Mkfifo(s.PipeName, 0666); err != nil {
114 | return fmt.Errorf("error creating named pipe: %v", err)
115 | }
116 | }
117 |
118 | //set pipe to non-blocking
119 | pipe, err := os.OpenFile(s.PipeName, os.O_RDWR|syscall.O_NONBLOCK, os.ModeNamedPipe)
120 | if err != nil {
121 | return fmt.Errorf("error opening named pipe: %v", err)
122 | }
123 |
124 | //set the pipe size to 1MB
125 | if _, err := unix.FcntlInt(pipe.Fd(), syscall.F_SETPIPE_SZ, 1024*1024); err != nil {
126 | return fmt.Errorf("error setting pipe size: %v", err)
127 | }
128 |
129 | //check size of pipe
130 | pipeSize, err := unix.FcntlInt(pipe.Fd(), syscall.F_GETPIPE_SZ, 0)
131 | if err != nil {
132 | return fmt.Errorf("error getting pipe size: %v", err)
133 | }
134 |
135 | fmt.Printf("created named pipe with name %v and size %v\n", s.PipeName, pipeSize)
136 |
137 | s.pipe = pipe
138 |
139 | //create log file for app
140 | logger, err := os.OpenFile(s.App+".log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
141 | if err != nil {
142 | fmt.Printf("error creating log file: %v\n", err)
143 | }
144 |
145 | //set io for cmd
146 | s.logger = logger
147 | cmd.Stderr = logger
148 | cmd.Stdout = pipe
149 |
150 | return nil
151 | }
152 |
153 | func (s *Stream) streamFromFile() error {
154 | connected := false
155 |
156 | for !connected {
157 | if len(s.room.Clients) < 1 {
158 | continue
159 | }
160 |
161 | for _, client := range s.room.Clients {
162 | if client.PC == nil {
163 | continue
164 | }
165 |
166 | if client.PC.ConnectionState() == webrtc.PeerConnectionStateConnected {
167 | connected = true
168 | break
169 | }
170 | }
171 | }
172 |
173 | go s.stream()
174 |
175 | return nil
176 | }
177 |
178 | func (s *Stream) streamFromDevice() error {
179 | go s.stream()
180 |
181 | return nil
182 | }
183 |
184 | func (s *Stream) stream() {
185 | buf := make([]byte, 1024*1024)
186 | frames := make(chan []byte, 240)
187 |
188 | go func() {
189 | for {
190 | n, err := s.pipe.Read(buf)
191 | if err != nil {
192 | continue
193 | }
194 |
195 | frames <- buf[:n]
196 | }
197 | }()
198 |
199 | go func() {
200 | for frame := range frames {
201 | for _, client := range s.room.Clients {
202 | if client.PC != nil {
203 | if client.PC.ConnectionState() == webrtc.PeerConnectionStateConnected {
204 | client.Frames <- frame
205 | }
206 | }
207 | }
208 | }
209 | }()
210 |
211 | s.cmd.Start()
212 | }
213 |
--------------------------------------------------------------------------------
/pkg/h264/payloader.go:
--------------------------------------------------------------------------------
1 | package h264
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | type Payloader struct {
9 | SPS []byte
10 | PPS []byte
11 | }
12 |
13 | func NewPayloader() *Payloader {
14 | return &Payloader{}
15 | }
16 |
17 | const (
18 | NALU_TYPE_P = 1
19 | NALU_TYPE_DPA = 2
20 | NALU_TYPE_DPB = 3
21 | NALU_TYPE_DPC = 4
22 | NALU_TYPE_IDR = 5
23 | NALU_TYPE_SEI = 6
24 | NALU_TYPE_SPS = 7
25 | NALU_TYPE_PPS = 8
26 | NALU_TYPE_AUD = 9
27 | NALU_TYPE_EOSEQ = 10
28 | NALU_TYPE_EOSTR = 11
29 | NALU_TYPE_FILL = 12
30 |
31 | NALU_TYPE_STAPA = 24
32 | NALU_TYPE_FUA = 28
33 | NALU_TYPE_FUB = 29
34 |
35 | //0x78 = 01111000
36 | //0 maps to the F bit, 11 maps to NRI, 11000 maps to type 24
37 | //0, 11, 11000 = 0, 3, 24, false, highest priority, stap-a nal type id
38 | STAP_A_HEADER = 0x78
39 | NAL_REF_IDC = 0x60
40 |
41 | FUA_HEADER_SIZE = 2
42 | )
43 |
44 | //findNal finds the start code prefix of a nal unit and returns the start index and length of the prefix
45 | //-1 indicates that the nal is at the beginning of the data
46 | //example
47 | //0 0 0 1 - prefixLength = 4
48 | //0 1 2 - zero count as we iterate through the data
49 | //0 + 2 - 3 = -1, 3 + 1 = 4 - nal at the beginning of the data, represented by -1, 4 for the prefix length
50 | func findNal(data []byte, start int) (prefixStart, prefixLength int) {
51 | zeros := 0
52 |
53 | for i, b := range data[start:] {
54 | if b == 0 {
55 | zeros++
56 | continue
57 | } else if b == 1 {
58 | if zeros >= 2 { //make sure we have at least 2 zeros, otherwise it's just a random 1
59 | return start + i - zeros, zeros + 1
60 | }
61 | }
62 |
63 | //reset the counter to start counting zeros again for finding the next nal
64 | zeros = 0
65 | }
66 | return -1, -1
67 | }
68 |
69 | //extractNalUnits extracts all nal units from the data, works with both single and multiple nal units
70 | //as well as with prefix 0 0 1 and 0 0 0 1 or a mix of both
71 | func extractNalUnits(data []byte, extractNal func([]byte)) {
72 | nalStart, prefixLength := findNal(data, 0)
73 |
74 | //single nal unit or nal at the start of the data
75 | if nalStart == -1 {
76 | extractNal(data)
77 | } else {
78 | for nalStart != -1 {
79 | prevNalStart := nalStart + prefixLength
80 | //find the next nal unit
81 | nalStart, prefixLength = findNal(data, prevNalStart)
82 | if nalStart != -1 {
83 | extractNal(data[prevNalStart:nalStart])
84 | } else {
85 | //nal unit is at the end of the data
86 | extractNal(data[prevNalStart:])
87 | }
88 | }
89 | }
90 | }
91 |
92 | func (p *Payloader) Payload(mtu uint16, data []byte) [][]byte {
93 | var payloads [][]byte
94 |
95 | extractNalUnits(data, func(nal []byte) {
96 | if len(nal) == 0 {
97 | return
98 | }
99 |
100 | naltype := nal[0] & 0x1F
101 | //this is the NRI (nal reference index) and is the priority of the nal unit, possible values are 0, 1, 2, 3 and are used to determine the priority of the nal
102 | nalRefIdc := nal[0] & 0x60
103 |
104 | if naltype == NALU_TYPE_SPS {
105 | p.SPS = nal
106 | return
107 | }
108 |
109 | if naltype == NALU_TYPE_PPS {
110 | p.PPS = nal
111 | return
112 | }
113 |
114 | if naltype == NALU_TYPE_AUD || naltype == NALU_TYPE_FILL {
115 | return
116 | }
117 |
118 | if p.SPS != nil && p.PPS != nil {
119 | spsLen := make([]byte, 2)
120 | binary.BigEndian.PutUint16(spsLen, uint16(len(p.SPS)))
121 |
122 | ppsLen := make([]byte, 2)
123 | binary.BigEndian.PutUint16(ppsLen, uint16(len(p.PPS)))
124 |
125 | stapANalu := []byte{STAP_A_HEADER}
126 | stapANalu = append(stapANalu, spsLen...)
127 | stapANalu = append(stapANalu, p.SPS...)
128 | stapANalu = append(stapANalu, ppsLen...)
129 | stapANalu = append(stapANalu, p.PPS...)
130 |
131 | if len(stapANalu) <= int(mtu) {
132 | nalOut := make([]byte, len(stapANalu))
133 | copy(nalOut, stapANalu)
134 | payloads = append(payloads, nalOut)
135 | }
136 |
137 | p.SPS = nil
138 | p.PPS = nil
139 | }
140 |
141 | if len(nal) <= int(mtu) {
142 | nalOut := make([]byte, len(nal))
143 | copy(nalOut, nal)
144 | payloads = append(payloads, nalOut)
145 |
146 | return
147 | }
148 |
149 | //Package as STAP-A, non-interleaved
150 | //Nal unit is too big to fit into a single RTP packet and needs to be split into multiple FU-A packets
151 | //FU-A header is 2 bytes long
152 | maxFragmentSize := int(mtu) - FUA_HEADER_SIZE
153 |
154 | nalData := nal
155 | nalDataIndex := 1
156 | nalDataLength := len(nal) - nalDataIndex
157 | nalDataRemaining := nalDataLength
158 |
159 | if min(maxFragmentSize, nalDataRemaining) <= 0 {
160 | return
161 | }
162 |
163 | for nalDataRemaining > 0 {
164 | currentFragmentSize := min(maxFragmentSize, nalDataRemaining)
165 | nalOut := make([]byte, currentFragmentSize+FUA_HEADER_SIZE)
166 |
167 | //set the FU indicator
168 | nalOut[0] = NALU_TYPE_FUA
169 | //set the NRI, which is the priority of the nal unit
170 | nalOut[0] |= nalRefIdc
171 | //set the type of the nal unit
172 | nalOut[1] = naltype
173 |
174 | if nalDataRemaining == nalDataLength {
175 | //set the start bit
176 | nalOut[1] |= 1 << 7
177 | } else if nalDataRemaining-currentFragmentSize == 0 {
178 | //set the end bit
179 | nalOut[1] |= 1 << 6
180 | }
181 |
182 | copy(nalOut[FUA_HEADER_SIZE:], nalData[nalDataIndex:nalDataIndex+currentFragmentSize])
183 | payloads = append(payloads, nalOut)
184 |
185 | nalDataRemaining -= currentFragmentSize
186 | nalDataIndex += currentFragmentSize
187 | }
188 | })
189 |
190 | return payloads
191 | }
192 |
193 | func nalType(data []byte) {
194 | //check for nal unit type
195 | //the nal unit type is the last 5 bits of the byte, so we need to mask the byte with 0x1F, which is 0001 1111
196 | //the bitwise AND operator will return the last 5 bits of the byte, nalType will be a value between 0 and 31
197 | //we are using 0001 1111 because the nal unit type can have a maximum value of 31, 11111 is 31 in binary with 000 masking the first 3 bits
198 |
199 | //check for nal ref idc
200 | //the nal ref idc is the last 2 bits of the byte, so we need to mask the byte with 0x03, which is 0000 0011
201 | //the bitwise AND operator will return the last 2 bits of the byte, nalRefIdc will be a value between 0 and 3
202 |
203 | nalRefIdc := (data[0] >> 5) & 0x03
204 | nalType := data[0] & 0x1F
205 |
206 | switch nalType {
207 | case NALU_TYPE_SPS:
208 | fmt.Println("found SPS")
209 | case NALU_TYPE_PPS:
210 | fmt.Println("found PPS")
211 | case NALU_TYPE_P:
212 | fmt.Println("found P")
213 | case NALU_TYPE_IDR:
214 | fmt.Println("found I")
215 | case NALU_TYPE_SEI:
216 | fmt.Println("found SEI")
217 | case NALU_TYPE_DPA:
218 | fmt.Println("found DPA")
219 | case NALU_TYPE_DPB:
220 | fmt.Println("found DPB")
221 | case NALU_TYPE_DPC:
222 | fmt.Println("found DPC")
223 | case NALU_TYPE_AUD:
224 | fmt.Println("found AUD")
225 | case NALU_TYPE_EOSEQ:
226 | fmt.Println("found EOSEQ")
227 | case NALU_TYPE_EOSTR:
228 | fmt.Println("found EOSTR")
229 | case NALU_TYPE_FILL:
230 | fmt.Println("found FILL")
231 | default:
232 | fmt.Println("found unknown nal unit type with value: ", nalType, " and nal ref idc: ", nalRefIdc)
233 | }
234 | }
235 |
236 | func min(a, b int) int {
237 | if a < b {
238 | return a
239 | }
240 | return b
241 | }
242 |
--------------------------------------------------------------------------------
/pkg/webrtc/signalling.go:
--------------------------------------------------------------------------------
1 | package webrtc
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "sync"
7 |
8 | "github.com/google/uuid"
9 | "github.com/pion/interceptor"
10 | "github.com/pion/interceptor/pkg/cc"
11 | "github.com/pion/interceptor/pkg/gcc"
12 | "github.com/pion/webrtc/v3"
13 | )
14 |
15 | const (
16 | OFFER = iota
17 | ANSWER
18 | ICECANDIDATE
19 | STOP
20 | )
21 |
22 | const (
23 | LowBitrate = 100_000
24 | MidBitrate = 300_000
25 | HighBitrate = 500_000
26 | VeryHighBitrate = 1_000_000
27 | )
28 |
29 | type Room struct {
30 | Clients map[string]*Client
31 | Broadcast chan []byte
32 | Register chan *Client
33 | Unregister chan *Client
34 | done chan bool
35 | mu sync.Mutex
36 | }
37 |
38 | func NewRoom(done chan bool) *Room {
39 | return &Room{
40 | Clients: make(map[string]*Client),
41 | Broadcast: make(chan []byte, 1),
42 | Register: make(chan *Client, 1),
43 | Unregister: make(chan *Client, 1),
44 | mu: sync.Mutex{},
45 | done: done,
46 | }
47 | }
48 |
49 | func (r *Room) Start() {
50 | for {
51 | select {
52 | case client := <-r.Register:
53 | fmt.Println("registering client with id: ", client.id)
54 | r.mu.Lock()
55 | r.Clients[client.id] = client
56 | r.mu.Unlock()
57 | case client := <-r.Unregister:
58 | if _, ok := r.Clients[client.id]; ok {
59 | r.mu.Lock()
60 | delete(r.Clients, client.id)
61 | r.mu.Unlock()
62 | close(client.send)
63 | }
64 | case msg := <-r.Broadcast:
65 | var m Message
66 | if err := json.Unmarshal(msg, &m); err != nil {
67 | fmt.Println(err)
68 | continue
69 | }
70 |
71 | client, exists := r.Clients[m.ClientID]
72 | if !exists {
73 | fmt.Println("client does not exist")
74 | continue
75 | }
76 |
77 | if m.Kind == OFFER {
78 | mediaEngine := webrtc.MediaEngine{}
79 |
80 | codec := webrtc.RTPCodecCapability{
81 | MimeType: webrtc.MimeTypeH264,
82 | ClockRate: 90000,
83 | Channels: 0,
84 | SDPFmtpLine: "packetization-mode=1",
85 | RTCPFeedback: []webrtc.RTCPFeedback{
86 | {Type: "nack"},
87 | {Type: "nack", Parameter: "pli"},
88 | {Type: "ccm", Parameter: "fir"},
89 | {Type: "goog-remb"},
90 | {Type: "transport-cc"},
91 | },
92 | }
93 |
94 | if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{RTPCodecCapability: codec, PayloadType: 96}, webrtc.RTPCodecTypeVideo); err != nil {
95 | fmt.Println("error registering codec: ", err)
96 | }
97 |
98 | interceptorRegistry := interceptor.Registry{}
99 |
100 | congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) {
101 | return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(LowBitrate))
102 | })
103 |
104 | if err != nil {
105 | fmt.Println("error creating congestion controller: ", err)
106 | }
107 |
108 | estimatorChan := make(chan cc.BandwidthEstimator, 1)
109 |
110 | congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) {
111 | estimatorChan <- estimator
112 | })
113 |
114 | interceptorRegistry.Add(congestionController)
115 |
116 | if err = webrtc.ConfigureTWCCHeaderExtensionSender(&mediaEngine, &interceptorRegistry); err != nil {
117 | fmt.Println("error registering default interceptors: ", err)
118 | }
119 |
120 | if err = webrtc.RegisterDefaultInterceptors(&mediaEngine, &interceptorRegistry); err != nil {
121 | fmt.Println("error registering default interceptors: ", err)
122 | }
123 |
124 | api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine), webrtc.WithInterceptorRegistry(&interceptorRegistry))
125 |
126 | peerConnection, err := api.NewPeerConnection(webrtc.Configuration{PeerIdentity: m.ClientID, ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}})
127 | if err != nil {
128 | fmt.Println("error creating peer connection: ", err)
129 | continue
130 | }
131 |
132 | client.PC = peerConnection
133 | client.Estimator = <-estimatorChan
134 |
135 | r.HandlePeer(peerConnection, client.id)
136 |
137 | if err := peerConnection.SetRemoteDescription(m.Offer); err != nil {
138 | fmt.Println("error setting remote description: ", err)
139 | continue
140 | }
141 |
142 | peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
143 | if candidate == nil {
144 | return
145 | }
146 |
147 | msg := Message{
148 | ClientID: client.id,
149 | Kind: ICECANDIDATE,
150 | ICECandidate: candidate,
151 | }
152 |
153 | msgJSON, err := json.Marshal(msg)
154 | if err != nil {
155 | fmt.Println("error marshalling iceCandidate message: ", err)
156 | return
157 | }
158 |
159 | client.Send(msgJSON)
160 | })
161 |
162 | _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
163 | if err != nil {
164 | fmt.Println("error adding transceiver: ", err)
165 | continue
166 | }
167 |
168 | streamID := uuid.New().String()
169 | trackID := uuid.New().String()
170 |
171 | trackLocalStaticRTP, err := webrtc.NewTrackLocalStaticRTP(codec, streamID, trackID)
172 | if err != nil {
173 | fmt.Println("error creating rtp track: ", err)
174 | }
175 |
176 | rtpSender, err := peerConnection.AddTrack(trackLocalStaticRTP)
177 | if err != nil {
178 | fmt.Println("error adding rtp video track: ", err)
179 | continue
180 | }
181 |
182 | encoding := rtpSender.GetParameters().Encodings
183 |
184 | client.Track = trackLocalStaticRTP
185 | client.RTPSender = rtpSender
186 | client.SSRC = encoding[0].SSRC
187 |
188 | answer, err := peerConnection.CreateAnswer(nil)
189 | if err != nil {
190 | fmt.Println("error creating answer: ", err)
191 | continue
192 | }
193 |
194 | if err := peerConnection.SetLocalDescription(answer); err != nil {
195 | fmt.Println("error setting local description: ", err)
196 | continue
197 | }
198 |
199 | msg := Message{
200 | ClientID: client.id,
201 | Kind: ANSWER,
202 | Answer: answer,
203 | }
204 |
205 | msgJSON, err := json.Marshal(msg)
206 | if err != nil {
207 | fmt.Println("error marshalling answer message: ", err)
208 | continue
209 | }
210 |
211 | client.Send(msgJSON)
212 | }
213 |
214 | if m.Kind == ICECANDIDATE {
215 | fmt.Println("iceCandidate from client received")
216 | fmt.Println("iceCandidate: ", m.ClientICECandidate)
217 |
218 | err := client.PC.AddICECandidate(m.ClientICECandidate)
219 | if err != nil {
220 | fmt.Println("error adding ice candidate: ", err)
221 | }
222 | continue
223 | }
224 |
225 | //TODO: handle stop
226 | if m.Kind == STOP {
227 | fmt.Println("stop from client received")
228 | client.Stop()
229 | }
230 | }
231 | }
232 | }
233 |
234 | func (r *Room) HandlePeer(pc *webrtc.PeerConnection, clientID string) {
235 | pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
236 | fmt.Printf("ICE connection state for peer:%v has changed:%v\n", clientID, connectionState.String())
237 |
238 | if connectionState == webrtc.ICEConnectionStateConnected {
239 | fmt.Println("peer connected")
240 |
241 | go r.Clients[clientID].WriteRTP()
242 | go r.Clients[clientID].ReadRTCP()
243 | go r.Clients[clientID].BandwidthEstimator()
244 |
245 | return
246 | }
247 |
248 | if connectionState == webrtc.ICEConnectionStateDisconnected {
249 | fmt.Printf("peer %v disconnected\n", clientID)
250 | r.RemoveClient(clientID)
251 | return
252 | }
253 |
254 | if connectionState == webrtc.ICEConnectionStateFailed {
255 | fmt.Printf("peer %v failed\n", clientID)
256 | return
257 | }
258 | })
259 | }
260 |
261 | func (r *Room) RemoveClient(clientID string) {
262 | r.Clients[clientID].Stop()
263 | r.mu.Lock()
264 | delete(r.Clients, clientID)
265 | r.mu.Unlock()
266 | }
267 |
268 | type Message struct {
269 | ClientID string `json:"client_id"`
270 | Kind int `json:"kind"`
271 | Offer webrtc.SessionDescription `json:"offer"`
272 | Answer webrtc.SessionDescription `json:"answer"`
273 | ICECandidate *webrtc.ICECandidate `json:"ice_candidate"`
274 | ClientICECandidate webrtc.ICECandidateInit `json:"client_ice_candidate"`
275 | }
276 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
6 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
8 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
9 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
10 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
11 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
12 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
13 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
14 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
15 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
16 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
17 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
18 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
20 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
21 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
22 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
23 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
24 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
25 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
26 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
27 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
31 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
33 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
34 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
35 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
36 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
37 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
38 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
39 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
40 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
41 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
42 | github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
43 | github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
44 | github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
45 | github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
46 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
47 | github.com/pion/ice/v2 v2.3.2/go.mod h1:AMIpuJqcpe+UwloocNebmTSWhCZM1TUCo9v7nW50jX0=
48 | github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
49 | github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
50 | github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
51 | github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
52 | github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
53 | github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
54 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
55 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
56 | github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
57 | github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
58 | github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
59 | github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
60 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
61 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
62 | github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
63 | github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
64 | github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
65 | github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
66 | github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
67 | github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
68 | github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
69 | github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
70 | github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
71 | github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
72 | github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
73 | github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
74 | github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
75 | github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
76 | github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
77 | github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
78 | github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
79 | github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
80 | github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
81 | github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
82 | github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
83 | github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
84 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
85 | github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
86 | github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
87 | github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
88 | github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
89 | github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
90 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
91 | github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
92 | github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
93 | github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
94 | github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
95 | github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
96 | github.com/pion/webrtc/v3 v3.1.59 h1:B3YFo8q6dwBYKA2LUjWRChP59Qtt+xvv1Ul7UPDp6Zc=
97 | github.com/pion/webrtc/v3 v3.1.59/go.mod h1:rJGgStRoFyFOWJULHLayaimsG+jIEoenhJ5MB5gIFqw=
98 | github.com/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
99 | github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
100 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
101 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
102 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
104 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
105 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
106 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
107 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
108 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
109 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
110 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
111 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
112 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
113 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
114 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
115 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
116 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
117 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
118 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
119 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
120 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
121 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
122 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
123 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
124 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
125 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
126 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
127 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
128 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
129 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
130 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
131 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
132 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
133 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
134 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
135 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
136 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
137 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
138 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
139 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
140 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
141 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
142 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
143 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
144 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
145 | golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
146 | golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
147 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
148 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
149 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
150 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
151 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
153 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
154 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
155 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
156 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
157 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
158 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
159 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
160 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
161 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
162 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
163 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
164 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
165 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
166 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
167 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
168 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
169 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
170 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
171 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
172 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
173 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
174 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
175 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
176 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
177 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
178 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
179 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
180 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
181 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
182 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
183 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
184 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
185 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
186 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
187 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
188 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
189 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
190 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
191 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
192 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
193 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
194 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
195 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
196 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
197 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
198 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
199 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
200 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
201 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
202 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
203 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
204 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
205 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
206 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
207 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
208 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
209 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
210 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
211 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
212 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
213 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
214 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
215 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
216 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
217 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
218 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
219 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
220 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
221 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
222 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
223 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
224 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
225 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
226 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
227 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
228 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
229 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
230 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
231 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
232 |
--------------------------------------------------------------------------------