├── .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 | --------------------------------------------------------------------------------