基础功能

This commit is contained in:
pans
2023-05-01 01:29:26 +08:00
parent 2692056ea7
commit 4870f3fb29
62 changed files with 4427 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
logFile
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# config配置文件
logFile
gb28181Panda
+84
View File
@@ -0,0 +1,84 @@
## 🐼项目描述
- GB28181服务
- 信令sip及路由采用go
- 流媒体采用夏楚大大的[ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
- 流媒体播放地址支持:rtsp、rtmp、wsFlv、m3u8、http-flv、fmp4、ts等地址
## 🔨技术栈
- Golang
- Sip
- ZLMediaKit
## 🚂搭配的前端项目
[gb28181PandaWeb](https://github.com/pans0930/gb28181PandaWeb)
## 🕹️线上测试系统地址
```
http://1.117.28.81:9999/
```
- 通道播放
![通道播放](./img/3.png)
![通道播放](./img/3-1.png)
- 历史视屏回放
![历史视屏回放](./img/4.png)
- 分屏展示效果
![分屏展示效果](./img/5.png)
![分屏展示效果](./img/6.png)
![分屏展示效果](./img/7.png)
![分屏展示效果](./img/8.png)
## 📺延迟效果
webrtc下大约在<=500ms左右,此情况根据不同的网络环境和服务器配置会有差距,延迟如下
- 摄像头webrtc延时如下
![image](./img/img1.png)
- 手机摄像头webrtc延时如下
![image](./img/img2.png)
## 📖已实现的功能
- [x] 下级设备的上报和通道上报
- [x] 下级设备的视频播放和视频停止
- [x] 下级级联设备的上报和通道的上报
- [x] 下级设备的视频播放和视频停止
- [x] 预警消息订阅
- [x] 历史视频的播放
- [x] ptz控制【无设备未测试】
- [x] 视频流传输模式:udp、tcp主动、tcp被动
## 📘TODO
- [ ] 国标级联
- [ ] 历史视频播放的seek操作-暂停、倍速、下载等
- [ ] 预警消息处理
- [ ] 位置订阅
- [ ] 上级级联播放本级视频
- [ ] 防止设备伪造请求,除了[Register],其他数据都要先查询来源【ip】和【port】在【t_device】中是否存在,存在放行数据,不存在直接丢弃
## 使用说明
- 相关配置项在config.yaml文件,请自行修改
- 数据库需导入sql文件夹下的2个sql文件
## 微信群
![image](./img/we.jpg)
## 📄windows下的编译命令
```shell
$env:CGO_ENABLED="0"
$env:GOOS="linux"
$env:GOARCH="amd64"
go build -o gb28181Panda main.go
```
```shell
其余平台编译方式请自行查阅go的交叉编译
```
+51
View File
@@ -0,0 +1,51 @@
# 开发模式, debug, release
runMode: release
#本地ip
ip: 127.0.0.1
# HTTP绑定端口
addr: :9090
# API Server的名字
name: gb28181Panda
# mysql配置信息
mysql:
username: root
password: 123456
host: 127.0.0.1
port: 3306
name: gb28181_panda
# redis配置信息
redis:
addr: 127.0.0.1:6379
password: 123456
dbIndex: 1
# ZLM的配置信息
media:
# [必修修改] zlm服务器的唯一id
id: zlm_id
# [必须修改] zlm服务器的内网IP 如果是外网的话需要设置外网ip地址
ip: 127.0.0.1
# [必须修改] zlm服务器的http.port
httpPort: 80
# [可选] zlm服务器的secret
secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
# rtsp的端口
rtspPort: 554
# rtmp的端口
rtmpPort: 1935
# 28181 服务器的配置
sip:
# [必须修改] 本机的IP
ip: "127.0.0.1"
# [可选] 28181服务监听的端口
port: 5060
# 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007
# 后两位为行业编码,定义参照附录D.3
# 3701020049标识山东济南历下区 信息行业接入
realm: "3402000000"
id: "34020000002000000001"
# [可选] 默认设备认证密码,移除密码将不进行校验
password: "123456"
# sip的头
userAgent: "gb28181Panda"
# 是否校验设备的域值 1 校验 0 不校验
checkRealm: 1
+27
View File
@@ -0,0 +1,27 @@
package config
import (
"github.com/spf13/viper"
)
func init() {
loadConfig()
}
func loadConfig() {
viper.AddConfigPath("./")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
panic("load config fail,please check your config file whether in config/ in the directory")
}
startOption()
}
func startOption() {
//加载配置项后加载zlm的配置
ZlmOp = InitZlmOptions()
//加载配置项后加载sip的配置
SipOp = InitSipOptions()
}
+24
View File
@@ -0,0 +1,24 @@
package config
import (
"github.com/spf13/viper"
)
type RedisOptions struct {
Addr string
Password string
DB int
}
func NewRedisOptions() *RedisOptions {
r := &RedisOptions{
Addr: "127.0.0.1:6379",
Password: "",
DB: 3,
}
err := viper.UnmarshalKey("redis", r)
if err != nil {
panic(err)
}
return r
}
+26
View File
@@ -0,0 +1,26 @@
package config
import (
"github.com/spf13/viper"
)
type SipOptions struct {
Id string
Realm string
Ip string
Port int
Password string
UserAgent string
CheckRealm int
}
var SipOp *SipOptions
func InitSipOptions() *SipOptions {
r := &SipOptions{}
err := viper.UnmarshalKey("sip", r)
if err != nil {
panic(err)
}
return r
}
+44
View File
@@ -0,0 +1,44 @@
package config
import (
"github.com/spf13/viper"
)
type ZlmOptions struct {
Id string
Ip string
HttpPort int
Secret string
RtspPort int
RtmpPort int
}
var ZlmOp *ZlmOptions
func InitZlmOptions() *ZlmOptions {
r := &ZlmOptions{}
err := viper.UnmarshalKey("media", r)
if err != nil {
panic(err)
}
return r
}
func (zlmOp *ZlmOptions) GetId() string {
return ZlmOp.Id
}
func (zlmOp *ZlmOptions) GetIp() string {
return ZlmOp.Ip
}
func (zlmOp *ZlmOptions) GetHttpPort() int {
return ZlmOp.HttpPort
}
func (zlmOp *ZlmOptions) GetSecret() string {
return ZlmOp.Secret
}
func (zlmOp *ZlmOptions) GetRtspPort() int {
return ZlmOp.RtspPort
}
func (zlmOp *ZlmOptions) GetRtmpPort() int {
return ZlmOp.RtmpPort
}
+78
View File
@@ -0,0 +1,78 @@
module gb28181Panda
go 1.20
require (
github.com/beevik/etree v1.1.0
github.com/ghettovoice/gosip v0.0.0-20230322091832-d77de1c97f89
github.com/gin-gonic/gin v1.9.0
github.com/go-redis/redis/v8 v8.11.5
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/panjjo/gosdp v0.0.0-20201029020038-56e3a0ec56ef
github.com/parnurzeal/gorequest v0.2.16
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cast v1.5.0
github.com/spf13/viper v1.15.0
github.com/x-cray/logrus-prefixed-formatter v0.5.2
go.uber.org/zap v1.24.0
golang.org/x/net v0.9.0
golang.org/x/text v0.9.0
gorm.io/driver/mysql v1.5.0
gorm.io/gorm v1.25.0
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0-rc.1 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/gomega v1.27.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/smartystreets/goconvey v1.8.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)
+646
View File
@@ -0,0 +1,646 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca h1:cTTdXpkQ1aVbOOmHwdwtYuwUZcQtcMrleD1UXLWhAq8=
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghettovoice/gosip v0.0.0-20230322091832-d77de1c97f89 h1:bUtgAwa7cfrp0bEnlfr+k6fgsfn7/OFQ0pvQvoujOAo=
github.com/ghettovoice/gosip v0.0.0-20230322091832-d77de1c97f89/go.mod h1:rlD1yLOErWYohWTryG/2bTTpmzB79p52ntLA/uIFXeI=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0-rc.1 h1:VK3aeRXMI8osaS6YCDKNZhU6RKtcP3B2wzqxOogNDz8=
github.com/gobwas/ws v1.1.0-rc.1/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/panjjo/gosdp v0.0.0-20201029020038-56e3a0ec56ef h1:RmpKwi6Ju0aSGZe9zdhX3dT0DoGZMTrRScUap6WVFSg=
github.com/panjjo/gosdp v0.0.0-20201029020038-56e3a0ec56ef/go.mod h1:VyTSJoai1m6iMalmg8kEMuaKk2amkc6JS0K7H0xfcmQ=
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.0 h1:6hSAT5QcyIaty0jfnff0z0CLDjyRgZ8mlMHLqSt7uXM=
gorm.io/driver/mysql v1.5.0/go.mod h1:FFla/fJuCvyTi7rJQd27qlNX2v3L6deTR1GgTjSOLPo=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

+158
View File
@@ -0,0 +1,158 @@
package log
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"path"
"time"
)
// Logger interface used as base logger throughout the library.
type Logger interface {
Print(args ...interface{})
Printf(format string, args ...interface{})
Trace(args ...interface{})
Tracef(format string, args ...interface{})
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Panic(args ...interface{})
Panicf(format string, args ...interface{})
WithPrefix(prefix string) Logger
Prefix() string
WithFields(fields Fields) Logger
Fields() Fields
SetLevel(level Level)
}
type Loggable interface {
Log() Logger
}
var (
l = initLog()
)
func initLog() *zap.SugaredLogger {
core := zapcore.NewCore(getJSONEncoder(), getLoggerWrite(), getLoggerLevel())
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
return logger.Sugar()
}
func getJSONEncoder() zapcore.Encoder {
// 自定义时间输出格式
customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
}
// 自定义日志级别显示
customLevelEncoder := func(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(level.CapitalString())
}
// 定义zap配置信息
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "line",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeTime: customTimeEncoder, // 自定义时间格式
EncodeLevel: customLevelEncoder, // 小写编码器
EncodeCaller: zapcore.ShortCallerEncoder, // 全路径编码器
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeName: zapcore.FullNameEncoder,
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
type Fields map[string]interface{}
func getLoggerWrite() zapcore.WriteSyncer {
// 定义日志切割配置
l := &lumberjack.Logger{
Filename: path.Join("./logFile", "gb28181Panda.log"), //Filename 是要写入日志的文件。
MaxSize: 1, //MaxSize 是日志文件在轮换之前的最大大小(以兆字节为单位)。它默认为 100 兆字节
MaxBackups: 30, //MaxBackups 是要保留的最大旧日志文件数。默认是保留所有旧的日志文件(尽管 MaxAge 可能仍会导致它们被删除。)
MaxAge: 30, //MaxAge 是根据文件名中编码的时间戳保留旧日志文件的最大天数。
Compress: true, //压缩
LocalTime: true, //LocalTime 确定用于格式化备份文件中的时间戳的时间是否是计算机的本地时间。默认是使用 UTC 时间。
}
// 控制台输出
return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(l))
}
func getLoggerLevel() zapcore.Level {
level, _ := zapcore.ParseLevel("debug")
return level
}
func Debug(args ...interface{}) {
l.Debug(args)
}
func Debugf(format string, args ...interface{}) {
l.Debugf(format, args)
}
func Info(args ...interface{}) {
l.Info(args)
}
func Infof(format string, args ...interface{}) {
l.Infof(format, args)
}
func Warn(args ...interface{}) {
l.Warn(args)
}
func Warnf(format string, args ...interface{}) {
l.Warnf(format, args)
}
func Error(args ...interface{}) {
l.Error(args)
}
func Errorf(format string, args ...interface{}) {
l.Errorf(format, args)
}
func Fatal(args ...interface{}) {
l.Fatal(args)
}
func Fatalf(format string, args ...interface{}) {
l.Fatalf(format, args)
}
func Panic(args ...interface{}) {
l.Panic(args)
}
func Panicf(format string, args ...interface{}) {
l.Panicf(format, args)
}
+160
View File
@@ -0,0 +1,160 @@
package log
import (
"github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
type LogrusLogger struct {
log logrus.Ext1FieldLogger
prefix string
fields Fields
}
// Level type
type Level uint32
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
// TraceLevel level. Designates finer-grained informational events than the Debug.
TraceLevel
)
func NewLogrusLogger(logrus logrus.Ext1FieldLogger, prefix string, fields Fields) *LogrusLogger {
return &LogrusLogger{
log: logrus,
prefix: prefix,
fields: fields,
}
}
func NewDefaultLogrusLogger() *LogrusLogger {
logger := logrus.New()
logger.Formatter = &prefixed.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05.000",
}
return NewLogrusLogger(logger, "main", nil)
}
func (fields Fields) WithFields(newFields Fields) Fields {
allFields := make(Fields)
for k, v := range fields {
allFields[k] = v
}
for k, v := range newFields {
allFields[k] = v
}
return allFields
}
func (l *LogrusLogger) Print(args ...interface{}) {
l.prepareEntry().Print(args...)
}
func (l *LogrusLogger) Printf(format string, args ...interface{}) {
l.prepareEntry().Printf(format, args...)
}
func (l *LogrusLogger) Trace(args ...interface{}) {
l.prepareEntry().Trace(args...)
}
func (l *LogrusLogger) Tracef(format string, args ...interface{}) {
l.prepareEntry().Tracef(format, args...)
}
func (l *LogrusLogger) Debug(args ...interface{}) {
l.prepareEntry().Debug(args...)
}
func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
l.prepareEntry().Debugf(format, args...)
}
func (l *LogrusLogger) Info(args ...interface{}) {
l.prepareEntry().Info(args...)
}
func (l *LogrusLogger) Infof(format string, args ...interface{}) {
l.prepareEntry().Infof(format, args...)
}
func (l *LogrusLogger) Warn(args ...interface{}) {
l.prepareEntry().Warn(args...)
}
func (l *LogrusLogger) Warnf(format string, args ...interface{}) {
l.prepareEntry().Warnf(format, args...)
}
func (l *LogrusLogger) Error(args ...interface{}) {
l.prepareEntry().Error(args...)
}
func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
l.prepareEntry().Errorf(format, args...)
}
func (l *LogrusLogger) Fatal(args ...interface{}) {
l.prepareEntry().Fatal(args...)
}
func (l *LogrusLogger) Fatalf(format string, args ...interface{}) {
l.prepareEntry().Fatalf(format, args...)
}
func (l *LogrusLogger) Panic(args ...interface{}) {
l.prepareEntry().Panic(args...)
}
func (l *LogrusLogger) Panicf(format string, args ...interface{}) {
l.prepareEntry().Panicf(format, args...)
}
func (l *LogrusLogger) WithPrefix(prefix string) Logger {
return NewLogrusLogger(l.log, prefix, l.Fields())
}
func (l *LogrusLogger) Prefix() string {
return l.prefix
}
func (l *LogrusLogger) WithFields(fields Fields) Logger {
return NewLogrusLogger(l.log, l.Prefix(), l.Fields().WithFields(fields))
}
func (l *LogrusLogger) Fields() Fields {
return l.fields
}
func (l *LogrusLogger) prepareEntry() *logrus.Entry {
return l.log.
WithFields(logrus.Fields(l.Fields())).
WithField("prefix", l.Prefix())
}
func (l *LogrusLogger) SetLevel(level Level) {
if ll, ok := l.log.(*logrus.Logger); ok {
ll.SetLevel(logrus.Level(level))
}
}
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"gb28181Panda/router"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func main() {
//初始化sip服务
router.InitSipServer()
//设置gin的运行环境
gin.SetMode(viper.GetString("runMode"))
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
_ = r.SetTrustedProxies([]string{viper.GetString("ip")})
router.InitGinRouter(r)
_ = r.Run(viper.GetString("addr")) // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
+9
View File
@@ -0,0 +1,9 @@
package model
type Cascade struct {
Id string
Ip string
Port int
Transport string
Password string
}
+77
View File
@@ -0,0 +1,77 @@
package model
import (
"fmt"
"gb28181Panda/storage"
)
type Channel struct {
Id int `json:"id"` //type:int comment:
DeviceId string `gorm:"column:device_id" json:"deviceId" xml:"DeviceID"` //type:string comment:通道Id号
Name string `gorm:"column:name" json:"name"` //type:string comment:通道名称
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"` //type:string comment:当为设备时,设备厂商
Model string `gorm:"column:model" json:"model"` //type:string comment:当为设备时,设备型号
Owner string `gorm:"column:owner" json:"owner"` //type:string comment:当为设备时,设备归属
CivilCode string `gorm:"column:civil_code" json:"civilCode"` //type:string comment:行政区域
Address string `gorm:"column:address" json:"address"` //type:string comment:当为设备时,安装地址
Parental string `gorm:"column:parental" json:"parental"` //type:string comment:当为设备时,是否有子设备,1有,0没有
ParentId string `gorm:"column:parent_id" json:"parentId" xml:"ParentID"` //type:string comment:父设备/区域/系统ID
SafetyWay string `gorm:"column:safety_way" json:"safetyWay"` //type:string comment:信令安全模式,0不采用、2 S/MIME签名方式、3 S/MIME加密他签名同时采用方式、4 数字摘要方式
RegisterWay string `gorm:"column:register_way" json:"registerWay"` //type:string comment:注册方式,1 标准认证注册模式 、2 基于口令的双向认证模式、3 基于数字证书的双向认证注册模式
Secrecy string `gorm:"column:secrecy" json:"secrecy"` //type:string comment:保密属性,0不涉密、1涉密
Status string `gorm:"column:status" json:"status"` //type:string comment:通道状态
TransportType string `gorm:"column:transport_type" json:"transportType"` //type:string comment:流媒体传输方式 udp、tcp被动 tcppassive、tcp主动 tcpactive
MediaStatus string `gorm:"column:media_status" json:"mediaStatus"` //type:string comment:流媒体当前直播状态 OPEN 有人观看 CLOSE 无人观看
CreatedAt string `gorm:"column:created_at" json:"createdAt"` //type:string comment:
UpdatedAt string `gorm:"column:updated_at" json:"updatedAt"` //type:string comment:
}
func (channel *Channel) ChannelList() ([]Channel, error) {
var list []Channel
if err := storage.MysqlDb.Table("t_channel").Where("parent_id = ?", channel.DeviceId).Order("id ASC").Find(&list).Error; err != nil {
return nil, err
} else {
return list, nil
}
}
func (channel *Channel) ChannelDetail() (Channel, error) {
var detail Channel
if err := storage.MysqlDb.Table("t_channel").Where("device_id = ?", channel.DeviceId).First(&detail).Error; err != nil {
return Channel{}, err
} else {
return detail, nil
}
}
func (channel *Channel) ChannelAdd() error {
if err := storage.MysqlDb.Debug().Table("t_channel").Create(&channel).Error; err != nil {
fmt.Println("err", err)
return err
} else {
return nil
}
}
func (channel *Channel) ChannelUpdate() error {
if err := storage.MysqlDb.Debug().Table("t_channel").Where("device_id = ?", channel.DeviceId).Updates(channel).Error; err != nil {
return err
} else {
return nil
}
}
func (channel *Channel) ChannelDelete() error {
if err := storage.MysqlDb.Table("t_channel").Where("device_id = ? AND parent_id = ?", channel.DeviceId, channel.ParentId).Delete(&channel).Error; err != nil {
return err
} else {
return nil
}
}
func (channel *Channel) ChannelDeleteWithParentId() error {
if err := storage.MysqlDb.Table("t_channel").Where("parent_id = ?", channel.ParentId).Delete(&channel).Error; err != nil {
return err
} else {
return nil
}
}
+104
View File
@@ -0,0 +1,104 @@
package model
import (
"fmt"
"gb28181Panda/storage"
)
type Device struct {
Id int `json:"id"` //type:int comment:id
DeviceId string `gorm:"column:device_id" json:"deviceId"` //type:string comment:设备deviceId号
Domain string `gorm:"column:domain" json:"domain"` //type:string comment:设备域
Name string `gorm:"column:name" json:"name"` //type:string comment:设备名称
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"` //type:string comment:设备厂家
Model string `gorm:"column:model" json:"model"` //type:string comment:设备型号
Firmware string `gorm:"column:firmware" json:"firmware"` //type:string comment:设备固件版本
Transport string `gorm:"column:transport" json:"transport"` //type:string comment:传输模式
Status string `gorm:"column:status" json:"status"` //type:string comment:上下线状态on 上线 off 下线
Ip string `gorm:"column:ip" json:"ip"` //type:string comment:设备ip地址
Port string `gorm:"column:port" json:"port"` //type:string comment:设备端口号
Expires string `gorm:"column:expires" json:"expires"` //type:string comment:设备有效时间
CreatedAt string `gorm:"column:created_at" json:"createdAt"` //type:string comment:创建时间
UpdatedAt string `gorm:"column:updated_at" json:"updatedAt"` //type:string comment:更新时间
RegisterAt string `gorm:"column:register_at" json:"registerAt"` //type:string comment:注册时间
KeepaliveAt string `gorm:"column:keepalive_at" json:"keepaliveAt"` //type:string comment:保持状态时间
}
// DeviceTree 设备通道树状数据
type DeviceTree struct {
Id uint `json:"id"`
DeviceId string `json:"deviceId" gorm:"primaryKey"`
Name string `json:"name"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
Status string `json:"status"`
KeepaliveAt string `json:"keepaliveAt"`
Channel []*DeviceTreeChannel `json:"channel" gorm:"foreignKey:ParentId"`
}
// DeviceTreeChannel 通道数据
type DeviceTreeChannel struct {
DeviceId string `json:"deviceId"`
Name string `json:"name"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
ParentId string `json:"parentId"`
}
func (deviceTree *DeviceTree) TableName() string {
return "t_device"
}
func (deviceTreeChannel *DeviceTreeChannel) TableName() string {
return "t_channel"
}
func (deviceTree *DeviceTree) DeviceTreeData() ([]DeviceTree, error) {
var dataTree []DeviceTree
if err := storage.MysqlDb.Preload("Channel").Find(&dataTree).Error; err != nil {
return nil, err
} else {
return dataTree, nil
}
}
func (device *Device) DeviceList() ([]Device, error) {
var list []Device
if err := storage.MysqlDb.Table("t_device").Order("id ASC").Find(&list).Error; err != nil {
return nil, err
} else {
return list, nil
}
}
func (device *Device) DeviceDetail() (Device, error) {
var detail Device
if err := storage.MysqlDb.Debug().Table("t_device").Where("device_id = ?", device.DeviceId).First(&detail).Error; err != nil {
return Device{}, err
} else {
return detail, nil
}
}
func (device *Device) DeviceAdd() error {
if err := storage.MysqlDb.Debug().Table("t_device").Create(&device).Error; err != nil {
fmt.Println("err", err)
return err
} else {
return nil
}
}
func (device *Device) DeviceUpdate() error {
if err := storage.MysqlDb.Debug().Table("t_device").Where("device_id = ?", device.DeviceId).Updates(device).Error; err != nil {
return err
} else {
return nil
}
}
func (device *Device) DeviceDelete() error {
if err := storage.MysqlDb.Table("t_device").Where("device_id = ?", device.DeviceId).Delete(&device).Error; err != nil {
return err
} else {
return nil
}
}
+67
View File
@@ -0,0 +1,67 @@
package model
import (
"fmt"
"gb28181Panda/config"
"strings"
)
// StreamInfo 流信息
type StreamInfo struct {
MediaServerId string `json:"mediaServerId"`
App string `json:"app"`
Ip string `json:"ip"`
DeviceID string `json:"deviceID"`
ChannelId string `json:"channelId"`
Stream string `json:"stream"`
Rtmp string `json:"rtmp"`
Rtsp string `json:"rtsp"`
Flv string `json:"flv"`
HttpsFlv string `json:"httpsFlv"`
WsFlv string `json:"wsFlv"`
//WSFly string `json:"WSFly"`
//WSSFly string `json:"WSSFly"`
Fmp4 string `json:"fmp4"`
//HttpsFmp4 string `json:"httpsFmp4"`
//WSFmpt4 string `json:"WSFmpt4"`
Hls string `json:"hls"`
//HttpsHls string `json:"httpsHls"`
//WsHls string `json:"wsHls"`
Ts string `json:"ts"`
//HttpsTs string `json:"httpsTs"`
//WebsocketTs string `json:"websocketTs"`
Ssrc string `json:"ssrc"`
}
const (
rtsp = "rtsp://%s:%d/rtp/%s"
rtmp = "rtmp://%s:%d/rtp/%s"
wsFlv = "ws://%s:%d/rtp/%s.live.flv"
http = "http://%s:%d/rtp/%s/hls.m3u8"
flv = "http://%s:%d/rtp/%s.live.flv"
fmp4 = "http://%s:%d/rtp/%s.llive.mp4"
ts = "http://%s:%d/rtp/%s.llive.ts"
)
func MustNewStreamInfo(ssrc string) StreamInfo {
index := strings.Index(ssrc, "_")
deviceId := ssrc[0:index]
channelID := ssrc[index+1:]
mediaIp := config.ZlmOp.Ip
httpPort := config.ZlmOp.HttpPort
rtmpPort := config.ZlmOp.RtmpPort
rtspPort := config.ZlmOp.RtspPort
return StreamInfo{
App: "rtp",
DeviceID: deviceId,
ChannelId: channelID,
Stream: ssrc,
Rtmp: fmt.Sprintf(rtmp, mediaIp, rtmpPort, ssrc),
Rtsp: fmt.Sprintf(rtsp, mediaIp, rtspPort, ssrc),
Hls: fmt.Sprintf(http, mediaIp, httpPort, ssrc),
Flv: fmt.Sprintf(flv, mediaIp, httpPort, ssrc),
Fmp4: fmt.Sprintf(fmp4, mediaIp, httpPort, ssrc),
Ts: fmt.Sprintf(ts, mediaIp, httpPort, ssrc),
WsFlv: fmt.Sprintf(wsFlv, mediaIp, httpPort, ssrc),
}
}
+294
View File
@@ -0,0 +1,294 @@
package router
import (
"bytes"
"encoding/json"
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/sipServer"
"gb28181Panda/util"
"github.com/gin-gonic/gin"
"io"
"net/http"
"time"
)
type WebrtcSdp struct {
Code int `json:"code"`
ID string `json:"id"`
Sdp string `json:"sdp"`
Type string `json:"type"`
}
func ChannelRouter(g *gin.RouterGroup) {
//设备channel列表
g.GET("/list/:deviceId", func(c *gin.Context) {
channel := model.Channel{DeviceId: c.Param("deviceId")}
channelList, err := channel.ChannelList()
if err != nil {
util.GinFRes(c, "查询通道列表失败", nil)
return
}
util.GinSRes(c, "查询通道列表成功", channelList)
})
//播放设备+通道的视频
g.GET("/play/:channelId", func(c *gin.Context) {
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
//channel.ParentId 为device的id
ssrc := channel.ParentId + "_" + c.Param("channelId")
var device model.Device
device.DeviceId = channel.ParentId
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
//先去zlm流媒体查询ssrc是否存在,存在就直接返回播放地址
mediaStatus, _ := util.GetMediaList(ssrc)
if mediaStatus {
streamInfo := model.MustNewStreamInfo(ssrc)
util.GinSRes(c, "成功", streamInfo)
//设置sql中当前通道是处于播放状态的
channel.MediaStatus = "OPEN"
_ = channel.ChannelUpdate()
return
}
rtpPort, err := util.ZlmOpenRtpServer(ssrc, channel)
if err != nil {
log.Info("初始化ZLM接受rtp信息失败")
util.GinFRes(c, "ZLM初始化失败", nil)
return
}
log.Info("初始化ZLM接受rtp信息成功", rtpPort, ssrc)
streamInfo, err := sipServer.Play(device, channel, ssrc, rtpPort, sipServer.PlayNowType, "0", "0")
if err != nil {
log.Errorf("%+v", err)
util.GinFRes(c, err.Error(), nil)
return
}
//设置sql中当前通道是处于播放状态的
channel.MediaStatus = "OPEN"
_ = channel.ChannelUpdate()
util.GinSRes(c, "成功", streamInfo)
})
//设置通道的流媒体传输方式
g.GET("/tcp-mode/:channelId", func(c *gin.Context) {
tcpMode := c.Query("tcpMode")
if tcpMode == "" {
util.GinFRes(c, "参数错误", nil)
return
}
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
//设置通道流媒体传输方式
channel.TransportType = tcpMode
_ = channel.ChannelUpdate()
util.GinSRes(c, "设置流媒体传输方式成功", nil)
})
//播放历史视频
g.GET("/playback/:channelId", func(c *gin.Context) {
startTime := c.Query("startTime")
endTime := c.Query("endTime")
userId := c.Query("userId")
if startTime == "" {
util.GinFRes(c, "开始时间错误", nil)
return
}
if endTime == "" {
util.GinFRes(c, "结束时间错误", nil)
return
}
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
//channel.ParentId 为device的id 区分不同的用户查看历史回放 加上userId,不然公用一个ssrc的话会导致不同用户查看相同通道历史回放时间一样
ssrc := channel.ParentId + "_" + c.Param("channelId") + "_playback_" + userId
var device model.Device
device.DeviceId = channel.ParentId
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
rtpPort, err := util.ZlmOpenRtpServer(ssrc, channel)
if err != nil {
log.Info("初始化ZLM接受rtp信息失败")
util.GinFRes(c, "ZLM初始化失败", nil)
return
}
log.Info("初始化ZLM接受rtp信息成功", rtpPort, ssrc)
startT, _ := time.ParseInLocation("2006-01-02 15:04:05", startTime, time.Local)
endT, _ := time.ParseInLocation("2006-01-02 15:04:05", endTime, time.Local)
//先停止历史视频播放
_ = sipServer.Stop(device, ssrc)
//强制I帧
//sipServer.IFameSip(device, c.Param("channelId"))
streamInfo, err := sipServer.Play(device, channel, ssrc, rtpPort, sipServer.PlaybackType, startT.Format("2006-01-02T15:04:05"), endT.Format("2006-01-02T15:04:05"))
if err != nil {
log.Info(err)
util.GinFRes(c, "SIP错误", nil)
return
}
util.GinSRes(c, "成功", streamInfo)
})
// 历史视频序列
g.GET("/record/:channelId", func(c *gin.Context) {
startTime := c.Query("startTime")
endTime := c.Query("endTime")
if startTime == "" {
util.GinFRes(c, "开始时间错误", nil)
return
}
if endTime == "" {
util.GinFRes(c, "结束时间错误", nil)
return
}
//TODO 比较开始时间和结束时间
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
var device model.Device
device.DeviceId = channel.ParentId
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
if device.Status == "off" {
util.GinFRes(c, "当前设备离线", nil)
return
}
startT, _ := time.ParseInLocation("2006-01-02 15:04:05", startTime, time.Local)
endT, _ := time.ParseInLocation("2006-01-02 15:04:05", endTime, time.Local)
res, err := sipServer.RecordInfoSip(device, c.Param("channelId"), startT.Format("2006-01-02T15:04:05"), endT.Format("2006-01-02T15:04:05"))
if err != nil {
util.GinFRes(c, "获取历史视频失败", nil)
return
}
util.GinSRes(c, "获取历史视频成功", res)
})
g.POST("/webrtc/:channelId", func(c *gin.Context) {
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
//channel.ParentId 为device的id
ssrc := channel.ParentId + "_" + c.Param("channelId")
var device model.Device
device.DeviceId = channel.ParentId
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
body := c.Request.Body
x, _ := io.ReadAll(body)
formBytesReader := bytes.NewReader(x)
client := &http.Client{}
url := fmt.Sprintf("http://%s:%d/index/api/webrtc?app=rtp&stream=%s&type=play&secret=%s", config.ZlmOp.Ip, config.ZlmOp.HttpPort, ssrc, config.ZlmOp.Secret)
req, err := http.NewRequest("POST", url, formBytesReader)
if err != nil {
log.Fatal("生成请求失败!", err)
util.GinFRes(c, "webrtc失败", nil)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := client.Do(req)
defer response.Body.Close()
webrtcSdp := WebrtcSdp{}
responseBody, readErr := io.ReadAll(response.Body)
if readErr != nil {
log.Fatal(readErr)
util.GinFRes(c, "webrtc失败", nil)
return
}
jsonErr := json.Unmarshal(responseBody, &webrtcSdp)
if jsonErr != nil {
log.Fatal(jsonErr)
util.GinFRes(c, "webrtc失败", nil)
return
}
if response.StatusCode == http.StatusOK {
if webrtcSdp.Code == 0 {
util.GinSRes(c, "成功", gin.H{
"sdp": webrtcSdp.Sdp,
"type": "answer",
})
} else {
util.GinFRes(c, "webrtc失败,清尝试重新打开视频后再播放webrtc", nil)
}
} else {
util.GinFRes(c, "webrtc失败", nil)
}
})
//停止视频
g.GET("/stop/:channelId", func(c *gin.Context) {
var channel model.Channel
channel.DeviceId = c.Param("channelId")
channel, err := channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
if err != nil {
util.GinFRes(c, "sql错误", nil)
return
}
var device model.Device
device.DeviceId = channel.ParentId
device, err = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
if err != nil {
util.GinFRes(c, "sql错误", nil)
return
}
//channel.ParentId 为device的id
ssrc := channel.ParentId + "_" + c.Param("channelId")
//更新sql中当前直播状态字段
channel.MediaStatus = "CLOSE"
_ = channel.ChannelUpdate()
err = sipServer.Stop(device, ssrc)
if err != nil {
util.GinFRes(c, "sip停止视频错误", nil)
return
}
util.ZlmCloseRtpServer(ssrc)
util.GinSRes(c, "关闭视频成功", nil)
})
}
+136
View File
@@ -0,0 +1,136 @@
package router
import (
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/sipServer"
"gb28181Panda/util"
"github.com/gin-gonic/gin"
"go/types"
"strconv"
"time"
)
func DeviceRouter(g *gin.RouterGroup) {
//设备device列表
g.GET("/list", func(c *gin.Context) {
var device model.Device
deviceList, err := device.DeviceList()
if err != nil {
util.GinFRes(c, "查询设备列表失败", nil)
return
}
for key, value := range deviceList {
formatTime, err := time.ParseInLocation(util.TIME_LAYOUT_With_SPACE, value.KeepaliveAt, time.Local)
if err != nil {
log.Error("Format Time Error")
return
}
if time.Now().Unix()-formatTime.Unix() > 60 && deviceList[key].Status == "ON" {
deviceList[key].Status = "OFF"
value.Status = "OFF"
_ = value.DeviceUpdate()
}
}
util.GinSRes(c, "查询设备列表成功", deviceList)
})
//设备device-channel树
g.GET("/tree", func(c *gin.Context) {
var deviceTree model.DeviceTree
deviceData, err := deviceTree.DeviceTreeData()
if err != nil {
util.GinFRes(c, "查询设备列表失败", nil)
return
}
for key, value := range deviceData {
formatTime, err := time.ParseInLocation(util.TIME_LAYOUT_With_SPACE, value.KeepaliveAt, time.Local)
if err != nil {
log.Error("格式化时间失败")
return
}
if time.Now().Unix()-formatTime.Unix() > 60 && deviceData[key].Status == "ON" {
deviceData[key].Status = "OFF"
}
}
util.GinSRes(c, "查询设备树成功", deviceData)
})
//更新通道信息
g.GET("/channel/sync/:deviceId", func(c *gin.Context) {
var device model.Device
device.DeviceId = c.Param("deviceId")
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
sipServer.QueryChannelSip(device)
util.GinSRes(c, "更新通道成功", nil)
})
// 订阅Alarm消息
g.GET("/subscribe/:deviceId", func(c *gin.Context) {
var device model.Device
device.DeviceId = c.Param("deviceId")
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
sipServer.SetGuardSip(device)
sipServer.SubscribeAlarmSip(device)
util.GinSRes(c, "预警订阅成功", types.Nil{})
})
//删除设备
g.DELETE("/delete/:deviceId", func(c *gin.Context) {
var device model.Device
device.DeviceId = c.Param("deviceId")
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
if device.Status == "ON" {
util.GinFRes(c, "当前设备在线,不可删除", nil)
return
}
//先删除device
_ = device.DeviceDelete()
var channel model.Channel
channel.ParentId = c.Param("deviceId")
//再删除channel
_ = channel.ChannelDeleteWithParentId()
util.GinSRes(c, "删除设备成功", nil)
})
/* 次处无设备可联调 个人理解 ptz控制的应该是deviceId而不是channelId
// 设备id
DeviceId string `json:"deviceId,omitempty"`
// 控制的命令,取值为:left、right、down、up、downright、downleft、upright、upleft、zoomin、zoomout
Command string `json:"command,omitempty"`
// 水平方向移动速度,取值:0-255
HorizonSpeed int `json:"horizonSpeed,omitempty"`
// 垂直方向移动速度,取值:0-255
VerticalSpeed int `json:"verticalSpeed,omitempty"`
// 变倍控制速度,取值:0-255
ZoomSpeed int `json:"zoomSpeed,omitempty"`
*/
g.GET("/ptz/:deviceId/:command/:horizonSpeed/:verticalSpeed/:zoomSpeed", func(c *gin.Context) {
var device model.Device
device.DeviceId = c.Param("deviceId")
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
horizonSpeed, _ := strconv.Atoi(c.Param("horizonSpeed"))
verticalSpeed, _ := strconv.Atoi(c.Param("verticalSpeed"))
zoomSpeed, _ := strconv.Atoi(c.Param("zoomSpeed"))
err := sipServer.ControlPTZ(device, c.Param("command"), horizonSpeed, verticalSpeed, zoomSpeed)
if err != nil {
util.GinFRes(c, "ptz-err", nil)
return
}
util.GinSRes(c, "ptz-ok", nil)
})
}
+15
View File
@@ -0,0 +1,15 @@
package router
import (
"github.com/gin-gonic/gin"
)
// InitGinRouter 初始化路由
func InitGinRouter(g *gin.Engine) {
//设备路由
DeviceRouter(g.Group("/api/device"))
//通道路由
ChannelRouter(g.Group("/api/channel"))
//ZLM的hook回调接口
HookRouter(g.Group("/index/hook"))
}
+201
View File
@@ -0,0 +1,201 @@
package router
import (
"fmt"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/sipServer"
"gb28181Panda/util"
"github.com/gin-gonic/gin"
"strings"
)
// 无人观看
type OnStreamNoneReader struct {
MediaServerId string `json:"mediaServerId,omitempty"`
App string `json:"app,omitempty"`
Schema string `json:"schema,omitempty"`
Stream string `json:"stream,omitempty"`
Vhost string `json:"vhost,omitempty"`
}
// HookReply hook事件默认回复
type HookReply struct {
// 0代表允许,其他均为不允许
Code int `json:"code,omitempty"`
// 当code不为0时,msg字段应给出相应提示
Msg string `json:"msg,omitempty"`
}
type OnStreamNoneReaderReply struct {
// 固定返回0
Code int `json:"code,omitempty"`
// 是否关闭该流,包括推流和拉流
Close bool `json:"close,omitempty"`
}
const (
ParseParamFail = iota + 1
)
const (
SuccessMsg = "success"
ParseParamFailMsg = "parser on_play_hook interface param fail, auth fail and not allow play"
)
// OnPlayHookParam 播放器鉴权hook事件
type OnPlayHookParam struct {
// 流应用名
App string `json:"app,omitempty" `
// TCP链接唯一ID
Id string `json:"id,omitempty"`
// 播放器ip
Ip string `json:"ip,omitempty"`
// 播放url参数
Params string `json:"params,omitempty"`
// 播放器端口号
Port int `json:"port,omitempty"`
// 播放的协议,可能是rtsp、rtmp、http
Schema string `json:"schema,omitempty"`
// 流ID
Stream string `json:"stream,omitempty"`
// 流虚拟主机
Vhost string `json:"vhost,omitempty"`
// 服务器id,通过配置文件设置
MediaServerId string `json:"mediaServerId,omitempty"`
}
type OnPublishHookReply struct {
HookReply
// 是否转换成hls协议
EnableHls bool `json:"enable_hls,omitempty"`
// 是否允许mp4录制
EnableMp4 bool `json:"enable_mp4,omitempty"`
// 是否转rtsp协议
EnableRtsp bool `json:"enable_rtsp,omitempty"`
// 是否转rtmp/flv协议
EnableRtmp bool `json:"enable_rtmp,omitempty"`
// 是否转http-ts/ws-ts协议
EnableTs bool `json:"enable_ts,omitempty"`
// 是否转http-fmp4/ws-fmp4协议
EnableFmp4 bool `json:"enable_fmp4,omitempty"`
// 转协议时是否开启音频
EnableAudio bool `json:"enable_audio,omitempty"`
// 转协议时,无音频是否添加静音aac音频
AddMuteAudio bool `json:"add_mute_audio,omitempty"`
// mp4录制文件保存根目录,置空使用默认
Mp4SavePath string `json:"mp4_save_path,omitempty"`
// mp4录制切片大小,单位秒
Mp4MaxSecond int `json:"mp4_max_second,omitempty"`
// hls文件保存保存根目录,置空使用默认
HlsSavePath string `json:"hls_save_path,omitempty"`
// 断连续推延时,单位毫秒,置空使用配置文件默认值
ContinuePushMs uint32 `json:"continue_push_ms,omitempty"`
// MP4录制是否当作观看者参与播放人数计数
Mp4AsPlayer bool `json:"mp4_as_player,omitempty"`
// 该流是否开启时间戳覆盖
ModifyStamp bool `json:"modify_stamp,omitempty"`
}
func NewOnPublishDefaultReply() OnPublishHookReply {
return OnPublishHookReply{
HookReply: HookReply{
Code: 0,
Msg: SuccessMsg,
},
AddMuteAudio: true,
ContinuePushMs: 10000,
EnableAudio: true,
EnableFmp4: true,
EnableHls: true,
EnableMp4: false,
EnableRtmp: true,
EnableRtsp: true,
EnableTs: true,
HlsSavePath: "/hls_save_path/",
ModifyStamp: false,
Mp4AsPlayer: false,
Mp4MaxSecond: 3600,
Mp4SavePath: "/mp4_save_path/",
}
}
func HookRouter(g *gin.RouterGroup) {
g.POST("on_stream_none_reader", func(c *gin.Context) {
hookParam := OnStreamNoneReader{}
if err := c.ShouldBindJSON(&hookParam); err != nil {
log.Error(err)
c.JSON(200, HookReply{
Code: ParseParamFail,
Msg: ParseParamFailMsg,
})
return
}
log.Info("收到流无人观看事件,stream_id:", hookParam.Stream, "media_server_id:", hookParam.MediaServerId)
deviceChannel := strings.Split(hookParam.Stream, "_")
//关闭监控的视频流推送
var device model.Device
device.DeviceId = deviceChannel[0]
device, _ = device.DeviceDetail()
if device.DeviceId == "" {
util.GinFRes(c, "当前设备不存在", nil)
return
}
var channel model.Channel
channel.DeviceId = deviceChannel[1]
channel, _ = channel.ChannelDetail()
if channel.DeviceId == "" {
util.GinFRes(c, "当前通道不存在", nil)
return
}
channel.MediaStatus = "CLOSE"
_ = channel.ChannelUpdate()
ssrc := hookParam.Stream
_ = sipServer.Stop(device, ssrc)
c.JSON(200, OnStreamNoneReaderReply{
Code: 0,
Close: true,
})
})
g.POST("on_stream_changed", func(c *gin.Context) {
log.Info("on_stream_changed")
c.JSON(200, HookReply{
Code: 0,
Msg: "success",
})
})
g.POST("on_server_keepalive", func(c *gin.Context) {
log.Info("on_server_keepalive")
c.JSON(200, HookReply{
Code: 0,
Msg: "success",
})
})
g.POST("on_play", func(c *gin.Context) {
log.Info("on_play")
hookParam := OnPlayHookParam{}
if err := c.ShouldBindJSON(&hookParam); err != nil {
log.Error(err)
c.JSON(200, HookReply{
Code: ParseParamFail,
Msg: ParseParamFailMsg,
})
return
}
fmt.Println("hookParam", hookParam)
c.JSON(200, HookReply{
Code: 0,
Msg: "success",
})
})
g.POST("on_publish", func(c *gin.Context) {
log.Info("on_publish")
c.JSON(200, NewOnPublishDefaultReply())
})
}
+10
View File
@@ -0,0 +1,10 @@
package router
import (
"gb28181Panda/sipServer"
)
// InitSipServer 初始化sip服务
func InitSipServer() {
sipServer.NewServer()
}
+15
View File
@@ -0,0 +1,15 @@
package sipServer
import (
"gb28181Panda/log"
"github.com/ghettovoice/gosip/sip"
"strings"
)
func Ack(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
transferFromLog(fromIp, fromPort)
log.Info("收到Ack数据", req)
}
+15
View File
@@ -0,0 +1,15 @@
package sipServer
import (
"gb28181Panda/log"
"github.com/ghettovoice/gosip/sip"
"strings"
)
func Bye(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
transferFromLog(fromIp, fromPort)
log.Info("收到Bye数据", req)
}
+15
View File
@@ -0,0 +1,15 @@
package sipServer
import (
"gb28181Panda/log"
"github.com/ghettovoice/gosip/sip"
"strings"
)
func Invite(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
transferFromLog(fromIp, fromPort)
log.Info("收到Invite数据", req)
}
+109
View File
@@ -0,0 +1,109 @@
package sipServer
import (
"encoding/xml"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"net/http"
"strings"
)
type MessageStruct struct {
XMLName xml.Name
CmdType string `xml:"CmdType"`
SN string `xml:"SN"`
DeviceId string `json:"DeviceId" xml:"DeviceID"`
DeviceType string `xml:"DeviceType"`
DeviceName string `xml:"DeviceName"`
Result string `xml:"Result"`
Manufacturer string `xml:"Manufacturer"`
Model string `xml:"Model"`
Channel string `xml:"Channel"`
Firmware string `xml:"Firmware"`
DeviceList []*model.Channel `xml:"DeviceList>Item"`
}
// Message Sip的Message
func Message(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
transferFromLog(fromIp, fromPort)
log.Info("MESSAGE-Request:\n", req)
if l, ok := req.ContentLength(); !ok || l.Equals(0) {
log.Info("该MESSAGE消息的消息体长度为0,返回OK")
_ = tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
}
body := req.Body()
//xml GB2312 解包有问题 换成UTF-8
body = strings.Replace(body, "GB2312", "UTF-8", 1)
msgData := &MessageStruct{}
if err := xml.Unmarshal([]byte(body), msgData); err != nil {
log.Error("解析deviceInfo响应包出错", err)
return
}
if msgData.XMLName.Local == "Response" {
switch msgData.CmdType {
case "Keepalive":
device := model.Device{
DeviceId: msgData.DeviceId,
KeepaliveAt: util.GetCurrenTimeNow(),
}
//更新device设备信息
_ = device.DeviceUpdate()
case "DeviceInfo":
device := model.Device{
Name: msgData.DeviceType,
Manufacturer: msgData.Manufacturer,
Model: msgData.Model,
Firmware: msgData.Firmware,
DeviceId: msgData.DeviceId,
}
//更新device设备信息
_ = device.DeviceUpdate()
case "Catalog":
for _, item := range msgData.DeviceList {
//先查询当前设备是否在数据库中
channel, _ := item.ChannelDetail()
if channel.DeviceId != "" { //更新
item.UpdatedAt = util.GetCurrenTimeNow()
item.ParentId = msgData.DeviceId
_ = item.ChannelUpdate()
} else { // 新增
item.CreatedAt = util.GetCurrenTimeNow()
item.UpdatedAt = util.GetCurrenTimeNow()
item.ParentId = msgData.DeviceId
item.TransportType = "UDP"
item.MediaStatus = "CLOSE"
_ = item.ChannelAdd()
}
}
case "RecordInfo":
_ = sipMessageRecordInfo(body)
default:
log.Infof("【cmdType】", msgData.CmdType)
}
_ = tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
} else if msgData.XMLName.Local == "Notify" {
//下级设备保持keepalive是走的Message的notify 区别于Notify本身
device := model.Device{
DeviceId: msgData.DeviceId,
KeepaliveAt: util.GetCurrenTimeNow(),
}
//更新device设备信息
_ = device.DeviceUpdate()
//需要回复信息 不然设备会发送注销再走注册流程
errBack := tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
transferToLog(fromIp, fromPort)
log.Info("回复设备保持在线成功")
if errBack != nil {
log.Info("回复设备keepalive出错了")
//只尝试一次再次回复
_ = tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
}
}
}
+58
View File
@@ -0,0 +1,58 @@
package sipServer
import (
"encoding/xml"
"gb28181Panda/log"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"net/http"
"strings"
)
// Notify 通知
func Notify(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
log.Info("Notify-Request:\n", req)
transferFromLog(fromIp, fromPort)
if l, ok := req.ContentLength(); !ok || l.Equals(0) {
log.Debug("该MESSAGE消息的消息体长度为0,返回OK")
_ = tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
}
body := req.Body()
body = strings.Replace(body, "GB2312", "UTF-8", 1)
msgData := &MessageStruct{}
if err := xml.Unmarshal([]byte(body), msgData); err != nil {
log.Error("解析deviceInfo响应包出错", err)
return
}
log.Info("Notify-Type:", msgData.CmdType)
if msgData.XMLName.Local == "Notify" {
switch msgData.CmdType {
case "Catalog":
log.Info("下级主动推送channel通道信息")
//下级主动推送通道信息
for _, item := range msgData.DeviceList {
//先查询当前设备是否在数据库中
channel, _ := item.ChannelDetail()
if channel.DeviceId != "" { //更新
item.UpdatedAt = util.GetCurrenTimeNow()
item.ParentId = msgData.DeviceId
_ = item.ChannelUpdate()
} else { // 新增
item.CreatedAt = util.GetCurrenTimeNow()
item.UpdatedAt = util.GetCurrenTimeNow()
item.ParentId = msgData.DeviceId
item.TransportType = "UDP"
item.MediaStatus = "CLOSE"
_ = item.ChannelAdd()
}
}
default:
log.Infof("【cmdType】", msgData.CmdType)
}
_ = tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, http.StatusText(http.StatusOK), ""))
}
}
+52
View File
@@ -0,0 +1,52 @@
package sipServer
import (
"gb28181Panda/log"
"gb28181Panda/model"
"github.com/ghettovoice/gosip/sip"
"github.com/pkg/errors"
"net/http"
)
// Play 播放实时视频或历史回放点播
func Play(device model.Device, channel model.Channel, ssrc string, rtpPort int, playType, startT, endT string) (model.StreamInfo, error) {
body := createVideoSdpInfo(channel, ssrc, rtpPort, playType, startT, endT)
request, _ := createVideoMessageRequest(device, contentTypeSDP, sip.INVITE, channel.DeviceId, ssrc, body)
transferToLog(device.Ip, device.Port)
log.Info("Play-Request:\n", request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送视频播放请求错误")
return model.StreamInfo{}, errors.New("发送视频播放请求错误")
}
resp := getResponse(tx)
//TODO 这块的错误处理后续优化
if resp.StatusCode() != sip.StatusCode(http.StatusOK) {
return model.StreamInfo{}, errors.New("当前时间错误")
}
log.Info("收到invite响应:\n", resp)
if resp == nil {
log.Error("获取响应超时")
return model.StreamInfo{}, errors.New("获取响应超时")
}
ackRequest := sip.NewAckRequest("", request, resp, "", nil)
ackRequest.SetRecipient(request.Recipient())
ackRequest.AppendHeader(&sip.ContactHeader{
Address: request.Recipient(),
Params: nil,
})
transferToLog(device.Ip, device.Port)
log.Info("发送ack确认:\n", ackRequest)
err = SipServer.Send(ackRequest)
if err != nil {
log.Errorf("发送ack失败", err)
return model.StreamInfo{}, errors.New("发送invite请求错误")
}
callId, fromTag, toTag, branch, err := getRequestTxField(request, resp)
saveStreamSession(device.DeviceId, channel.DeviceId, ssrc, callId, fromTag, toTag, branch)
if err != nil {
return model.StreamInfo{}, err
}
info := model.MustNewStreamInfo(ssrc)
return info, nil
}
+116
View File
@@ -0,0 +1,116 @@
package sipServer
import (
"fmt"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"github.com/pkg/errors"
"strings"
)
// ControlPTZ 设备ptz控制指令
func ControlPTZ(device model.Device, command string, horizonSpeed, verticalSpeed, zoomSpeed int) error {
cmdStr, err := createPTZCode(command, horizonSpeed, verticalSpeed, zoomSpeed)
if err != nil {
log.Error(err)
return err
}
xml, err := util.CreateControlXml(util.DeviceControl, device.DeviceId, util.WithPTZCmd(cmdStr))
if err != nil {
log.Error(err)
return err
}
request, _ := createMessageRequest(device, "Application/MANSCDP+xml", sip.MESSAGE, xml)
transferToLog(device.Ip, device.Port)
log.Info("Ptz-Request", request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("Ptz发送失败")
return errors.New("Ptz发送失败")
}
resp := getResponse(tx)
log.Info("收到invite响应:\n", resp)
if err != nil {
log.Error(err)
return err
}
return nil
}
// 创建PTZ指令
// 根据gb28181协议的标准,前端指令中一共包含4个字节
func createPTZCode(command string, horizonSpeed, verticalSpeed, zoomSpeed int) (string, error) {
var ptz strings.Builder
// gb28181协议中控制指令中的前三个字节
// 字节1是A5,字节2是组合码,高4位由版本信息组成,版本信息为0H;低四位是校验位,校验位=(字节1的高4位+字节1的低四位+字节2的高四位) % 16
// 所以校验码 = (0xa + 0x5 + 0) % 16 = (1010 + 0101 + 0) % 16 = 15 % 16 = 15;十进制数15转十六进制= F
// 所以字节2 = 0F
// 字节3是地址的低8位,这里直接设置为01
ptz.WriteString("A50F01")
var cmd int
// 指令码以一个字节来表示
// 0000 0000,高位的前两个bit不做表示
// 所以有作用的也就是后6个bit,从高到低,这些bit分别控制云台的镜头缩小、镜头放大、上、下、左、右
// 如果有做对应的操作,就将对应的bit位置1
switch command {
case "right":
// 0000 0001
cmd = 1
case "left":
// 0000 0010
cmd = 2
case "down":
// 0000 0100
cmd = 4
case "up":
// 0000 1000
cmd = 8
case "downright":
// 0000 0101
cmd = 5
case "downleft":
// 0000 0110
cmd = 6
case "upright":
// 0000 1001
cmd = 9
case "upleft":
// 0000 1010
cmd = 10
case "zoomin":
// 0001 0000
cmd = 16
case "zoomout":
// 0010 0000
cmd = 32
case "stop":
cmd = 0
default:
return "", errors.New("不合规的控制字符串")
}
// 根据gb标准,字节4用于表示云台的镜头缩小、镜头放大、上、下、左、右,写入指令码的16进制数
ptz.WriteString(fmt.Sprintf("%02X", cmd))
log.Debug("合并字节4之后:" + ptz.String())
// 根据gb标准,字节5用于表示水平控制速度,写入水平控制方向速度的十六进制数
ptz.WriteString(fmt.Sprintf("%02X", horizonSpeed))
// 根据gb标准,字节6用于表示垂直控制速度,写入垂直控制方向速度的十六进制数
ptz.WriteString(fmt.Sprintf("%02X", verticalSpeed))
// 最后字节7的高4位用于表示变倍控制速度,后4位不关注
// 所以这里直接与0xF0做与操作,保留前4位,后4为置0
c := zoomSpeed & 0xF0
ptz.WriteString(fmt.Sprintf("%02X", c))
// 字节8用于校验位,根据gb标准,校验位=(字节1+字节2+字节3+字节4+字节5+字节6+字节7) % 256
checkCode := (0xA5 + 0x0F + 0x01 + cmd + horizonSpeed + verticalSpeed + c) % 0x100
ptz.WriteString(fmt.Sprintf("%02X", checkCode))
log.Debug("最终生成的PTZCmd: " + ptz.String())
return ptz.String(), nil
}
+165
View File
@@ -0,0 +1,165 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"github.com/pkg/errors"
"time"
)
// QueryDeviceSip 查询设备信息SIP
func QueryDeviceSip(device model.Device) {
xml, err := util.CreateQueryXML(util.DeviceInfoCmdType, device.DeviceId)
if err != nil {
return
}
request, _ := createMessageRequest(device, contentTypeXML, sip.MESSAGE, xml)
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送查询通道请求错误")
return
}
resp := getResponse(tx)
log.Info("收到invite响应:\n", resp)
}
// QueryChannelSip 查询设备通道SIP
func QueryChannelSip(device model.Device) {
xml, err := util.CreateQueryXML(util.CatalogCmdType, device.DeviceId)
if err != nil {
return
}
request, _ := createMessageRequest(device, contentTypeXML, sip.MESSAGE, xml)
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送查询通道请求错误")
return
}
resp := getResponse(tx)
log.Info("收到invite响应:\n", resp)
}
// SetGuardSip 设防SIP
func SetGuardSip(device model.Device) {
xml, err := util.CreateGuardXML(util.DeviceControl, device.DeviceId)
if err != nil {
return
}
_, requestBuilder := createMessageRequest(device, contentTypeXML, sip.MESSAGE, xml)
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
request, _ := requestBuilder.Build()
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送预警订阅请求错误")
return
}
resp := getResponse(tx)
log.Info("收到预警订阅响应:\n", resp)
}
// SubscribeAlarmSip 订阅预警消息SIP
func SubscribeAlarmSip(device model.Device) {
xml, err := util.CreateSubscribeAlarmXML(util.AlarmCmdType, device.DeviceId)
if err != nil {
return
}
_, requestBuilder := createMessageRequest(device, contentTypeXML, sip.SUBSCRIBE, xml)
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
exe := sip.Expires(3600)
requestBuilder.SetExpires(&exe)
//测试添加头部Event内容
requestBuilder.AddHeader(&sip.GenericHeader{
HeaderName: "Event",
Contents: "Alarm",
})
request, _ := requestBuilder.Build()
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送预警订阅请求错误")
return
}
resp := getResponse(tx)
log.Info("收到预警订阅响应:\n", resp)
}
// 发送I帧 SIP
func IFameSip(device model.Device, channelId string) {
xml, err := util.CreateIFameXML(channelId)
if err != nil {
log.Info("发送IFame请求错误")
}
_, requestBuilder := createMessageRequest(device, contentTypeXML, sip.MESSAGE, xml)
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
exe := sip.Expires(3600)
requestBuilder.SetExpires(&exe)
request, _ := requestBuilder.Build()
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Info("发送IFame请求错误")
}
response := getResponse(tx)
log.Info("收到IFame结果:\n", response)
}
// RecordInfoSip 获取历史视频SIP
func RecordInfoSip(device model.Device, channelId string, startTime string, endTime string) (*[]RecordInfo, error) {
ch := make(chan int, 1)
defer close(ch)
xml, sn, err := util.CreateRecordInfoXml(util.RecordInfoCmdType, channelId, startTime, endTime)
if err != nil {
return nil, errors.New("获取数据超时")
}
_, requestBuilder := createMessageRequest(device, contentTypeXML, sip.MESSAGE, xml)
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
exe := sip.Expires(3600)
requestBuilder.SetExpires(&exe)
request, _ := requestBuilder.Build()
transferToLog(device.Ip, device.Port)
log.Info(request)
tx, err := transmitRequest(request)
if err != nil {
log.Error("发送请求历史视频请求错误")
return nil, errors.New("获取数据超时")
}
response := getResponse(tx)
log.Info("收到历史视频响应:\n", response)
recordKey := fmt.Sprintf("%s%s", channelId, sn)
//开始和结束时间
_recordList.Delete(recordKey)
_recordList.Store(recordKey, recordList{ch: ch, num: 0, data: []RecordInfo{}})
tick := time.NewTicker(30 * time.Second)
select {
case _, chStatus := <-ch:
if !chStatus {
return nil, errors.New("获取数据超时-chan通道已关闭")
}
if list, ok := _recordList.Load(recordKey); ok {
data := list.(recordList)
recordData := parseRecordData(data.data)
return &recordData, nil
}
return nil, errors.New("获取数据超时")
case <-tick.C:
// 30秒未完成返回当前获取到的数据
if list, ok := _recordList.Load(recordKey); ok {
data := list.(recordList)
recordData := parseRecordData(data.data)
return &recordData, nil
}
return nil, errors.New("获取数据超时")
}
}
+95
View File
@@ -0,0 +1,95 @@
package sipServer
import (
"fmt"
"gb28181Panda/log"
"gb28181Panda/util"
"github.com/pkg/errors"
"sync"
"time"
)
// MessageRecordInfoResponse 目录列表
type MessageRecordInfoResponse struct {
CmdType string `xml:"CmdType"`
SN int `xml:"SN"`
DeviceID string `xml:"DeviceID"`
SumNum int `xml:"SumNum"`
Item []RecordItem `xml:"RecordList>Item"`
}
// RecordItem 目录详情
type RecordItem struct {
// DeviceID 设备编号
DeviceID string `xml:"DeviceID" bson:"DeviceID" json:"DeviceID"`
// Name 设备名称
Name string `xml:"Name" bson:"Name" json:"Name"`
FilePath string `xml:"FilePath" bson:"FilePath" json:"FilePath"`
Address string `xml:"Address" bson:"Address" json:"Address"`
StartTime string `xml:"StartTime" bson:"StartTime" json:"StartTime"`
EndTime string `xml:"EndTime" bson:"EndTime" json:"EndTime"`
Secrecy int `xml:"Secrecy" bson:"Secrecy" json:"Secrecy"`
Type string `xml:"Type" bson:"Type" json:"Type"`
}
type recordList struct {
ch chan int
num int
data []RecordInfo
}
// RecordInfo 录像开始时间段
type RecordInfo struct {
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
}
// 当前获取目录文件设备集合
var _recordList *sync.Map
func sipMessageRecordInfo(body string) error {
message := &MessageRecordInfoResponse{}
if err := util.XMLDecode([]byte(body), message); err != nil {
log.Info("Message Unmarshal xml err:", err, "body:", string(body))
return err
}
recordKey := fmt.Sprintf("%s%d", message.DeviceID, message.SN)
if list, ok := _recordList.Load(recordKey); ok {
info := list.(recordList)
info.num += len(message.Item)
for _, item := range message.Item {
s, _ := time.ParseInLocation("2006-01-02T15:04:05", item.StartTime, time.Local)
e, _ := time.ParseInLocation("2006-01-02T15:04:05", item.EndTime, time.Local)
recordInfo := RecordInfo{
StartTime: s.Unix(),
EndTime: e.Unix(),
}
info.data = append(info.data, recordInfo)
}
if info.num == message.SumNum {
// 获取到完整数据
_recordList.Store(recordKey, info)
info.ch <- 1
}
_recordList.Store(recordKey, info)
return nil
}
return errors.New("未查询到历史视频")
}
func parseRecordData(data []RecordInfo) []RecordInfo {
var tempData []RecordInfo
for _, v := range data {
if len(tempData) > 0 {
recordL := len(tempData)
if tempData[recordL-1:][0].EndTime == v.StartTime {
tempData[recordL-1:][0].EndTime = v.EndTime
} else {
tempData = append(tempData, v)
}
} else {
tempData = append(tempData, v)
}
}
return tempData
}
+199
View File
@@ -0,0 +1,199 @@
package sipServer
import (
"crypto/md5"
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"net/http"
"strings"
)
const (
DefaultAlgorithm = "MD5"
WWWHeader = "WWW-Authenticate"
ExpiresHeader = "Expires"
)
type Authorization struct {
*sip.Authorization
}
func (a *Authorization) Verify(username, passwd string) bool {
//1、将 username,realm,password 依次组合获取 1 个字符串,并用算法加密的到密文 r1
//是否校验域值
var s1 string
if config.SipOp.CheckRealm == 1 {
s1 = fmt.Sprintf("%s:%s:%s", username, config.SipOp.Realm, passwd) //检验域值,从当前配置读取域值
} else {
s1 = fmt.Sprintf("%s:%s:%s", username, a.Realm(), passwd) //不检验域值,从设备取域值
}
r1 := a.getDigest(s1)
//2、将 method,即REGISTER ,uri 依次组合获取 1 个字符串,并对这个字符串使用算法 加密得到密文 r2
s2 := fmt.Sprintf("REGISTER:%s", a.Uri())
r2 := a.getDigest(s2)
if r1 == "" || r2 == "" {
log.Error("Authorization algorithm wrong")
return false
}
//3、将密文 1,nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3,即Response
s3 := fmt.Sprintf("%s:%s:%s", r1, a.Nonce(), r2)
r3 := a.getDigest(s3)
//4、计算服务端和客户端上报的是否相等
isGet := r3 == a.Response()
//log.Info("[Password-Verify-密码校验情况]", isGet)
return isGet
}
func (a *Authorization) getDigest(raw string) string {
switch a.Algorithm() {
case "MD5":
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
default: //如果没有算法,默认使用MD5
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
}
}
// Register express等于0 相当于注销后设备下线
func Register(req sip.Request, tx sip.ServerTransaction) {
idx := strings.Index(req.Source(), ":")
from, _ := req.From()
fromIp := req.Source()[:idx]
fromPort := req.Source()[idx+1:]
id := from.Address.User().String()
transferFromLog(fromIp, fromPort)
log.Info("Register-Request", req)
//注销的代码
h := req.GetHeaders(ExpiresHeader)
if len(h) != 1 {
log.Error("从头部获取expires失败", req)
return
} else {
expires := h[0].(*sip.Expires)
// 如果v=0,则代表该请求是注销请求
if expires.Equals(new(sip.Expires)) {
//expires值为0,该请求是注销请求
log.Info("Register-Logout")
fromRequest, ok := util.DeviceFromRequest(req)
if !ok {
return
}
device, _ := fromRequest.DeviceDetail()
if device.DeviceId != "" {
device = fromRequest
device.Status = "OFF"
log.Info("deviceInfo", device)
//插入数据库的
_ = device.DeviceUpdate()
}
} else {
if len(id) != 20 {
transferToLog(fromIp, fromPort)
log.Infof("错误的国标id: %s", id)
response := sip.NewResponseFromRequest("", req, http.StatusForbidden, "Forbidden", "")
_ = tx.Respond(response)
return
}
passAuth := false
// 不需要密码情况
if config.SipOp.Password == "" {
passAuth = true
} else {
// 需要密码情况 设备第一次上报,返回401和加密算法
if headers := req.GetHeaders("Authorization"); len(headers) > 0 {
log.Info("Register-With-Authorization")
authenticateHeader := headers[0].(*sip.GenericHeader)
auth := &Authorization{sip.AuthFromValue(authenticateHeader.Contents)}
// 有些摄像头没有配置用户名的地方,用户名就是摄像头自己的国标id
var username string
username = id
// 设备第二次上报,校验
if auth.Verify(username, config.SipOp.Password) {
log.Info("Check-Username-Password-Success")
passAuth = true
} else {
log.Info("Check-Username-Password-Error")
transferToLog(fromIp, fromPort)
response := sip.NewResponseFromRequest("", req, http.StatusForbidden, "Forbidden", "")
log.Info(response)
_ = tx.Respond(response)
return
}
}
}
if passAuth {
//认证账号和密码成功
log.Info("Auth-Success")
//从回复数据中解析device设备信息
fromRequest, ok := util.DeviceFromRequest(req)
if !ok {
log.Error("fromRequest-Error")
return
}
//先查询当前设备是否在数据库中
device, _ := fromRequest.DeviceDetail()
//不存在插入数据
if device.DeviceId == "" {
device = fromRequest
device.CreatedAt = util.GetCurrenTimeNow()
device.UpdatedAt = util.GetCurrenTimeNow()
device.RegisterAt = util.GetCurrenTimeNow()
device.KeepaliveAt = util.GetCurrenTimeNow()
device.Expires = expires.Value()
device.Status = "ON"
device.Ip = fromIp
device.Port = fromPort
log.Info("Insert Database Device", device)
//插入数据库的
_ = device.DeviceAdd()
} else {
var deviceSql model.Device
deviceSql.RegisterAt = util.GetCurrenTimeNow()
deviceSql.KeepaliveAt = util.GetCurrenTimeNow()
deviceSql.Status = "ON"
deviceSql.Ip = fromIp
deviceSql.Port = fromPort
deviceSql.DeviceId = device.DeviceId
// 存在,更新数据
log.Info("Update Database Device", device)
_ = deviceSql.DeviceUpdate()
}
resp := sip.NewResponseFromRequest("", req, http.StatusOK, "OK", "")
to, _ := resp.To()
resp.ReplaceHeaders("To", []sip.Header{&sip.ToHeader{Address: to.Address, Params: sip.NewParams().Add("tag", sip.String{Str: util.RandString(9)})}})
resp.RemoveHeader("Allow")
expires := sip.Expires(3600)
resp.AppendHeader(&expires)
resp.AppendHeader(&sip.GenericHeader{
HeaderName: "Date",
Contents: util.GetCurrenTimeNowFormat(),
})
transferToLog(fromIp, fromPort)
log.Info("Register-Success", resp)
_ = tx.Respond(resp)
//查询设备的信息和通道信息
QueryDeviceSip(device)
QueryChannelSip(device)
} else {
response := sip.NewResponseFromRequest("", req, http.StatusUnauthorized, "Unauthorized", "")
auth := fmt.Sprintf(
`Digest realm="%s",algorithm=%s,nonce="%s"`,
config.SipOp.Realm,
DefaultAlgorithm,
util.RandString(32),
)
response.AppendHeader(&sip.GenericHeader{
HeaderName: WWWHeader,
Contents: auth,
})
transferToLog(fromIp, fromPort)
log.Info("Register-Back-With-[WWW-Authenticate]-Header", response)
_ = tx.Respond(response)
}
}
}
}
+225
View File
@@ -0,0 +1,225 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/storage"
"gb28181Panda/util"
"github.com/ghettovoice/gosip/sip"
"github.com/pkg/errors"
"github.com/spf13/cast"
"net/http"
"strconv"
"time"
)
const (
contentTypeXML = "Application/MANSCDP+xml"
contentTypeSDP = "APPLICATION/SDP"
)
// 普通的消息组成
func createMessageRequest(device model.Device, contentType string, method sip.RequestMethod, body string) (sip.Request, *sip.RequestBuilder) {
requestBuilder := sip.NewRequestBuilder()
devicePort, _ := strconv.Atoi(device.Port)
requestBuilder.SetFrom(newFromAddress(newParams(map[string]string{"tag": util.RandString(32)})))
to := newToPort(device.DeviceId, device.Ip, devicePort)
requestBuilder.SetTo(to)
requestBuilder.SetRecipient(to.Uri)
requestBuilder.AddVia(newVia(device.Transport))
requestContentType := sip.ContentType(contentType)
requestBuilder.SetContentType(&requestContentType)
requestBuilder.SetMethod(method)
userAgent := sip.UserAgentHeader(fmt.Sprintf("%s", config.SipOp.UserAgent))
requestBuilder.SetUserAgent(&userAgent)
requestBuilder.SetBody(body)
ceq, _ := storage.GetCeq("ceq")
requestBuilder.SetSeqNo(cast.ToUint(ceq))
request, _ := requestBuilder.Build()
return request, requestBuilder
}
// 视频播放和点播
func createVideoMessageRequest(device model.Device, contentType string, method sip.RequestMethod, channelId string, ssrc string, body string) (sip.Request, *sip.RequestBuilder) {
requestBuilder := sip.NewRequestBuilder()
devicePort, _ := strconv.Atoi(device.Port)
to := newToPort(channelId, device.Ip, devicePort)
requestBuilder.SetMethod(method)
requestBuilder.SetFrom(newFromAddress(newParams(map[string]string{"tag": util.RandString(32)})))
requestBuilder.SetTo(to)
port := sip.Port(devicePort)
sipUri := &sip.SipUri{
FUser: sip.String{Str: channelId},
FHost: to.Uri.Host(),
FPort: &port,
}
requestBuilder.SetRecipient(sipUri)
requestBuilder.AddVia(newVia(device.Transport))
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
requestContentType := sip.ContentType(contentType)
requestBuilder.SetContentType(&requestContentType)
requestBuilder.SetBody(body)
userAgent := sip.UserAgentHeader(config.SipOp.UserAgent)
requestBuilder.SetUserAgent(&userAgent)
ceq, _ := storage.GetCeq("ceq")
requestBuilder.SetSeqNo(cast.ToUint(ceq))
callID := sip.CallID(fmt.Sprintf("%s", util.RandString(22)))
requestBuilder.SetCallID(&callID)
header := sip.GenericHeader{
HeaderName: "Subject",
Contents: fmt.Sprintf("%s:%s,%s:%d", channelId, ssrc, config.SipOp.Id, 0),
}
requestBuilder.AddHeader(&header)
request, _ := requestBuilder.Build()
return request, requestBuilder
}
// 向上级联注册
func createCascadeRequest(method sip.RequestMethod, cascade model.Cascade) sip.Request {
//创建请求
requestBuilder := sip.NewRequestBuilder()
to := newToPort(cascade.Id, cascade.Ip, cascade.Port)
requestBuilder.SetMethod(method)
requestBuilder.SetFrom(newFromAddress(newParams(map[string]string{"tag": util.RandString(32)})))
requestBuilder.SetTo(to)
port := sip.Port(cascade.Port)
sipUri := &sip.SipUri{
FUser: sip.String{Str: cascade.Id},
FHost: to.Uri.Host(),
FPort: &port,
}
requestBuilder.SetRecipient(sipUri)
requestBuilder.AddVia(newVia(cascade.Transport))
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
userAgent := sip.UserAgentHeader(config.SipOp.UserAgent)
requestBuilder.SetUserAgent(&userAgent)
ceq, _ := storage.GetCeq("ceq")
requestBuilder.SetSeqNo(cast.ToUint(ceq))
callID := sip.CallID(fmt.Sprintf("%s", util.RandString(22)))
requestBuilder.SetCallID(&callID)
exe := sip.Expires(3600)
requestBuilder.SetExpires(&exe)
request, err := requestBuilder.Build()
if err != nil {
log.Error("发生错误:", err)
}
return request
}
// get sip tx info by sip request and response
func getRequestTxField(request sip.Request, response sip.Response) (callId, fromTag, toTag, viaBranch string, err error) {
callID, ok := request.CallID()
if !ok {
return "", "", "", "", errors.New("获取CallId失败")
}
fromHeader, ok := request.From()
if !ok {
return "", "", "", "", errors.New("获取fromHeader失败")
}
ft, ok := fromHeader.Params.Get("tag")
if !ok {
return "", "", "", "", errors.New("获取fromTag失败")
}
toHeader, ok := response.To()
if !ok {
return "", "", "", "", errors.New("获取toHeader失败")
}
tg, okTag := toHeader.Params.Get("tag")
if !okTag {
return "", "", "", "", errors.New("获取toTag失败")
}
viaHop, ok := request.ViaHop()
if !ok {
return "", "", "", "", errors.New("获取viaHop失败")
}
branch, ok := viaHop.Params.Get("branch")
if !ok {
return "", "", "", "", errors.New("获取branch失败")
}
callId = callID.Value()
fromTag = ft.String()
if !okTag {
toTag = "unkonw to tag"
} else {
toTag = tg.String()
}
viaBranch = branch.String()
return
}
// transmitRequest 发送sip请求
func transmitRequest(req sip.Request) (sip.ClientTransaction, error) {
transaction, err := SipServer.Request(req)
return transaction, err
}
// getResponse 获取sip的响应结果
func getResponse(tx sip.ClientTransaction) sip.Response {
timer := time.NewTimer(5 * time.Second)
for {
select {
case resp := <-tx.Responses():
if resp.StatusCode() == sip.StatusCode(http.StatusContinue) ||
resp.StatusCode() == sip.StatusCode(http.StatusSwitchingProtocols) {
continue
}
return resp
case <-timer.C:
log.Error("获取响应超时")
return nil
}
}
}
func newFromAddress(params sip.Params) *sip.Address {
portFrom := sip.Port(config.SipOp.Port)
return &sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: config.SipOp.Id},
FHost: config.SipOp.Realm,
FPort: &portFrom,
},
Params: params,
}
}
func newParams(m map[string]string) sip.Params {
params := sip.NewParams()
for k, v := range m {
params.Add(k, sip.String{Str: v})
}
return params
}
func newVia(transport string) *sip.ViaHop {
p := sip.Port(config.SipOp.Port)
params := newParams(map[string]string{
"branch": fmt.Sprintf("%s%d", "z9hG4bK", time.Now().UnixMilli()),
})
return &sip.ViaHop{
ProtocolName: "SIP",
ProtocolVersion: "2.0",
Transport: transport,
Host: config.SipOp.Ip,
Port: &p,
Params: params,
}
}
func newToPort(channelId, host string, port int) *sip.Address {
toPort := sip.Port(port)
return &sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: channelId},
FHost: host,
FPort: &toPort,
},
}
}
+86
View File
@@ -0,0 +1,86 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/model"
sdp "github.com/panjjo/gosdp"
"net"
"time"
)
var (
PlayNowType = "Play"
PlaybackType = "Playback"
)
// createVideoSdpInfo 告知设备接受流媒体信息的相关信息 目前只有udp传输方式
func createVideoSdpInfo(channel model.Channel, ssrc string, rtpPort int, playType, startT, endT string) string {
mediaIp := fmt.Sprintf("%s", config.ZlmOp.Ip)
//对应的是sdp的[o]参数
origin := sdp.Origin{
Username: channel.DeviceId,
SessionID: 0,
SessionVersion: 0,
NetworkType: "IN",
AddressType: "IP4",
Address: mediaIp,
}
//UDP
protocol := "RTP/AVP"
//TCP被动或者TCP主动
if channel.TransportType != "UDP" {
protocol = "TCP/RTP/AVP"
}
video := sdp.Media{
Description: sdp.MediaDescription{
Type: "video",
Port: rtpPort,
Protocol: protocol,
Formats: []string{"96", "97", "98", "99"},
},
Connection: sdp.ConnectionData{
NetworkType: "IN",
AddressType: "IP4",
IP: net.ParseIP(mediaIp),
TTL: 0,
},
}
video.AddAttribute("recvonly")
video.AddAttribute("rtpmap", "96", "PS/90000")
video.AddAttribute("rtpmap", "98", "H264/90000")
video.AddAttribute("rtpmap", "97", "MPEG4/90000")
video.AddAttribute("rtpmap", "99", "H265/90000")
//TCP主动
if channel.TransportType != "TCPACTIVE" {
video.AddAttribute("setup", "active")
video.AddAttribute("connection", "new")
}
//TCP被动
if channel.TransportType != "TCPPASSIVE" {
video.AddAttribute("setup", "passive")
video.AddAttribute("connection", "new")
}
timeing := []sdp.Timing{
{
Start: time.Time{},
End: time.Time{},
},
}
if playType == "Playback" {
sT, _ := time.ParseInLocation("2006-01-02T15:04:05", startT, time.Local)
eT, _ := time.ParseInLocation("2006-01-02T15:04:05", endT, time.Local)
timeing = []sdp.Timing{sdp.Timing{Start: sT, End: eT}}
}
msg := sdp.Message{
Version: 0,
Origin: origin,
Name: playType,
Medias: sdp.Medias{video},
Timing: timeing,
SSRC: ssrc,
}
session := msg.Append(sdp.Session{})
bytes := session.AppendTo([]byte{})
return string(bytes)
}
+57
View File
@@ -0,0 +1,57 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"github.com/ghettovoice/gosip"
l "github.com/ghettovoice/gosip/log"
"github.com/ghettovoice/gosip/sip"
"sync"
)
var (
SipServer gosip.Server
serverOnce sync.Once
)
func NewServer() {
serverOnce.Do(func() {
SipServer = gosip.NewServer(
gosip.ServerConfig{
UserAgent: config.SipOp.UserAgent,
},
nil,
nil,
l.NewDefaultLogrusLogger(),
)
})
if err := ListenTCP(); err != nil {
log.Error("初始化SIP-TCP服务错误", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port))
return
}
log.Info("初始化SIP-TCP服务成功", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port))
if err := ListenUDP(); err != nil {
log.Error("初始化SIP-UDP服务错误", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port))
return
}
log.Info("初始化SIP-UDP服务成功", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port))
registerHandler()
_recordList = &sync.Map{}
}
func ListenTCP() error {
return SipServer.Listen("tcp", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port), nil)
}
func ListenUDP() error {
return SipServer.Listen("udp", fmt.Sprintf("%s:%d", config.SipOp.Ip, config.SipOp.Port), nil)
}
func registerHandler() {
_ = SipServer.OnRequest(sip.REGISTER, Register)
_ = SipServer.OnRequest(sip.MESSAGE, Message)
_ = SipServer.OnRequest(sip.NOTIFY, Notify)
_ = SipServer.OnRequest(sip.INVITE, Invite)
_ = SipServer.OnRequest(sip.ACK, Ack)
_ = SipServer.OnRequest(sip.BYE, Bye)
}
+54
View File
@@ -0,0 +1,54 @@
package sipServer
import (
"encoding/json"
"fmt"
"gb28181Panda/log"
"gb28181Panda/storage"
"github.com/pkg/errors"
)
// SipTX sip会话事务
type SipTX struct {
DeviceId string `json:"deviceId,omitempty"`
ChannelId string `json:"channelId,omitempty"`
SSRC string `json:"SSRC,omitempty"`
CallId string `json:"callId,omitempty"`
FromTag string `json:"fromTag,omitempty"`
ToTag string `json:"toTag,omitempty"`
ViaBranch string `json:"viaBranch,omitempty"`
}
// 保存sip事务信息
func saveStreamSession(deviceId string, channelId string, ssrc string, callId string, fromTag string, toTag string, viaBranch string) {
tx := SipTX{
DeviceId: deviceId,
ChannelId: channelId,
SSRC: ssrc,
CallId: callId,
FromTag: fromTag,
ToTag: toTag,
ViaBranch: viaBranch,
}
key := fmt.Sprintf("%s:%s", "Media:Stream:Transaction", ssrc)
b, _ := json.MarshalIndent(tx, "", " ")
storage.Set(key, b)
}
func GetTx(ssrc string) (SipTX, error) {
key := fmt.Sprintf("%s:%s", "Media:Stream:Transaction", ssrc)
j, err := storage.Get(key)
if err != nil {
log.Error(err)
return SipTX{}, errors.WithMessage(err, "gei sip session tx fail")
}
var tx SipTX
err = json.Unmarshal([]byte(j), &tx)
if err != nil {
return SipTX{}, errors.WithMessage(err, "unmarshal json data to struct fail")
}
return tx, nil
}
+58
View File
@@ -0,0 +1,58 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"gb28181Panda/storage"
"github.com/ghettovoice/gosip/sip"
"github.com/pkg/errors"
"github.com/spf13/cast"
"strconv"
)
// Stop 停止播放视频
func Stop(device model.Device, ssrc string) error {
// 从Reds读取sip相关session用户关系监控推送视频流
txInfo, err := GetTx(ssrc)
if err != nil {
return errors.New("Redis中无视频流信息")
}
key := fmt.Sprintf("%s:%s", "Media:Stream:Transaction", ssrc)
_ = storage.Del(key)
requestBuilder := sip.NewRequestBuilder()
devicePort, _ := strconv.Atoi(device.Port)
//to := newToPort(d.DeviceId, d.Ip, devicePort)
to := newToPort(txInfo.ChannelId, device.Ip, devicePort)
to.Params = newParams(map[string]string{"tag": txInfo.ToTag})
requestBuilder.SetFrom(newFromAddress(newParams(map[string]string{"tag": txInfo.FromTag})))
requestBuilder.SetMethod(sip.BYE)
requestBuilder.SetTo(to)
requestBuilder.SetRecipient(to.Uri)
via := newVia(device.Transport)
requestBuilder.AddVia(via)
requestBuilder.SetContact(newToPort(config.SipOp.Id, config.SipOp.Ip, config.SipOp.Port))
callId := sip.CallID(txInfo.CallId)
requestBuilder.SetCallID(&callId)
userAgent := sip.UserAgentHeader(config.SipOp.UserAgent)
requestBuilder.SetUserAgent(&userAgent)
ceq, _ := storage.GetCeq("ceq")
requestBuilder.SetSeqNo(cast.ToUint(ceq))
req, _ := requestBuilder.Build()
transferToLog(device.Ip, device.Port)
log.Info("发送停止播放视频请求:\n", req)
tx, err := transmitRequest(req)
if err != nil {
log.Error("发送停止视频播放请求错误")
return errors.New("发送停止视频播放请求错误")
}
resp := getResponse(tx)
transferFromLog(device.Ip, device.Port)
log.Info("收到停止播放视频响应:\n", resp)
if resp == nil {
log.Error("获取响应超时")
return errors.New("获取响应超时")
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
package sipServer
import (
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
)
func transferFromLog(fromIp, fromPort string) {
log.Info(fmt.Sprintf("[%s:%d]<<<<<<[%s:%s]", config.SipOp.Ip, config.SipOp.Port, fromIp, fromPort))
}
func transferToLog(toIp, toPort string) {
log.Info(fmt.Sprintf("[%s:%d]>>>>[%s:%s]", config.SipOp.Ip, config.SipOp.Port, toIp, toPort))
}
+33
View File
@@ -0,0 +1,33 @@
SET NAMES utf8mb4;
SET
FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_channel
-- ----------------------------
DROP TABLE IF EXISTS `t_channel`;
CREATE TABLE `t_channel`
(
`id` int NOT NULL AUTO_INCREMENT,
`device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '通道Id号',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '通道名称',
`manufacturer` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '当为设备时,设备厂商',
`model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '当为设备时,设备型号',
`owner` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '当为设备时,设备归属',
`civil_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '行政区域',
`address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '当为设备时,安装地址',
`parental` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '当为设备时,是否有子设备,1有,0没有',
`parent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '父设备/区域/系统ID',
`safety_way` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '信令安全模式,0不采用、2 S/MIME签名方式、3 S/MIME加密他签名同时采用方式、4 数字摘要方式',
`register_way` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '注册方式,1 标准认证注册模式 、2 基于口令的双向认证模式、3 基于数字证书的双向认证注册模式',
`secrecy` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '保密属性,0不涉密、1涉密',
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'ON' COMMENT '通道状态',
`transport_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'UDP' COMMENT '流媒体传输方式 udp、tcp被动 tcppassive、tcp主动 tcpactive',
`media_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'CLOSE' COMMENT '流媒体当前观看状态 CLOSE 无人 OPEN 有人',
`created_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`updated_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET
FOREIGN_KEY_CHECKS = 1;
+31
View File
@@ -0,0 +1,31 @@
SET NAMES utf8mb4;
SET
FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_device
-- ----------------------------
DROP TABLE IF EXISTS `t_device`;
CREATE TABLE `t_device`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备deviceId号',
`domain` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备域',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备名称',
`manufacturer` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备厂家',
`model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备型号',
`firmware` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备固件版本',
`transport` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '传输模式',
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'ON' COMMENT '上下线状态ON 上线 OFF 下线',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备ip地址',
`port` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备端口号',
`expires` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备有效时间',
`created_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建时间',
`updated_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新时间',
`register_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '注册时间',
`keepalive_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '保持状态时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 152 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET
FOREIGN_KEY_CHECKS = 1;
+24
View File
@@ -0,0 +1,24 @@
package storage
type Cache interface {
Get(key string) (string, error)
Set(key string, value interface{})
Del(key string) error
GetCeq(key string) (int64, error)
}
var cache Cache = newRedis()
func Get(key string) (string, error) {
return cache.Get(key)
}
func Set(key string, val interface{}) {
cache.Set(key, val)
}
func Del(key string) error {
return cache.Del(key)
}
func GetCeq(key string) (int64, error) {
return cache.GetCeq(key)
}
+44
View File
@@ -0,0 +1,44 @@
package storage
import (
"fmt"
"gb28181Panda/log"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var MysqlDb *gorm.DB
func init() {
//创建一个数据库的连接
var err error
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
viper.GetString("mysql.username"),
viper.GetString("mysql.password"),
viper.GetString("mysql.host"),
viper.GetString("mysql.port"),
viper.GetString("mysql.name"),
)
//newLogger := logger.New(
// log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
// logger.Config{
// SlowThreshold: time.Second, // Slow SQL threshold
// LogLevel: logger.Info, // Log level
// IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
// ParameterizedQueries: true, // Don't include params in the SQL log
// Colorful: false, // Disable color
// },
//)
MysqlDb, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
//Logger: newLogger,
})
//显示sql语句
MysqlDb.Debug()
log.Info("连接Mysql成功", fmt.Sprintf("%s:%s", viper.GetString("mysql.host"), viper.GetString("mysql.port")))
if err != nil {
panic("连接数据库失败")
}
}
+60
View File
@@ -0,0 +1,60 @@
package storage
import (
"context"
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"github.com/go-redis/redis/v8"
"github.com/pkg/errors"
)
type redisClient struct {
rdb *redis.Client
}
func newRedis() *redisClient {
redisConfig := config.NewRedisOptions()
rdb := redis.NewClient(&redis.Options{
Addr: redisConfig.Addr,
Password: redisConfig.Password,
DB: redisConfig.DB,
})
if err := rdb.Ping(context.Background()).Err(); err != nil {
panic(fmt.Errorf("connection to redis fail,addr: %s, err: %w",
redisConfig.Addr,
err,
))
}
rdb.IncrBy(context.Background(), "ceq", 1)
log.Info("连接Redis成功", redisConfig.Addr)
return &redisClient{
rdb: rdb,
}
}
func (r *redisClient) Get(key string) (string, error) {
result, err := r.rdb.Get(context.Background(), key).Result()
if err != nil {
log.Error(err)
}
return result, err
}
func (r *redisClient) Set(key string, val interface{}) {
if err := r.rdb.Set(context.Background(), key, val, redis.KeepTTL).Err(); err != nil {
log.Error(err)
}
}
func (r *redisClient) Del(key string) error {
_, err := r.rdb.Del(context.Background(), key).Result()
if err != nil {
log.Error(err)
return errors.New(err.Error())
}
return err
}
func (r *redisClient) GetCeq(key string) (int64, error) {
return r.rdb.Incr(context.Background(), key).Result()
}
+30
View File
@@ -0,0 +1,30 @@
package util
import (
"math/rand"
"time"
)
const (
letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
func RandString(n int) string {
rand.Seed(time.Now().UnixNano())
output := make([]byte, n)
randomness := make([]byte, n)
_, err := rand.Read(randomness)
if err != nil {
panic(err)
}
l := len(letterBytes)
for pos := range output {
random := randomness[pos]
randomPos := random % uint8(l)
output[pos] = letterBytes[randomPos]
}
return string(output)
}
+21
View File
@@ -0,0 +1,21 @@
package util
import (
"github.com/gin-gonic/gin"
"net/http"
)
func GinSRes(c *gin.Context, msg string, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": msg,
"data": data,
})
}
func GinFRes(c *gin.Context, msg string, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": msg,
"data": data,
})
}
+24
View File
@@ -0,0 +1,24 @@
package util
import (
"gb28181Panda/log"
"github.com/parnurzeal/gorequest"
"net/http"
"time"
)
var client = gorequest.New()
func SendPost(url string, params map[string]interface{}) (b string, err error) {
client.Post(url).SendMap(params).
Timeout(3 * time.Second).
End(func(response gorequest.Response, body string, errs []error) {
if response.StatusCode != http.StatusOK || errs != nil {
log.Error(errs)
err = errs[0]
return
}
b = body
})
return
}
+164
View File
@@ -0,0 +1,164 @@
package util
import (
"encoding/json"
"fmt"
"gb28181Panda/config"
"gb28181Panda/log"
"gb28181Panda/model"
"github.com/pkg/errors"
)
type C struct {
Code int `json:"code"`
}
type Message struct {
Msg string `json:"msg"`
}
type CodeMessage struct {
C
Message
}
type GetRtpInfoResp struct {
C
Message
Exist bool `json:"exist,omitempty"`
PeerIp string `json:"peer_ip,omitempty"`
PeerPort int `json:"peer_port,omitempty"`
LocalIp string `json:"local_ip,omitempty"`
LocalPort int `json:"local_port,omitempty"`
}
type CreateRtpServerResp struct {
C
Message
Port int `json:"port"`
}
type MediaInfoResp struct {
Code int `json:"code"`
Data []struct {
AliveSecond int `json:"aliveSecond"`
App string `json:"app"`
BytesSpeed int `json:"bytesSpeed"`
CreateStamp int `json:"createStamp"`
IsRecordingHLS bool `json:"isRecordingHLS"`
IsRecordingMP4 bool `json:"isRecordingMP4"`
OriginSock struct {
Identifier string `json:"identifier"`
LocalIP string `json:"local_ip"`
LocalPort int `json:"local_port"`
PeerIP string `json:"peer_ip"`
PeerPort int `json:"peer_port"`
} `json:"originSock"`
OriginType int `json:"originType"`
OriginTypeStr string `json:"originTypeStr"`
OriginURL string `json:"originUrl"`
ReaderCount int `json:"readerCount"`
Schema string `json:"schema"`
Stream string `json:"stream"`
TotalReaderCount int `json:"totalReaderCount"`
Tracks []struct {
Channels int `json:"channels,omitempty"`
CodecID int `json:"codec_id"`
CodecIDName string `json:"codec_id_name"`
CodecType int `json:"codec_type"`
Frames int `json:"frames"`
Ready bool `json:"ready"`
SampleBit int `json:"sample_bit,omitempty"`
SampleRate int `json:"sample_rate,omitempty"`
Fps float64 `json:"fps,omitempty"`
GopIntervalMs int `json:"gop_interval_ms,omitempty"`
GopSize int `json:"gop_size,omitempty"`
Height int `json:"height,omitempty"`
KeyFrames int `json:"key_frames,omitempty"`
Width int `json:"width,omitempty"`
} `json:"tracks"`
Vhost string `json:"vhost"`
} `json:"data"`
}
// 参考https://github.com/ZLMediaKit/ZLMediaKit/wiki/MediaServer%E6%94%AF%E6%8C%81%E7%9A%84HTTP-API#24indexapiopenrtpserver
func ZlmOpenRtpServer(stream string, channel model.Channel) (rtpPort int, err error) {
tcpMode := 0
//0 udp 模式,1 tcp 被动模式, 2 tcp 主动模式。
if channel.TransportType == "TCPACTIVE" {
tcpMode = 2
} else if channel.TransportType == "TCPPASSIVE" {
tcpMode = 1
}
url := fmt.Sprintf("http://%s:%d/index/api/openRtpServer?port=0&tcp_mode=%d&stream_id=%s&secret=%s", config.ZlmOp.Ip, config.ZlmOp.HttpPort, tcpMode, stream, config.ZlmOp.Secret)
params := map[string]interface{}{}
body, err := SendPost(url, params)
if err != nil {
return 0, errors.WithMessage(err, "create rtp server fail")
}
resp := CreateRtpServerResp{}
err = json.Unmarshal([]byte(body), &resp)
if resp.Code == -300 {
ZlmCloseRtpServer(stream)
return ZlmOpenRtpServer(stream, channel)
}
if err != nil {
return 0, errors.WithMessage(err, "unmarshal data to struct fail")
}
if resp.Code != 0 {
return 0, errors.New(resp.Msg)
}
rtpPort = resp.Port
return
}
// ZlmCloseRtpServer 关闭zlm中的stream数据
func ZlmCloseRtpServer(stream string) {
url := fmt.Sprintf("http://%s:%d/index/api/closeRtpServer?stream_id=%s&secret=%s", config.ZlmOp.Ip, config.ZlmOp.HttpPort, stream, config.ZlmOp.Secret)
params := map[string]interface{}{}
body, err := SendPost(url, params)
if err != nil {
log.Error(err, "close rtp server fail")
}
resp := CreateRtpServerResp{}
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
log.Error(err, "unmarshal data to struct fail")
}
if resp.Code != 0 {
log.Error("ZLM内部错误")
}
}
func GetMediaList(stream string) (status bool, err error) {
url := fmt.Sprintf("http://%s:%d/index/api/getMediaList?stream=%s&secret=%s&app=rtp", config.ZlmOp.Ip, config.ZlmOp.HttpPort, stream, config.ZlmOp.Secret)
params := map[string]interface{}{}
body, err := SendPost(url, params)
if err != nil {
return false, errors.WithMessage(err, "getMediaList fail")
}
resp := MediaInfoResp{}
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
log.Info("ZLM接口出错了")
return false, errors.WithMessage(err, "unmarshal data to struct fail")
}
if resp.Code != 0 {
log.Info("ZLM接口出错了")
return false, errors.New("ZLM接口出错了")
}
if len(resp.Data) == 0 {
log.Info("不存在")
return false, errors.New("不存在")
}
return true, nil
}
+36
View File
@@ -0,0 +1,36 @@
package util
import (
"gb28181Panda/model"
"github.com/ghettovoice/gosip/sip"
"github.com/sirupsen/logrus"
)
func DeviceFromRequest(req sip.Request) (model.Device, bool) {
d := model.Device{}
from, ok := req.From()
if !ok {
logrus.Debugln("从请求中无法解析from头部信息", req.String())
return d, false
}
if from.Address == nil {
logrus.Debugln("从请求中无法解析from头address部分信息", req.String())
return d, false
}
if from.Address.User() == nil {
logrus.Debugln("从请求中无法解析from头user部分信息", req.String())
return d, false
}
d.DeviceId = from.Address.User().String()
d.Domain = from.Address.Host()
via, ok := req.ViaHop()
if !ok {
logrus.Debugln("从请求中无法解析出via头部信息", via.String())
return d, false
}
d.Transport = via.Transport
logrus.Debugf("从请求中解析出的设备信息: %v\n", d)
return d, true
}
+30
View File
@@ -0,0 +1,30 @@
package util
import (
"bytes"
"encoding/xml"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io/ioutil"
)
func GbkToUtf8(s []byte) ([]byte, error) {
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
d, e := ioutil.ReadAll(reader)
if e != nil {
return s, e
}
return d, nil
}
func DecodeGbk(v interface{}, body []byte) error {
bodyBytes, err := GbkToUtf8(body)
if err != nil {
return err
}
decoder := xml.NewDecoder(bytes.NewReader(bodyBytes))
decoder.CharsetReader = charset.NewReaderLabel
err = decoder.Decode(v)
return err
}
+13
View File
@@ -0,0 +1,13 @@
package util
import "time"
const TIME_LAYOUT = "2006-01-02T15:04:05"
const TIME_LAYOUT_With_SPACE = "2006-01-02 15:04:05"
func GetCurrenTimeNowFormat() string {
return time.Now().Format(TIME_LAYOUT)
}
func GetCurrenTimeNow() string {
return time.Now().Format(TIME_LAYOUT_With_SPACE)
}
+119
View File
@@ -0,0 +1,119 @@
package util
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/net/html/charset"
)
// Error Error
type Error struct {
err error
params []interface{}
}
func (err *Error) Error() string {
if err == nil {
return "<nil>"
}
str := fmt.Sprint(err.params...)
if err.err != nil {
str += fmt.Sprintf(" err:%s", err.err.Error())
}
return str
}
// NewError NewError
func NewError(err error, params ...interface{}) error {
return &Error{err, params}
}
// JSONEncode JSONEncode
func JSONEncode(data interface{}) []byte {
d, err := json.Marshal(data)
if err != nil {
logrus.Errorln("JSONEncode error:", err)
}
return d
}
func timeoutClient() *http.Client {
connectTimeout := time.Duration(20 * time.Second)
readWriteTimeout := time.Duration(30 * time.Second)
return &http.Client{
Transport: &http.Transport{
DialContext: timeoutDialer(connectTimeout, readWriteTimeout),
MaxIdleConnsPerHost: 200,
DisableKeepAlives: true,
},
}
}
func timeoutDialer(cTimeout time.Duration,
rwTimeout time.Duration) func(ctx context.Context, net, addr string) (c net.Conn, err error) {
return func(ctx context.Context, netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, cTimeout)
if err != nil {
return nil, err
}
conn.SetDeadline(time.Now().Add(rwTimeout))
return conn, nil
}
}
// PostRequest PostRequest
func PostRequest(url string, bodyType string, body io.Reader) ([]byte, error) {
client := timeoutClient()
resp, err := client.Post(url, bodyType, body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respbody, nil
}
// PostJSONRequest PostJSONRequest
func PostJSONRequest(url string, data interface{}) ([]byte, error) {
bytesData, err := json.Marshal(data)
if err != nil {
return nil, err
}
return PostRequest(url, "application/json;charset=UTF-8", bytes.NewReader(bytesData))
}
// GetRequest GetRequest
func GetRequest(url string) ([]byte, error) {
client := timeoutClient()
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respbody, nil
}
// XMLDecode XMLDecode
func XMLDecode(data []byte, v interface{}) error {
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charset.NewReaderLabel
return decoder.Decode(v)
}
+191
View File
@@ -0,0 +1,191 @@
package util
import (
"gb28181Panda/log"
"github.com/beevik/etree"
"github.com/pkg/errors"
"github.com/spf13/cast"
"math/rand"
"time"
)
type QueryType string
type ControlType string
type WithKeyValue func(element *etree.Element)
const (
DeviceConfig ControlType = "DeviceConfig"
DeviceControl ControlType = "DeviceControl"
)
const (
DeviceStatusCmdType QueryType = "DeviceStatus"
CatalogCmdType QueryType = "Catalog"
DeviceInfoCmdType QueryType = "DeviceInfo"
RecordInfoCmdType QueryType = "RecordInfo"
AlarmCmdType QueryType = "Alarm"
ConfigDownloadCmdType QueryType = "ConfigDownload"
PresetQueryCmdType QueryType = "PresetQuery"
MobilePositionCmdType QueryType = "MobilePosition"
KeepaliveCmdType QueryType = "Keepalive"
)
// CreateQueryXML 主动查询设备相关信息xml文件
func CreateQueryXML(cmd QueryType, deviceId string, kvs ...WithKeyValue) (string, error) {
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("Query")
query.CreateElement("CmdType").CreateText(string(cmd))
query.CreateElement("SN").CreateText(getSN())
query.CreateElement("DeviceID").CreateText(deviceId)
for _, kv := range kvs {
kv(query)
}
document.Indent(2)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", errors.Wrap(err, "encoding catalog query request xml fail")
}
return body, nil
}
// CreateIFameXML 主动查询设备相关信息xml文件
func CreateIFameXML(deviceId string, kvs ...WithKeyValue) (string, error) {
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("Control")
query.CreateElement("CmdType").CreateText("DeviceControl")
query.CreateElement("SN").CreateText(getSN())
query.CreateElement("DeviceID").CreateText(deviceId)
query.CreateElement("IFameCmd").CreateText("send")
info := query.CreateElement("Info")
info.CreateElement("IFameCmd").CreateText("5")
for _, kv := range kvs {
kv(query)
}
document.Indent(4)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", errors.Wrap(err, "encoding catalog query request xml fail")
}
return body, nil
}
// CreateGuardXML 布防xml文件
func CreateGuardXML(cmd ControlType, deviceId string, kvs ...WithKeyValue) (string, error) {
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("Control")
query.CreateElement("CmdType").CreateText(string(cmd))
query.CreateElement("SN").CreateText(getSN())
query.CreateElement("DeviceID").CreateText(deviceId)
query.CreateElement("GuardCmd").CreateText("SetGuard")
for _, kv := range kvs {
kv(query)
}
document.Indent(4)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", errors.Wrap(err, "encoding catalog query request xml fail")
}
return body, nil
}
// CreateSubscribeAlarmXML 订阅预警信息xml文件
func CreateSubscribeAlarmXML(cmd QueryType, deviceId string, kvs ...WithKeyValue) (string, error) {
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("Query")
query.CreateElement("CmdType").CreateText(string(cmd))
query.CreateElement("SN").CreateText(getSN())
query.CreateElement("DeviceID").CreateText(deviceId)
query.CreateElement("StartAlarmPriority").CreateText("0")
query.CreateElement("EndAlarmPriority").CreateText("0")
query.CreateElement("AlarmMethod").CreateText("0")
query.CreateElement("StartTime").CreateText("2023-04-25T00:00:00")
query.CreateElement("EndTime").CreateText("2023-04-27T23:59:59")
for _, kv := range kvs {
kv(query)
}
document.Indent(4)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", errors.Wrap(err, "encoding catalog query request xml fail")
}
return body, nil
}
// CreateControlXml Ptz控制指令
func CreateControlXml(cmd ControlType, deviceId string, kvs ...WithKeyValue) (string, error) {
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("ControlPTZ")
query.CreateElement("CmdType").CreateText(string(cmd))
query.CreateElement("SN").CreateText(getSN())
query.CreateElement("DeviceID").CreateText(deviceId)
for _, kv := range kvs {
kv(query)
}
document.Indent(2)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", errors.Wrap(err, "encoding device control request xml fail")
}
return body, nil
}
// CreateRecordInfoXml 查询录像数据
func CreateRecordInfoXml(cmd QueryType, channelId string, startTime string, endTime string, kvs ...WithKeyValue) (string, string, error) {
sn := getSN()
document := etree.NewDocument()
document.CreateProcInst("xml", "version=\"1.0\" encoding=\"UTF-8\"")
query := document.CreateElement("Query")
query.CreateElement("CmdType").CreateText(string(cmd))
query.CreateElement("SN").CreateText(sn)
query.CreateElement("DeviceID").CreateText(channelId)
query.CreateElement("StartTime").CreateText(startTime)
query.CreateElement("EndTime").CreateText(endTime)
query.CreateElement("Secrecy").CreateText("0")
query.CreateElement("Type").CreateText("all")
for _, kv := range kvs {
kv(query)
}
document.Indent(2)
body, err := document.WriteToString()
if err != nil {
log.Error(err)
return "", "", errors.Wrap(err, "encoding device control request xml fail")
}
return body, sn, nil
}
// WithPTZCmd create 'PTZCmd' item of xml by value
func WithPTZCmd(ptz string) WithKeyValue {
return func(element *etree.Element) {
element.CreateElement("PTZCmd").CreateText(ptz)
}
}
func getSN() string {
rand.New(rand.NewSource(time.Now().UnixMilli()))
return cast.ToString(rand.Intn(10) * 9876)
}
+14
View File
@@ -0,0 +1,14 @@
## UDP
这个是普遍的传输方式GB28181服务端在发invite时,在携带的SDP中包含了接收媒体的端口,设备端(被呼叫端)收到invite后,解析该端口,通过UDP
将媒体流发向该端口。
## TCP被动
GB28181服务端在发invite时,在携带的SDP中包含了接收媒体的端口,并监听该端口的媒体数据,设备端(被呼叫端)收到invite后,解析该端口,通过TCP将媒体流发向该端口。
## TCP主动
设备端(被呼叫端)告知服务端自己的媒体流tcp端口,服务端主动去连接设备端(被呼叫端)的该端口,获取数据。
这个很少用,因为设备端可能在内网环境,流媒体无法访问这个设备
此项目中忽略