mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
Compare commits
136 Commits
tmp
...
v5.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e021131e06 | ||
|
|
2c8e1a7f6e | ||
|
|
d9ef7e46f9 | ||
|
|
987cd4fc4f | ||
|
|
aa611c3a0d | ||
|
|
57f3d150e4 | ||
|
|
3c5becd569 | ||
|
|
25d10473f3 | ||
|
|
ee3a94c0ba | ||
|
|
aef28a4c60 | ||
|
|
a5678668a3 | ||
|
|
8d5daba63b | ||
|
|
1e83a96c40 | ||
|
|
742f8938c3 | ||
|
|
bef04a41ef | ||
|
|
3e6e8de20b | ||
|
|
21d5728607 | ||
|
|
7bcd619bf5 | ||
|
|
4c2a8ed7f4 | ||
|
|
e29a22a875 | ||
|
|
cf604cadc6 | ||
|
|
8ca001e74c | ||
|
|
aff69206d3 | ||
|
|
4de4a832b7 | ||
|
|
c8fa0bd087 | ||
|
|
192a8460ce | ||
|
|
16dcafba9d | ||
|
|
42b29bccec | ||
|
|
dbd1a3697c | ||
|
|
c9954149aa | ||
|
|
324193d30e | ||
|
|
8c6cb12d48 | ||
|
|
d25f85f9a3 | ||
|
|
5f4815c7ed | ||
|
|
66a3e93f4b | ||
|
|
ea2c61cf69 | ||
|
|
34397235d8 | ||
|
|
6069ddf2c2 | ||
|
|
3d6d618a79 | ||
|
|
45479b41b5 | ||
|
|
122a91a9b8 | ||
|
|
7af711fbf4 | ||
|
|
ed6e4b48fe | ||
|
|
546ca02eb6 | ||
|
|
74dd4d7235 | ||
|
|
da338c05c1 | ||
|
|
88c35d22d2 | ||
|
|
f5abb1b436 | ||
|
|
032855f2cc | ||
|
|
254bd2d98e | ||
|
|
851ba4329a | ||
|
|
d1d6b28e0a | ||
|
|
c2f49795fd | ||
|
|
a1a455306b | ||
|
|
6c898cb487 | ||
|
|
79365b7315 | ||
|
|
940a220c11 | ||
|
|
5f77f2f5f9 | ||
|
|
4e46ecc8cd | ||
|
|
0914fb8da7 | ||
|
|
6fdc855279 | ||
|
|
3f698660ae | ||
|
|
dbd3d55237 | ||
|
|
6f51a15fc7 | ||
|
|
470cab36da | ||
|
|
01d41a3426 | ||
|
|
2b462b4c10 | ||
|
|
cc4ee2a447 | ||
|
|
7998d55b41 | ||
|
|
b305f18b2e | ||
|
|
9827efe43e | ||
|
|
6583bc21a8 | ||
|
|
349e9f35a4 | ||
|
|
674d149039 | ||
|
|
18e77cd594 | ||
|
|
6c8c44486c | ||
|
|
69797670be | ||
|
|
262d24d728 | ||
|
|
6ec2de3a82 | ||
|
|
400e8d17e1 | ||
|
|
5916c6838f | ||
|
|
9818b54ef8 | ||
|
|
dfde7c896a | ||
|
|
df7ccaa952 | ||
|
|
f4face865c | ||
|
|
f5fdb51052 | ||
|
|
d5187b56d6 | ||
|
|
551eac055d | ||
|
|
d7872ec492 | ||
|
|
7d83b9dede | ||
|
|
8866e7a68d | ||
|
|
6fa5aba7ff | ||
|
|
4a52cc89bc | ||
|
|
1764a9f7e7 | ||
|
|
0dcfe382fd | ||
|
|
1fa85d39d9 | ||
|
|
4059112b3a | ||
|
|
0cf80cedbf | ||
|
|
8c47c5b513 | ||
|
|
67f979c0d7 | ||
|
|
76e213cbef | ||
|
|
ae3e76b20b | ||
|
|
61607d54fc | ||
|
|
75f1b0fa57 | ||
|
|
90d59eb406 | ||
|
|
d92d3b5820 | ||
|
|
7a7b77d2b4 | ||
|
|
13e4d3fe3d | ||
|
|
518716f383 | ||
|
|
e9e1d7fe95 | ||
|
|
8811e5e0b6 | ||
|
|
7f9bdec10b | ||
|
|
6728be29af | ||
|
|
12555c31eb | ||
|
|
7343e24fb4 | ||
|
|
34c4e9a18d | ||
|
|
a2dcb8a3ef | ||
|
|
2cb60d5a9c | ||
|
|
eef8892618 | ||
|
|
d2fe58be6d | ||
|
|
8ab2fa29d1 | ||
|
|
84f4390834 | ||
|
|
321bba6a0c | ||
|
|
bb92152c15 | ||
|
|
827a0f3fc1 | ||
|
|
45408c78be | ||
|
|
e37b244cc9 | ||
|
|
81a4d60a1e | ||
|
|
58dd654617 | ||
|
|
467ec2356a | ||
|
|
a5399ed11f | ||
|
|
942eeb11b0 | ||
|
|
c1a5ebda13 | ||
|
|
6c8cd34076 | ||
|
|
896f3c107a | ||
|
|
f4923d9df6 |
7
.github/workflows/go.yml
vendored
7
.github/workflows/go.yml
vendored
@@ -27,11 +27,10 @@ jobs:
|
||||
go-version: 1.23.4
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: runner.os−go−{ { hashFiles('**/go.sum') } }
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
key: ${{ runner.os }}−go−${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
@@ -84,7 +83,7 @@ jobs:
|
||||
- name: docker build
|
||||
if: success() && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
tar -zxvf bin/m7s_linux_amd64.tar.gz
|
||||
tar -zxvf bin/m7s_v5_linux_amd64.tar.gz
|
||||
mv m7s monibuca_linux
|
||||
docker login -u langhuihui -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker build -t langhuihui/monibuca:v5 .
|
||||
|
||||
@@ -13,7 +13,7 @@ ENV HOME /monibuca
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN git clone -b v5 --depth 1 https://github.com/langhuihui/monibuca
|
||||
RUN git clone --depth 1 https://github.com/langhuihui/monibuca
|
||||
|
||||
# compile
|
||||
WORKDIR /monibuca
|
||||
@@ -28,7 +28,7 @@ WORKDIR /monibuca
|
||||
COPY --from=builder /monibuca/build /monibuca/
|
||||
RUN cp -r ./config.yaml /etc/monibuca
|
||||
# Export necessary ports
|
||||
EXPOSE 8080 8443 1935 554 5060 9000-20000
|
||||
EXPOSE 5060/udp
|
||||
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
|
||||
EXPOSE 5060/udp 44944/udp
|
||||
|
||||
CMD [ "./monibuca", "-c", "/etc/monibuca/config.yaml" ]
|
||||
|
||||
@@ -112,6 +112,7 @@ The following build tags can be used to customize your build:
|
||||
| postgres | Enables the postgres DB |
|
||||
| duckdb | Enables the duckdb DB |
|
||||
| taskpanic | Throws panic, for testing |
|
||||
| fasthttp | Enables the fasthttp server instead of net/http |
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ go run -tags sqlite main.go
|
||||
| postgres | 启用 PostgreSQL 存储 |
|
||||
| duckdb | 启用 DuckDB 存储 |
|
||||
| taskpanic | 抛出 panic(用于测试) |
|
||||
| fasthttp | 使用 fasthttp 服务器代替标准库 |
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
|
||||
101
api.go
101
api.go
@@ -383,27 +383,24 @@ func (s *Server) api_VideoTrack_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sse := util.NewSSE(rw, r.Context())
|
||||
PlayBlock(suber, (func(frame *pkg.AVFrame) (err error))(nil), func(frame *pkg.AVFrame) (err error) {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = frame.Sequence
|
||||
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(frame.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
|
||||
snap.KeyFrame = frame.IDR
|
||||
for i, wrap := range frame.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
util.NewSSE(rw, r.Context(), func(sse *util.SSE) {
|
||||
PlayBlock(suber, (func(frame *pkg.AVFrame) (err error))(nil), func(frame *pkg.AVFrame) (err error) {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = frame.Sequence
|
||||
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(frame.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
|
||||
snap.KeyFrame = frame.IDR
|
||||
for i, wrap := range frame.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return sse.WriteJSON(&snap)
|
||||
return sse.WriteJSON(&snap)
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) api_AudioTrack_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -419,27 +416,24 @@ func (s *Server) api_AudioTrack_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sse := util.NewSSE(rw, r.Context())
|
||||
PlayBlock(suber, func(frame *pkg.AVFrame) (err error) {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = frame.Sequence
|
||||
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(frame.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
|
||||
snap.KeyFrame = frame.IDR
|
||||
for i, wrap := range frame.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
util.NewSSE(rw, r.Context(), func(sse *util.SSE) {
|
||||
PlayBlock(suber, func(frame *pkg.AVFrame) (err error) {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = frame.Sequence
|
||||
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(frame.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
|
||||
snap.KeyFrame = frame.IDR
|
||||
for i, wrap := range frame.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return sse.WriteJSON(&snap)
|
||||
}, (func(frame *pkg.AVFrame) (err error))(nil))
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
return sse.WriteJSON(&snap)
|
||||
}, (func(frame *pkg.AVFrame) (err error))(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) VideoTrackSnap(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.TrackSnapShotResponse, err error) {
|
||||
@@ -783,29 +777,6 @@ func (s *Server) GetConfig(_ context.Context, req *pb.GetConfigRequest) (res *pb
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) ModifyConfig(_ context.Context, req *pb.ModifyConfigRequest) (res *pb.SuccessResponse, err error) {
|
||||
var conf *config.Config
|
||||
if req.Name == "global" {
|
||||
conf = &s.Config
|
||||
defer s.SaveConfig()
|
||||
} else {
|
||||
p, ok := s.Plugins.Get(req.Name)
|
||||
if !ok {
|
||||
err = pkg.ErrNotFound
|
||||
return
|
||||
}
|
||||
defer p.SaveConfig()
|
||||
conf = &p.Config
|
||||
}
|
||||
var modified map[string]any
|
||||
err = yaml.Unmarshal([]byte(req.Yaml), &modified)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conf.ParseModifyFile(modified)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.ResponseList, err error) {
|
||||
if s.DB == nil {
|
||||
err = pkg.ErrNoDB
|
||||
@@ -842,6 +813,9 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
query = query.Where("end_time <= ?", endTime)
|
||||
}
|
||||
}
|
||||
if req.EventLevel != "" {
|
||||
query = query.Where("event_level = ?", req.EventLevel)
|
||||
}
|
||||
|
||||
query.Count(&totalCount)
|
||||
err = query.Offset(int(offset)).Limit(int(req.PageSize)).Order("start_time desc").Find(&result).Error
|
||||
@@ -860,6 +834,9 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
EndTime: timestamppb.New(recordFile.EndTime),
|
||||
FilePath: recordFile.FilePath,
|
||||
StreamPath: recordFile.StreamPath,
|
||||
EventLevel: recordFile.EventLevel,
|
||||
EventDesc: recordFile.EventDesc,
|
||||
EventName: recordFile.EventName,
|
||||
})
|
||||
}
|
||||
return
|
||||
|
||||
BIN
architecture.jpg
BIN
architecture.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 625 KiB After Width: | Height: | Size: 659 KiB |
434
doc/fmp4.md
Normal file
434
doc/fmp4.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# fMP4 Technology Implementation and Application Based on HLS v7
|
||||
|
||||
## Author's Foreword
|
||||
|
||||
As developers of the Monibuca streaming server, we have been continuously seeking to provide more efficient and flexible streaming solutions. With the evolution of Web frontend technologies, especially the widespread application of Media Source Extensions (MSE), we gradually recognized that traditional streaming transmission solutions can no longer meet the demands of modern applications. During our exploration and practice, we discovered that fMP4 (fragmented MP4) technology effectively bridges traditional media formats with modern Web technologies, providing users with a smoother video experience.
|
||||
|
||||
In the implementation of the MP4 plugin for the Monibuca project, we faced the challenge of efficiently converting recorded MP4 files into a format compatible with MSE playback. Through in-depth research on the HLS v7 protocol and fMP4 container format, we ultimately developed a comprehensive solution supporting real-time conversion from MP4 to fMP4, seamless merging of multiple MP4 segments, and optimizations for frontend MSE playback. This article shares our technical exploration and implementation approach during this process.
|
||||
|
||||
## Introduction
|
||||
|
||||
As streaming media technology evolves, video distribution methods continue to advance. From traditional complete downloads to progressive downloads, and now to widely used adaptive bitrate streaming technology, each advancement has significantly enhanced the user experience. This article will explore the implementation of fMP4 (fragmented MP4) technology based on HLS v7, and how it integrates with Media Source Extensions (MSE) in modern Web frontends to create efficient and smooth video playback experiences.
|
||||
|
||||
## Evolution of HLS Protocol and Introduction of fMP4
|
||||
|
||||
### Traditional HLS and Its Limitations
|
||||
|
||||
HTTP Live Streaming (HLS) is an HTTP adaptive bitrate streaming protocol developed by Apple. In earlier versions, HLS primarily used TS (Transport Stream) segments as the media container format. Although the TS format has good error resilience and streaming characteristics, it also has several limitations:
|
||||
|
||||
1. Larger file size compared to container formats like MP4
|
||||
2. Each TS segment needs to contain complete initialization information, causing redundancy
|
||||
3. Lower integration with other parts of the Web technology stack
|
||||
|
||||
### HLS v7 and fMP4
|
||||
|
||||
HLS v7 introduced support for fMP4 (fragmented MP4) segments, marking a significant advancement in the HLS protocol. As a media container format, fMP4 offers the following advantages over TS:
|
||||
|
||||
1. Smaller file size, higher transmission efficiency
|
||||
2. Shares the same underlying container format with other streaming protocols like DASH, facilitating a unified technology stack
|
||||
3. Better support for modern codecs
|
||||
4. Better compatibility with MSE (Media Source Extensions)
|
||||
|
||||
In HLS v7, seamless playback of fMP4 segments is achieved by specifying initialization segments using the `#EXT-X-MAP` tag in the playlist.
|
||||
|
||||
## MP4 File Structure and fMP4 Basic Principles
|
||||
|
||||
### Traditional MP4 Structure
|
||||
|
||||
Traditional MP4 files follow the ISO Base Media File Format (ISO BMFF) specification and mainly consist of the following parts:
|
||||
|
||||
1. **ftyp** (File Type Box): Indicates the format and compatibility information of the file
|
||||
2. **moov** (Movie Box): Contains metadata about the media, such as track information, codec parameters, etc.
|
||||
3. **mdat** (Media Data Box): Contains the actual media data
|
||||
|
||||
In traditional MP4, the `moov` is usually located at the beginning or end of the file and contains all the metadata and index data for the entire video. This structure is not friendly for streaming transmission because the player needs to acquire the complete `moov` before playback can begin.
|
||||
|
||||
Below is a diagram of the MP4 file box structure:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
MP4[MP4 File] --> FTYP[ftyp box]
|
||||
MP4 --> MOOV[moov box]
|
||||
MP4 --> MDAT[mdat box]
|
||||
MOOV --> MVHD[mvhd: Movie header]
|
||||
MOOV --> TRAK1[trak: Video track]
|
||||
MOOV --> TRAK2[trak: Audio track]
|
||||
TRAK1 --> TKHD1[tkhd: Track header]
|
||||
TRAK1 --> MDIA1[mdia: Media info]
|
||||
TRAK2 --> TKHD2[tkhd: Track header]
|
||||
TRAK2 --> MDIA2[mdia: Media info]
|
||||
MDIA1 --> MDHD1[mdhd: Media header]
|
||||
MDIA1 --> HDLR1[hdlr: Handler]
|
||||
MDIA1 --> MINF1[minf: Media info container]
|
||||
MDIA2 --> MDHD2[mdhd: Media header]
|
||||
MDIA2 --> HDLR2[hdlr: Handler]
|
||||
MDIA2 --> MINF2[minf: Media info container]
|
||||
MINF1 --> STBL1[stbl: Sample table]
|
||||
MINF2 --> STBL2[stbl: Sample table]
|
||||
STBL1 --> STSD1[stsd: Sample description]
|
||||
STBL1 --> STTS1[stts: Time-to-sample]
|
||||
STBL1 --> STSC1[stsc: Sample-to-chunk]
|
||||
STBL1 --> STSZ1[stsz: Sample size]
|
||||
STBL1 --> STCO1[stco: Chunk offset]
|
||||
STBL2 --> STSD2[stsd: Sample description]
|
||||
STBL2 --> STTS2[stts: Time-to-sample]
|
||||
STBL2 --> STSC2[stsc: Sample-to-chunk]
|
||||
STBL2 --> STSZ2[stsz: Sample size]
|
||||
STBL2 --> STCO2[stco: Chunk offset]
|
||||
```
|
||||
|
||||
### fMP4 Structural Characteristics
|
||||
|
||||
fMP4 (fragmented MP4) restructures the traditional MP4 format with the following key features:
|
||||
|
||||
1. Divides media data into multiple fragments
|
||||
2. Each fragment contains its own metadata and media data
|
||||
3. The file structure is more suitable for streaming transmission
|
||||
|
||||
The main components of fMP4:
|
||||
|
||||
1. **ftyp**: Same as traditional MP4, located at the beginning of the file
|
||||
2. **moov**: Contains overall track information, but not specific sample information
|
||||
3. **moof** (Movie Fragment Box): Contains metadata for specific fragments
|
||||
4. **mdat**: Contains media data associated with the preceding moof
|
||||
|
||||
Below is a diagram of the fMP4 file box structure:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
FMP4[fMP4 File] --> FTYP[ftyp box]
|
||||
FMP4 --> MOOV[moov box]
|
||||
FMP4 --> MOOF1[moof 1: Fragment 1 metadata]
|
||||
FMP4 --> MDAT1[mdat 1: Fragment 1 media data]
|
||||
FMP4 --> MOOF2[moof 2: Fragment 2 metadata]
|
||||
FMP4 --> MDAT2[mdat 2: Fragment 2 media data]
|
||||
FMP4 -.- MOOFN[moof n: Fragment n metadata]
|
||||
FMP4 -.- MDATN[mdat n: Fragment n media data]
|
||||
|
||||
MOOV --> MVHD[mvhd: Movie header]
|
||||
MOOV --> MVEX[mvex: Movie extends]
|
||||
MOOV --> TRAK1[trak: Video track]
|
||||
MOOV --> TRAK2[trak: Audio track]
|
||||
|
||||
MVEX --> TREX1[trex 1: Track extends]
|
||||
MVEX --> TREX2[trex 2: Track extends]
|
||||
|
||||
MOOF1 --> MFHD1[mfhd: Fragment header]
|
||||
MOOF1 --> TRAF1[traf: Track fragment]
|
||||
|
||||
TRAF1 --> TFHD1[tfhd: Track fragment header]
|
||||
TRAF1 --> TFDT1[tfdt: Track fragment decode time]
|
||||
TRAF1 --> TRUN1[trun: Track run]
|
||||
```
|
||||
|
||||
This structure allows the player to immediately begin processing subsequent `moof`+`mdat` fragments after receiving the initial `ftyp` and `moov`, making it highly suitable for streaming transmission and real-time playback.
|
||||
|
||||
## Conversion Principles from MP4 to fMP4
|
||||
|
||||
The MP4 to fMP4 conversion process can be illustrated by the following sequence diagram:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP4 as Source MP4 File
|
||||
participant Demuxer as MP4 Parser
|
||||
participant Muxer as fMP4 Muxer
|
||||
participant fMP4 as Target fMP4 File
|
||||
|
||||
MP4->>Demuxer: Read MP4 file
|
||||
Note over Demuxer: Parse file structure
|
||||
Demuxer->>Demuxer: Extract ftyp info
|
||||
Demuxer->>Demuxer: Parse moov box
|
||||
Demuxer->>Demuxer: Extract tracks info<br>(video, audio tracks)
|
||||
Demuxer->>Muxer: Pass track metadata
|
||||
|
||||
Muxer->>fMP4: Write ftyp box
|
||||
Muxer->>Muxer: Create streaming-friendly moov
|
||||
Muxer->>Muxer: Add mvex extension
|
||||
Muxer->>fMP4: Write moov box
|
||||
|
||||
loop For each media sample
|
||||
Demuxer->>MP4: Read sample data
|
||||
Demuxer->>Muxer: Pass sample
|
||||
Muxer->>Muxer: Create moof box<br>(time and position info)
|
||||
Muxer->>Muxer: Create mdat box<br>(actual media data)
|
||||
Muxer->>fMP4: Write moof+mdat pair
|
||||
end
|
||||
|
||||
Note over fMP4: Conversion complete
|
||||
```
|
||||
|
||||
As shown in the diagram, the conversion process consists of three key steps:
|
||||
|
||||
1. **Parse the source MP4 file**: Read and parse the structure of the original MP4 file, extract information about video and audio tracks, including codec type, frame rate, resolution, and other metadata.
|
||||
|
||||
2. **Create the initialization part of fMP4**: Build the file header and initialization section, including the ftyp and moov boxes. These serve as the initialization segment, containing all the information needed by the decoder, but without actual media sample data.
|
||||
|
||||
3. **Create fragments for each sample**: Read the sample data from the original MP4 one by one, then create corresponding moof and mdat box pairs for each sample (or group of samples).
|
||||
|
||||
This conversion method transforms MP4 files that were only suitable for download-and-play into fMP4 format suitable for streaming transmission.
|
||||
|
||||
## Multiple MP4 Segment Merging Technology
|
||||
|
||||
### User Requirement: Time-Range Recording Downloads
|
||||
|
||||
In scenarios such as video surveillance, course playback, and live broadcast recording, users often need to download recorded content within a specific time range. For example, a security system operator might only need to export video segments containing specific events, or a student on an educational platform might only want to download key parts of a course. However, since systems typically divide recorded files by fixed durations (e.g., 30 minutes or 1 hour) or specific events (such as the start/end of a live broadcast), the time range needed by users often spans multiple independent MP4 files.
|
||||
|
||||
In the Monibuca project, we developed a solution based on time range queries and multi-file merging to address this need. Users only need to specify the start and end times of the content they require, and the system will:
|
||||
|
||||
1. Query the database to find all recording files that overlap with the specified time range
|
||||
2. Extract relevant time segments from each file
|
||||
3. Seamlessly merge these segments into a single downloadable file
|
||||
|
||||
This approach greatly enhances the user experience, allowing them to precisely obtain the content they need without having to download and browse through large amounts of irrelevant video content.
|
||||
|
||||
### Database Design and Time Range Queries
|
||||
|
||||
To support time range queries, our recording file metadata in the database includes the following key fields:
|
||||
|
||||
- Stream Path: Identifies the video source
|
||||
- Start Time: The start time of the recording segment
|
||||
- End Time: The end time of the recording segment
|
||||
- File Path: The storage location of the actual recording file
|
||||
- Type: The file format, such as "mp4"
|
||||
|
||||
When a user requests recordings within a specific time range, the system executes a query similar to the following:
|
||||
|
||||
```sql
|
||||
SELECT * FROM record_streams
|
||||
WHERE stream_path = ? AND type = 'mp4'
|
||||
AND start_time <= ? AND end_time >= ?
|
||||
```
|
||||
|
||||
This returns all recording segments that intersect with the requested time range, after which the system needs to extract the relevant parts and merge them.
|
||||
|
||||
### Technical Challenges of Multiple MP4 Merging
|
||||
|
||||
Merging multiple MP4 files is not a simple file concatenation but requires addressing the following technical challenges:
|
||||
|
||||
1. **Timestamp Continuity**: Ensuring that the timestamps in the merged video are continuous, without jumps or overlaps
|
||||
2. **Codec Consistency**: Handling cases where different MP4 files may use different encoding parameters
|
||||
3. **Metadata Merging**: Correctly merging the moov box information from various files
|
||||
4. **Precise Cutting**: Precisely extracting content within the user-specified time range from each file
|
||||
|
||||
In practical applications, we implemented two merging strategies: regular MP4 merging and fMP4 merging. These strategies each have their advantages and are suitable for different application scenarios.
|
||||
|
||||
### Regular MP4 Merging Process
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as User
|
||||
participant API as API Service
|
||||
participant DB as Database
|
||||
participant MP4s as Multiple MP4 Files
|
||||
participant Muxer as MP4 Muxer
|
||||
participant Output as Output MP4 File
|
||||
|
||||
User->>API: Request time-range recording<br>(stream, startTime, endTime)
|
||||
API->>DB: Query records within specified range
|
||||
DB-->>API: Return matching recording list
|
||||
|
||||
loop For each MP4 file
|
||||
API->>MP4s: Read file
|
||||
MP4s->>Muxer: Parse file structure
|
||||
Muxer->>Muxer: Parse track info
|
||||
Muxer->>Muxer: Extract media samples
|
||||
Muxer->>Muxer: Adjust timestamps for continuity
|
||||
Muxer->>Muxer: Record sample info and offsets
|
||||
Note over Muxer: Skip samples outside time range
|
||||
end
|
||||
|
||||
Muxer->>Output: Write ftyp box
|
||||
Muxer->>Output: Write adjusted sample data
|
||||
Muxer->>Muxer: Create moov containing all sample info
|
||||
Muxer->>Output: Write merged moov box
|
||||
Output-->>User: Provide merged file to user
|
||||
```
|
||||
|
||||
In this approach, the merging process primarily involves arranging media samples from different MP4 files in sequence and adjusting timestamps to ensure continuity. Finally, a new `moov` box containing all sample information is generated. The advantage of this method is its good compatibility, as almost all players can play the merged file normally, making it suitable for download and offline playback scenarios.
|
||||
|
||||
It's particularly worth noting that in the code implementation, we handle the overlap relationship between the time range in the parameters and the actual recording time, extracting only the content that users truly need:
|
||||
|
||||
```go
|
||||
if i == 0 {
|
||||
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
|
||||
var startSample *box.Sample
|
||||
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
|
||||
tsOffset = 0
|
||||
continue
|
||||
}
|
||||
tsOffset = -int64(startSample.Timestamp)
|
||||
}
|
||||
|
||||
// In the last file, frames beyond the end time are skipped
|
||||
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
### fMP4 Merging Process
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as User
|
||||
participant API as API Service
|
||||
participant DB as Database
|
||||
participant MP4s as Multiple MP4 Files
|
||||
participant Muxer as fMP4 Muxer
|
||||
participant Output as Output fMP4 File
|
||||
|
||||
User->>API: Request time-range recording<br>(stream, startTime, endTime)
|
||||
API->>DB: Query records within specified range
|
||||
DB-->>API: Return matching recording list
|
||||
|
||||
Muxer->>Output: Write ftyp box
|
||||
Muxer->>Output: Write initial moov box<br>(including mvex)
|
||||
|
||||
loop For each MP4 file
|
||||
API->>MP4s: Read file
|
||||
MP4s->>Muxer: Parse file structure
|
||||
Muxer->>Muxer: Parse track info
|
||||
Muxer->>Muxer: Extract media samples
|
||||
|
||||
loop For each sample
|
||||
Note over Muxer: Check if sample is within target time range
|
||||
Muxer->>Muxer: Adjust timestamp
|
||||
Muxer->>Muxer: Create moof+mdat pair
|
||||
Muxer->>Output: Write moof+mdat pair
|
||||
end
|
||||
end
|
||||
|
||||
Output-->>User: Provide merged file to user
|
||||
```
|
||||
|
||||
The fMP4 merging is more flexible, with each sample packed into an independent `moof`+`mdat` fragment, maintaining independently decodable characteristics, which is more conducive to streaming transmission and random access. This approach is particularly suitable for integration with MSE and HLS, providing support for real-time streaming playback, allowing users to efficiently play merged content directly in the browser without waiting for the entire file to download.
|
||||
|
||||
### Handling Codec Compatibility in Merging
|
||||
|
||||
In the process of merging multiple recordings, a key challenge we face is handling potential codec parameter differences between files. For example, during long-term recording, a camera might adjust video resolution due to environmental changes, or an encoder might reinitialize, causing changes in encoding parameters.
|
||||
|
||||
To solve this problem, Monibuca implements a smart track version management system that identifies changes by comparing encoder-specific data (ExtraData):
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Muxer as Merger
|
||||
participant Track as Track Manager
|
||||
participant History as Track Version History
|
||||
|
||||
loop For each new track
|
||||
Muxer->>Track: Check track encoding parameters
|
||||
Track->>History: Compare with existing track versions
|
||||
alt Found matching track version
|
||||
History-->>Track: Return existing track
|
||||
Track-->>Muxer: Use existing track
|
||||
else No matching version
|
||||
Track->>Track: Create new track version
|
||||
Track->>History: Add to version history
|
||||
Track-->>Muxer: Use new track
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This design ensures that even if there are encoding parameter changes in the original recordings, the merged file can maintain correct decoding parameters, providing users with a smooth playback experience.
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
When processing large video files or a large number of concurrent requests, the performance of the merging process is an important consideration. We have adopted the following optimization measures:
|
||||
|
||||
1. **Streaming Processing**: Process samples frame by frame to avoid loading entire files into memory
|
||||
2. **Parallel Processing**: Use parallel processing for multiple independent tasks (such as file parsing)
|
||||
3. **Smart Caching**: Cache commonly used encoding parameters and file metadata
|
||||
4. **On-demand Reading**: Only read and process samples within the target time range
|
||||
|
||||
These optimizations enable the system to efficiently process large-scale recording merging requests, completing processing within a reasonable time even for long-term recordings spanning hours or days.
|
||||
|
||||
The multiple MP4 merging functionality greatly enhances the flexibility and user experience of Monibuca as a streaming server, allowing users to precisely obtain the recorded content they need, regardless of how the original recordings are segmented and stored.
|
||||
|
||||
## Media Source Extensions (MSE) and fMP4 Compatibility Implementation
|
||||
|
||||
### MSE Technology Overview
|
||||
|
||||
Media Source Extensions (MSE) is a JavaScript API that allows web developers to directly manipulate media stream data. It enables custom adaptive bitrate streaming players to be implemented entirely in the browser without relying on external plugins.
|
||||
|
||||
The core working principle of MSE is:
|
||||
1. Create a MediaSource object
|
||||
2. Create one or more SourceBuffer objects
|
||||
3. Append media fragments to the SourceBuffer
|
||||
4. The browser is responsible for decoding and playing these fragments
|
||||
|
||||
### Perfect Integration of fMP4 with MSE
|
||||
|
||||
The fMP4 format has natural compatibility with MSE, mainly reflected in:
|
||||
|
||||
1. Each fragment of fMP4 can be independently decoded
|
||||
2. The clear separation of initialization segments and media segments conforms to MSE's buffer management model
|
||||
3. Precise timestamp control enables seamless splicing
|
||||
|
||||
The following sequence diagram shows how fMP4 works with MSE:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Browser Client
|
||||
participant Server as Server
|
||||
participant MSE as MediaSource API
|
||||
participant Video as HTML5 Video Element
|
||||
|
||||
Client->>Video: Create video element
|
||||
Client->>MSE: Create MediaSource object
|
||||
Client->>Video: Set video.src = URL.createObjectURL(mediaSource)
|
||||
MSE-->>Client: sourceopen event
|
||||
|
||||
Client->>MSE: Create SourceBuffer
|
||||
Client->>Server: Request initialization segment (ftyp+moov)
|
||||
Server-->>Client: Return initialization segment
|
||||
Client->>MSE: appendBuffer(initialization segment)
|
||||
|
||||
loop During playback
|
||||
Client->>Server: Request media segment (moof+mdat)
|
||||
Server-->>Client: Return media segment
|
||||
Client->>MSE: appendBuffer(media segment)
|
||||
MSE-->>Video: Decode and render frames
|
||||
end
|
||||
```
|
||||
|
||||
In Monibuca's implementation, we've made special optimizations for MSE: creating independent moof and mdat for each frame. Although this approach adds some overhead, it provides high flexibility, particularly suitable for low-latency real-time streaming scenarios and precise frame-level operations.
|
||||
|
||||
## Integration of HLS and fMP4 in Practical Applications
|
||||
|
||||
In practical applications, we combine fMP4 technology with the HLS v7 protocol to implement time-range-based on-demand playback. The system can find the corresponding MP4 records from the database based on the time range specified by the user, and then generate an fMP4 format HLS playlist:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client
|
||||
participant Server as HLS Server
|
||||
participant DB as Database
|
||||
participant MP4Plugin as MP4 Plugin
|
||||
|
||||
Client->>Server: Request fMP4.m3u8<br>with time range parameters
|
||||
Server->>DB: Query MP4 records within specified range
|
||||
DB-->>Server: Return record list
|
||||
|
||||
Server->>Server: Create HLS v7 playlist<br>Version: 7
|
||||
loop For each record
|
||||
Server->>Server: Calculate duration
|
||||
Server->>Server: Add media segment URL<br>/mp4/download/{stream}.fmp4?id={id}
|
||||
end
|
||||
|
||||
Server->>Server: Add #EXT-X-ENDLIST marker
|
||||
Server-->>Client: Return HLS playlist
|
||||
|
||||
loop For each segment
|
||||
Client->>MP4Plugin: Request fMP4 segment
|
||||
MP4Plugin->>MP4Plugin: Convert to fMP4 format
|
||||
MP4Plugin-->>Client: Return fMP4 segment
|
||||
end
|
||||
```
|
||||
|
||||
Through this approach, we maintain compatibility with existing HLS clients while leveraging the advantages of the fMP4 format to provide more efficient streaming services.
|
||||
|
||||
## Conclusion
|
||||
|
||||
As a modern media container format, fMP4 combines the efficient compression of MP4 with the flexibility of streaming transmission, making it highly suitable for video distribution needs in modern web applications. Through integration with HLS v7 and MSE technologies, more efficient and flexible streaming services can be achieved.
|
||||
|
||||
In the practice of the Monibuca project, we have successfully built a complete streaming solution by implementing MP4 to fMP4 conversion, merging multiple MP4 files, and optimizing fMP4 fragment generation for MSE. The application of these technologies enables our system to provide a better user experience, including faster startup times, smoother quality transitions, and lower bandwidth consumption.
|
||||
|
||||
As video technology continues to evolve, fMP4, as a bridge connecting traditional media formats with modern Web technologies, will continue to play an important role in the streaming media field. The Monibuca project will also continue to explore and optimize this technology to provide users with higher quality streaming services.
|
||||
434
doc_CN/fmp4_technology_overview.md
Normal file
434
doc_CN/fmp4_technology_overview.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# 基于HLS v7的fMP4技术实现与应用
|
||||
|
||||
## 作者前言
|
||||
|
||||
作为Monibuca流媒体服务器的开发者,我们一直在寻求提供更高效、更灵活的流媒体解决方案。随着Web前端技术的发展,特别是Media Source Extensions (MSE) 的广泛应用,我们逐渐认识到传统的流媒体传输方案已难以满足现代应用的需求。在探索与实践中,我们发现fMP4(fragmented MP4)技术能够很好地连接传统媒体格式与现代Web技术,为用户提供更流畅的视频体验。
|
||||
|
||||
Monibuca项目在MP4插件的实现中,我们面临着如何将已录制的MP4文件高效转换为支持MSE播放的格式这一挑战。通过深入研究HLS v7协议和fMP4容器格式,我们最终实现了一套完整的解决方案,支持MP4到fMP4的实时转换、多段MP4的无缝合并,以及针对前端MSE播放的优化。本文将分享我们在这一过程中的技术探索和实现思路。
|
||||
|
||||
## 引言
|
||||
|
||||
随着流媒体技术的发展,视频分发方式不断演进。从传统的整体式下载到渐进式下载,再到现在广泛使用的自适应码率流媒体技术,每一步演进都极大地提升了用户体验。本文将探讨基于HLS v7的fMP4(fragmented MP4)技术实现,以及它如何与现代Web前端中的媒体源扩展(Media Source Extensions, MSE)结合,打造高效流畅的视频播放体验。
|
||||
|
||||
## HLS协议演进与fMP4的引入
|
||||
|
||||
### 传统HLS与其局限性
|
||||
|
||||
HTTP Live Streaming (HLS)是由Apple公司开发的HTTP自适应比特率流媒体通信协议。在早期版本中,HLS主要使用TS(Transport Stream)切片作为媒体容器格式。虽然TS格式具有良好的容错性和流式传输特性,但也存在一些局限性:
|
||||
|
||||
1. 相比于MP4等容器格式,TS文件体积较大
|
||||
2. 每个TS切片都需要包含完整的初始化信息,导致冗余
|
||||
3. 与Web技术栈的其他部分集成度不高
|
||||
|
||||
### HLS v7与fMP4
|
||||
|
||||
HLS v7版本引入了对fMP4(fragmented MP4)切片的支持,这是HLS协议的一个重大进步。fMP4作为媒体容器格式相比TS具有以下优势:
|
||||
|
||||
1. 文件体积更小,传输效率更高
|
||||
2. 与DASH等其他流媒体协议共享相同的底层容器格式,有利于统一技术栈
|
||||
3. 更好地支持现代编解码器
|
||||
4. 与MSE(Media Source Extensions)有更好的兼容性
|
||||
|
||||
在HLS v7中,通过在播放列表中使用`#EXT-X-MAP`标签指定初始化片段,可以实现fMP4切片的无缝播放。
|
||||
|
||||
## MP4文件结构与fMP4的基本原理
|
||||
|
||||
### 传统MP4结构
|
||||
|
||||
传统的MP4文件遵循ISO Base Media File Format(ISO BMFF)规范,主要由以下几个部分组成:
|
||||
|
||||
1. **ftyp** (File Type Box): 指示文件的格式和兼容性信息
|
||||
2. **moov** (Movie Box): 包含媒体的元数据信息,如轨道信息、编解码器参数等
|
||||
3. **mdat** (Media Data Box): 包含实际的媒体数据
|
||||
|
||||
在传统MP4中,`moov`通常位于文件开头或结尾,包含了整个视频的所有元信息和索引数据。这种结构对于流式传输不友好,因为播放器需要先获取完整的`moov`才能开始播放。
|
||||
|
||||
以下是MP4文件的box结构示意图:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
MP4[MP4文件] --> FTYP[ftyp box]
|
||||
MP4 --> MOOV[moov box]
|
||||
MP4 --> MDAT[mdat box]
|
||||
MOOV --> MVHD[mvhd: 电影头信息]
|
||||
MOOV --> TRAK1[trak: 视频轨道]
|
||||
MOOV --> TRAK2[trak: 音频轨道]
|
||||
TRAK1 --> TKHD1[tkhd: 轨道头信息]
|
||||
TRAK1 --> MDIA1[mdia: 媒体信息]
|
||||
TRAK2 --> TKHD2[tkhd: 轨道头信息]
|
||||
TRAK2 --> MDIA2[mdia: 媒体信息]
|
||||
MDIA1 --> MDHD1[mdhd: 媒体头信息]
|
||||
MDIA1 --> HDLR1[hdlr: 处理器信息]
|
||||
MDIA1 --> MINF1[minf: 媒体信息容器]
|
||||
MDIA2 --> MDHD2[mdhd: 媒体头信息]
|
||||
MDIA2 --> HDLR2[hdlr: 处理器信息]
|
||||
MDIA2 --> MINF2[minf: 媒体信息容器]
|
||||
MINF1 --> STBL1[stbl: 采样表]
|
||||
MINF2 --> STBL2[stbl: 采样表]
|
||||
STBL1 --> STSD1[stsd: 采样描述]
|
||||
STBL1 --> STTS1[stts: 时间戳信息]
|
||||
STBL1 --> STSC1[stsc: 块到采样映射]
|
||||
STBL1 --> STSZ1[stsz: 采样大小]
|
||||
STBL1 --> STCO1[stco: 块偏移]
|
||||
STBL2 --> STSD2[stsd: 采样描述]
|
||||
STBL2 --> STTS2[stts: 时间戳信息]
|
||||
STBL2 --> STSC2[stsc: 块到采样映射]
|
||||
STBL2 --> STSZ2[stsz: 采样大小]
|
||||
STBL2 --> STCO2[stco: 块偏移]
|
||||
```
|
||||
|
||||
### fMP4的结构特点
|
||||
|
||||
fMP4(fragmented MP4)对传统MP4格式进行了重构,主要特点是:
|
||||
|
||||
1. 将媒体数据分割成多个片段(fragments)
|
||||
2. 每个片段包含自己的元数据和媒体数据
|
||||
3. 文件结构更适合流式传输
|
||||
|
||||
fMP4的主要组成部分:
|
||||
|
||||
1. **ftyp**: 与传统MP4相同,位于文件开头
|
||||
2. **moov**: 包含整体的轨道信息,但不包含具体的样本信息
|
||||
3. **moof** (Movie Fragment Box): 包含特定片段的元数据
|
||||
4. **mdat**: 包含与前面的moof相关联的媒体数据
|
||||
|
||||
以下是fMP4文件的box结构示意图:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
FMP4[fMP4文件] --> FTYP[ftyp box]
|
||||
FMP4 --> MOOV[moov box]
|
||||
FMP4 --> MOOF1[moof 1: 片段1元数据]
|
||||
FMP4 --> MDAT1[mdat 1: 片段1媒体数据]
|
||||
FMP4 --> MOOF2[moof 2: 片段2元数据]
|
||||
FMP4 --> MDAT2[mdat 2: 片段2媒体数据]
|
||||
FMP4 -.- MOOFN[moof n: 片段n元数据]
|
||||
FMP4 -.- MDATN[mdat n: 片段n媒体数据]
|
||||
|
||||
MOOV --> MVHD[mvhd: 电影头信息]
|
||||
MOOV --> MVEX[mvex: 电影扩展]
|
||||
MOOV --> TRAK1[trak: 视频轨道]
|
||||
MOOV --> TRAK2[trak: 音频轨道]
|
||||
|
||||
MVEX --> TREX1[trex 1: 轨道扩展]
|
||||
MVEX --> TREX2[trex 2: 轨道扩展]
|
||||
|
||||
MOOF1 --> MFHD1[mfhd: 片段头]
|
||||
MOOF1 --> TRAF1[traf: 轨道片段]
|
||||
|
||||
TRAF1 --> TFHD1[tfhd: 轨道片段头]
|
||||
TRAF1 --> TFDT1[tfdt: 轨道片段基准时间]
|
||||
TRAF1 --> TRUN1[trun: 轨道运行信息]
|
||||
```
|
||||
|
||||
这种结构允许播放器在接收到初始的`ftyp`和`moov`后,可以立即开始处理后续接收到的`moof`+`mdat`片段,非常适合流式传输和实时播放。
|
||||
|
||||
## MP4到fMP4的转换原理
|
||||
|
||||
MP4到fMP4的转换过程可以通过以下时序图来说明:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP4 as 源MP4文件
|
||||
participant Demuxer as MP4解析器
|
||||
participant Muxer as fMP4封装器
|
||||
participant fMP4 as 目标fMP4文件
|
||||
|
||||
MP4->>Demuxer: 读取MP4文件
|
||||
Note over Demuxer: 解析文件结构
|
||||
Demuxer->>Demuxer: 提取ftyp信息
|
||||
Demuxer->>Demuxer: 解析moov box
|
||||
Demuxer->>Demuxer: 提取tracks信息<br>(视频、音频轨道)
|
||||
Demuxer->>Muxer: 传递tracks元数据
|
||||
|
||||
Muxer->>fMP4: 写入ftyp box
|
||||
Muxer->>Muxer: 创建适合流式传输的moov
|
||||
Muxer->>Muxer: 添加mvex扩展
|
||||
Muxer->>fMP4: 写入moov box
|
||||
|
||||
loop 对每个媒体样本
|
||||
Demuxer->>MP4: 读取样本数据
|
||||
Demuxer->>Muxer: 传递样本
|
||||
Muxer->>Muxer: 创建moof box<br>(包含时间和位置信息)
|
||||
Muxer->>Muxer: 创建mdat box<br>(包含实际媒体数据)
|
||||
Muxer->>fMP4: 写入moof+mdat对
|
||||
end
|
||||
|
||||
Note over fMP4: 完成转换
|
||||
```
|
||||
|
||||
从上图可以看出,转换过程主要包含三个关键步骤:
|
||||
|
||||
1. **解析源MP4文件**:读取并解析原始MP4文件的结构,提取出视频轨、音频轨的相关信息,包括编解码器类型、帧率、分辨率等元数据。
|
||||
|
||||
2. **创建fMP4的初始化部分**:构建文件头和初始化部分,包括ftyp和moov box,它们作为初始化段(initialization segment),包含了解码器需要的所有信息,但不包含实际的媒体样本数据。
|
||||
|
||||
3. **为每个样本创建片段**:逐个读取原始MP4中的样本数据,然后为每个样本(或一组样本)创建对应的moof和mdat box对。
|
||||
|
||||
这种转换方式使得原本只适合下载后播放的MP4文件变成了适合流式传输的fMP4格式。
|
||||
|
||||
## MP4多段合并技术
|
||||
|
||||
### 用户需求:时间范围录像下载
|
||||
|
||||
在视频监控、课程回放和直播录制等场景中,用户经常需要下载特定时间范围内的录像内容。例如,一个安防系统的操作员可能只需要导出包含特定事件的视频片段,或者一个教育平台的学生可能只想下载课程中的重点部分。然而,由于系统通常按照固定时长(如30分钟或1小时)或特定事件(如直播开始/结束)来分割录制文件,用户需要的时间范围往往横跨多个独立的MP4文件。
|
||||
|
||||
在Monibuca项目中,我们针对这一需求,开发了基于时间范围查询和多文件合并的解决方案。用户只需指定所需内容的起止时间,系统会:
|
||||
|
||||
1. 查询数据库,找出所有与指定时间范围重叠的录像文件
|
||||
2. 从每个文件中提取相关的时间片段
|
||||
3. 将这些片段无缝合并为单个下载文件
|
||||
|
||||
这种方式极大地提升了用户体验,使其能够精确获取所需内容,而不必下载和浏览大量无关的视频内容。
|
||||
|
||||
### 数据库设计与时间范围查询
|
||||
|
||||
为支持时间范围查询,我们的录像文件元数据在数据库中包含以下关键字段:
|
||||
|
||||
- 流路径(StreamPath):标识视频源
|
||||
- 开始时间(StartTime):录像片段的开始时间
|
||||
- 结束时间(EndTime):录像片段的结束时间
|
||||
- 文件路径(FilePath):实际录像文件的存储位置
|
||||
- 文件类型(Type):文件格式,如"mp4"
|
||||
|
||||
当用户请求特定时间范围的录像时,系统执行类似以下的查询:
|
||||
|
||||
```sql
|
||||
SELECT * FROM record_streams
|
||||
WHERE stream_path = ? AND type = 'mp4'
|
||||
AND start_time <= ? AND end_time >= ?
|
||||
```
|
||||
|
||||
这将返回所有与请求时间范围有交集的录像片段,然后系统需要从中提取相关部分并合并。
|
||||
|
||||
### 多段MP4合并的技术挑战
|
||||
|
||||
合并多个MP4文件并非简单的文件拼接,而是需要处理以下技术挑战:
|
||||
|
||||
1. **时间戳连续性**:确保合并后视频的时间戳连续,没有跳跃或重叠
|
||||
2. **编解码一致性**:处理不同MP4文件可能使用不同编码参数的情况
|
||||
3. **元数据合并**:正确合并各文件的moov box信息
|
||||
4. **精确剪切**:从每个文件中精确提取用户指定时间范围的内容
|
||||
|
||||
在实际应用中,我们实现了两种合并策略:普通MP4合并和fMP4合并。这两种策略各有优势,适用于不同的应用场景。
|
||||
|
||||
### 普通MP4合并流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
participant API as API服务
|
||||
participant DB as 数据库
|
||||
participant MP4s as 多个MP4文件
|
||||
participant Muxer as MP4封装器
|
||||
participant Output as 输出MP4文件
|
||||
|
||||
User->>API: 请求时间范围录像<br>(stream, startTime, endTime)
|
||||
API->>DB: 查询指定范围的录像记录
|
||||
DB-->>API: 返回符合条件的录像列表
|
||||
|
||||
loop 对每个MP4文件
|
||||
API->>MP4s: 读取文件
|
||||
MP4s->>Muxer: 解析文件结构
|
||||
Muxer->>Muxer: 解析轨道信息
|
||||
Muxer->>Muxer: 提取媒体样本
|
||||
Muxer->>Muxer: 调整时间戳保持连续性
|
||||
Muxer->>Muxer: 记录样本信息和偏移量
|
||||
Note over Muxer: 跳过时间范围外的样本
|
||||
end
|
||||
|
||||
Muxer->>Output: 写入ftyp box
|
||||
Muxer->>Output: 写入调整后的样本数据
|
||||
Muxer->>Muxer: 创建包含所有样本信息的moov
|
||||
Muxer->>Output: 写入合并后的moov box
|
||||
Output-->>User: 向用户提供合并后的文件
|
||||
```
|
||||
|
||||
这种方式下,合并过程主要是将不同MP4文件的媒体样本连续排列,并调整时间戳确保连续性。最后,重新生成一个包含所有样本信息的`moov` box。这种方法的优点是兼容性好,几乎所有播放器都能正常播放合并后的文件,适合用于下载和离线播放场景。
|
||||
|
||||
特别值得注意的是,在代码实现中,我们会处理参数中时间范围与实际录像时间的重叠关系,只提取用户真正需要的内容:
|
||||
|
||||
```go
|
||||
if i == 0 {
|
||||
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
|
||||
var startSample *box.Sample
|
||||
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
|
||||
tsOffset = 0
|
||||
continue
|
||||
}
|
||||
tsOffset = -int64(startSample.Timestamp)
|
||||
}
|
||||
|
||||
// 在最后一个文件中,超出结束时间的帧会被跳过
|
||||
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
### fMP4合并流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
participant API as API服务
|
||||
participant DB as 数据库
|
||||
participant MP4s as 多个MP4文件
|
||||
participant Muxer as fMP4封装器
|
||||
participant Output as 输出fMP4文件
|
||||
|
||||
User->>API: 请求时间范围录像<br>(stream, startTime, endTime)
|
||||
API->>DB: 查询指定范围的录像记录
|
||||
DB-->>API: 返回符合条件的录像列表
|
||||
|
||||
Muxer->>Output: 写入ftyp box
|
||||
Muxer->>Output: 写入初始moov box<br>(包含mvex)
|
||||
|
||||
loop 对每个MP4文件
|
||||
API->>MP4s: 读取文件
|
||||
MP4s->>Muxer: 解析文件结构
|
||||
Muxer->>Muxer: 解析轨道信息
|
||||
Muxer->>Muxer: 提取媒体样本
|
||||
|
||||
loop 对每个样本
|
||||
Note over Muxer: 检查样本是否在目标时间范围内
|
||||
Muxer->>Muxer: 调整时间戳
|
||||
Muxer->>Muxer: 创建moof+mdat对
|
||||
Muxer->>Output: 写入moof+mdat对
|
||||
end
|
||||
end
|
||||
|
||||
Output-->>User: 向用户提供合并后的文件
|
||||
```
|
||||
|
||||
fMP4的合并更加灵活,每个样本都被封装成独立的`moof`+`mdat`片段,保持了可独立解码的特性,更有利于流式传输和随机访问。这种方式特别适合与MSE和HLS结合,为实时流媒体播放提供支持,让用户能够在浏览器中直接高效地播放合并后的内容,而无需等待整个文件下载完成。
|
||||
|
||||
### 合并中的编解码兼容性处理
|
||||
|
||||
在多段录像合并过程中,我们面临的一个关键挑战是处理不同文件可能存在的编码参数差异。例如,在长时间录制过程中,摄像头可能因环境变化调整了视频分辨率,或者编码器可能重新初始化导致编码参数变化。
|
||||
|
||||
为了解决这一问题,Monibuca实现了一个智能的轨道版本管理系统,通过比较编码器特定数据(ExtraData)来识别变化:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Muxer as 合并器
|
||||
participant Track as 轨道管理器
|
||||
participant History as 轨道历史版本
|
||||
|
||||
loop 对每个新轨道
|
||||
Muxer->>Track: 检查轨道编码参数
|
||||
Track->>History: 比较已有轨道版本
|
||||
alt 发现匹配的轨道版本
|
||||
History-->>Track: 返回现有轨道
|
||||
Track-->>Muxer: 使用已有轨道
|
||||
else 无匹配版本
|
||||
Track->>Track: 创建新轨道版本
|
||||
Track->>History: 添加到历史版本库
|
||||
Track-->>Muxer: 使用新轨道
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
这种设计确保了即使原始录像中存在编码参数变化,合并后的文件也能保持正确的解码参数,为用户提供流畅的播放体验。
|
||||
|
||||
### 性能优化
|
||||
|
||||
在处理大型视频文件或大量并发请求时,合并过程的性能是一个重要考量。我们采取了以下优化措施:
|
||||
|
||||
1. **流式处理**:逐帧处理样本,避免将整个文件加载到内存
|
||||
2. **并行处理**:对多个独立任务(如文件解析)采用并行处理
|
||||
3. **智能缓存**:缓存常用的编码参数和文件元数据
|
||||
4. **按需读取**:仅读取和处理目标时间范围内的样本
|
||||
|
||||
这些优化使得系统能够高效处理大规模的录像合并请求,即使是跨越数小时或数天的长时间录像,也能在合理的时间内完成处理。
|
||||
|
||||
多段MP4合并功能极大地增强了Monibuca作为流媒体服务器的灵活性和用户体验,使用户能够精确获取所需的录像内容,无论原始录像如何分段存储。
|
||||
|
||||
## 媒体源扩展(MSE)与fMP4的兼容实现
|
||||
|
||||
### MSE技术概述
|
||||
|
||||
媒体源扩展(Media Source Extensions, MSE)是一种JavaScript API,允许网页开发者直接操作媒体流数据。它使得自定义的自适应比特率流媒体播放器可以完全在浏览器中实现,无需依赖外部插件。
|
||||
|
||||
MSE的核心工作原理是:
|
||||
1. 创建一个MediaSource对象
|
||||
2. 创建一个或多个SourceBuffer对象
|
||||
3. 将媒体片段追加到SourceBuffer中
|
||||
4. 浏览器负责解码和播放这些片段
|
||||
|
||||
### fMP4与MSE的完美适配
|
||||
|
||||
fMP4格式与MSE有着天然的兼容性,主要体现在:
|
||||
|
||||
1. fMP4的每个片段都可以独立解码
|
||||
2. 初始化段和媒体段的清晰分离符合MSE的缓冲区管理模型
|
||||
3. 时间戳的精确控制使得无缝拼接成为可能
|
||||
|
||||
以下时序图展示了fMP4如何与MSE配合工作:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 浏览器客户端
|
||||
participant Server as 服务器
|
||||
participant MSE as MediaSource API
|
||||
participant Video as HTML5 Video元素
|
||||
|
||||
Client->>Video: 创建video元素
|
||||
Client->>MSE: 创建MediaSource对象
|
||||
Client->>Video: 设置video.src = URL.createObjectURL(mediaSource)
|
||||
MSE-->>Client: sourceopen事件
|
||||
|
||||
Client->>MSE: 创建SourceBuffer
|
||||
Client->>Server: 请求初始化段(ftyp+moov)
|
||||
Server-->>Client: 返回初始化段
|
||||
Client->>MSE: appendBuffer(初始化段)
|
||||
|
||||
loop 播放过程
|
||||
Client->>Server: 请求媒体段(moof+mdat)
|
||||
Server-->>Client: 返回媒体段
|
||||
Client->>MSE: appendBuffer(媒体段)
|
||||
MSE-->>Video: 解码并渲染帧
|
||||
end
|
||||
```
|
||||
|
||||
在Monibuca的实现中,我们针对MSE进行了特殊优化:为每一帧创建独立的moof和mdat。这种实现方式尽管会增加一些开销,但提供了极高的灵活性,特别适合于低延迟的实时流媒体场景和精确的帧级操作。
|
||||
|
||||
## HLS与fMP4在实际应用中的集成
|
||||
|
||||
在实际应用中,我们将fMP4技术与HLS v7协议结合,实现了基于时间范围的点播功能。系统可以根据用户指定的时间范围,从数据库中查找对应的MP4记录,然后生成fMP4格式的HLS播放列表:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant Server as HLS服务
|
||||
participant DB as 数据库
|
||||
participant MP4Plugin as MP4插件
|
||||
|
||||
Client->>Server: 请求fMP4.m3u8<br>带时间范围参数
|
||||
Server->>DB: 查询指定时间范围的MP4记录
|
||||
DB-->>Server: 返回记录列表
|
||||
|
||||
Server->>Server: 创建HLS v7播放列表<br>Version: 7
|
||||
loop 对每个记录
|
||||
Server->>Server: 计算时长
|
||||
Server->>Server: 添加媒体片段URL<br>/mp4/download/{stream}.fmp4?id={id}
|
||||
end
|
||||
|
||||
Server->>Server: 添加#EXT-X-ENDLIST标记
|
||||
Server-->>Client: 返回HLS播放列表
|
||||
|
||||
loop 对每个片段
|
||||
Client->>MP4Plugin: 请求fMP4片段
|
||||
MP4Plugin->>MP4Plugin: 转换为fMP4格式
|
||||
MP4Plugin-->>Client: 返回fMP4片段
|
||||
end
|
||||
```
|
||||
|
||||
通过这种方式,我们在保持兼容现有HLS客户端的同时,利用了fMP4格式的优势,提供了更高效的流媒体服务。
|
||||
|
||||
## 结论
|
||||
|
||||
fMP4作为一种现代媒体容器格式,结合了MP4的高效压缩和流媒体传输的灵活性,非常适合现代Web应用中的视频分发需求。通过与HLS v7和MSE技术的结合,可以实现更高效、更灵活的流媒体服务。
|
||||
|
||||
在Monibuca项目的实践中,我们通过实现MP4到fMP4的转换、多段MP4文件的合并,以及针对MSE优化fMP4片段生成,成功构建了一套完整的流媒体解决方案。这些技术的应用使得我们的系统能够提供更好的用户体验,包括更快的启动时间、更平滑的画质切换以及更低的带宽消耗。
|
||||
|
||||
随着视频技术的不断发展,fMP4作为连接传统媒体格式与现代Web技术的桥梁,将继续在流媒体领域发挥重要作用。而Monibuca项目也将持续探索和优化这一技术,为用户提供更优质的流媒体服务。
|
||||
16
example/8080/snap.yaml
Normal file
16
example/8080/snap.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
snap:
|
||||
onpub:
|
||||
transform:
|
||||
.+:
|
||||
output:
|
||||
- watermark:
|
||||
text: "abcd" # 水印文字内容
|
||||
fontpath: /Users/dexter/Library/Fonts/MapleMono-NF-CN-Medium.ttf # 水印字体文件路径
|
||||
fontcolor: "rgba(255,165,0,1)" # 水印字体颜色,支持rgba格式
|
||||
fontsize: 36 # 水印字体大小
|
||||
offsetx: 0 # 水印位置X偏移
|
||||
offsety: 0 # 水印位置Y偏移
|
||||
timeinterval: 1s # 截图时间间隔
|
||||
savepath: "snaps" # 截图保存路径
|
||||
iframeinterval: 3 # 间隔多少帧截图
|
||||
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
||||
12
example/8081/rtsp_server.yaml
Normal file
12
example/8081/rtsp_server.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
global:
|
||||
loglevel: debug
|
||||
tcp: :50052
|
||||
http: :8081
|
||||
disableall: true
|
||||
flv:
|
||||
enable: true
|
||||
pull:
|
||||
live/test: /Users/dexter/Movies/jb-demo.flv
|
||||
rtsp:
|
||||
enable: true
|
||||
tcp: :8554
|
||||
@@ -4,15 +4,14 @@ global:
|
||||
loglevel: debug
|
||||
admin:
|
||||
enablelogin: false
|
||||
# db:
|
||||
# dbtype: mysql
|
||||
# dsn: root:Monibuca#!4@tcp(sh-cynosdbmysql-grp-kxt43lv6.sql.tencentcdb.com:28520)/lkm7s_v5?parseTime=true
|
||||
srt:
|
||||
listenaddr: :6000
|
||||
passphrase: foobarfoobar
|
||||
gb28181:
|
||||
enable: false
|
||||
autoinvite: true
|
||||
autoinvite: false
|
||||
mediaip: 192.168.1.21 #流媒体收流IP
|
||||
sipip: 192.168.1.21 #SIP通讯IP
|
||||
sip:
|
||||
listenaddr:
|
||||
- udp::5060
|
||||
@@ -23,14 +22,16 @@ gb28181:
|
||||
.* : $0
|
||||
mp4:
|
||||
# enable: false
|
||||
publish:
|
||||
delayclosetimeout: 3s
|
||||
# publish:
|
||||
# delayclosetimeout: 3s
|
||||
# onpub:
|
||||
# record:
|
||||
# ^live/.+:
|
||||
# fragment: 10s
|
||||
# filepath: record/$0
|
||||
# type: mp4
|
||||
# type: fmp4
|
||||
# pull:
|
||||
# live/test: /Users/dexter/Movies/1744963190.mp4
|
||||
onsub:
|
||||
pull:
|
||||
^vod_mp4_\d+/(.+)$: $1
|
||||
@@ -70,26 +71,21 @@ hls:
|
||||
|
||||
snap:
|
||||
enable: false
|
||||
ismanualmodesave: true # 手动截图是否保存文件
|
||||
watermark:
|
||||
text: "Monibuca $T{2006-01-02 15:04:05.000}"
|
||||
fontpath: "/System/Library/Fonts/STHeiti Light.ttc" # mac字体路径
|
||||
# fontpath: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # linux字体路径 思源黑体
|
||||
# fontpath: "C:/Windows/Fonts/msyh.ttf" # windows字体路径 微软雅黑
|
||||
fontsize: 16
|
||||
fontspacing: 2 # 添加字体间距配置
|
||||
fontcolor: "rgba(255,165,0,1)"
|
||||
offsetx: 10
|
||||
offsety: 10
|
||||
mode: 2 #截图模式:0-时间间隔,1-关键帧间隔 2-HTTP请求模式(手动触发)
|
||||
timeinterval: 3s
|
||||
savepath: "./snap"
|
||||
iframeinterval: 3 # 截图i帧间隔,默认为3,即每隔3个i帧截图一次
|
||||
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
||||
filter: "^live/.*"
|
||||
onpub:
|
||||
transform:
|
||||
.* : $0
|
||||
.+:
|
||||
output:
|
||||
- watermark:
|
||||
text: "abcd" # 水印文字内容
|
||||
fontpath: /Users/dexter/Library/Fonts/MapleMono-NF-CN-Medium.ttf # 水印字体文件路径
|
||||
fontcolor: "rgba(255,165,0,1)" # 水印字体颜色,支持rgba格式
|
||||
fontsize: 36 # 水印字体大小
|
||||
offsetx: 0 # 水印位置X偏移
|
||||
offsety: 0 # 水印位置Y偏移
|
||||
timeinterval: 1s # 截图时间间隔
|
||||
savepath: "snaps" # 截图保存路径
|
||||
iframeinterval: 3 # 间隔多少帧截图
|
||||
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
||||
|
||||
crypto:
|
||||
enable: false
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
_ "m7s.live/v5/plugin/stress"
|
||||
_ "m7s.live/v5/plugin/transcode"
|
||||
_ "m7s.live/v5/plugin/webrtc"
|
||||
_ "m7s.live/v5/plugin/webtransport"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
46
go.mod
46
go.mod
@@ -1,27 +1,32 @@
|
||||
module m7s.live/v5
|
||||
|
||||
go 1.23
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Eyevinn/mp4ff v0.45.1
|
||||
github.com/IOTechSystems/onvif v1.2.0
|
||||
github.com/VictoriaMetrics/VictoriaMetrics v1.102.0
|
||||
github.com/asavie/xdp v0.3.3
|
||||
github.com/beevik/etree v1.4.1
|
||||
github.com/bluenviron/gohlslib v1.4.0
|
||||
github.com/c0deltin/duckdb-driver v0.1.0
|
||||
github.com/cilium/ebpf v0.15.0
|
||||
github.com/cloudwego/goref v0.0.0-20240724113447-685d2a9523c8
|
||||
github.com/deepch/vdk v0.0.27
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/emiago/sipgo v0.22.0
|
||||
github.com/emiago/sipgo v0.29.0
|
||||
github.com/go-delve/delve v1.23.0
|
||||
github.com/gobwas/ws v1.3.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8
|
||||
github.com/icholy/digest v0.1.22
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/mcuadros/go-defaults v1.2.0
|
||||
github.com/mozillazg/go-pinyin v0.20.0
|
||||
github.com/ncruces/go-sqlite3 v0.18.1
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.18.0
|
||||
github.com/pion/interceptor v0.1.37
|
||||
@@ -30,14 +35,15 @@ require (
|
||||
github.com/pion/rtp v1.8.10
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/pion/webrtc/v4 v4.0.7
|
||||
github.com/quic-go/quic-go v0.43.1
|
||||
github.com/quic-go/quic-go v0.50.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/samber/slog-common v0.17.1
|
||||
github.com/shirou/gopsutil/v4 v4.24.8
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.1.0
|
||||
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7
|
||||
golang.org/x/image v0.22.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/text v0.24.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
@@ -47,15 +53,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/IOTechSystems/onvif v1.2.0 // indirect
|
||||
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
|
||||
github.com/VictoriaMetrics/metrics v1.35.1 // indirect
|
||||
github.com/VictoriaMetrics/metricsql v0.76.0 // indirect
|
||||
github.com/abema/go-mp4 v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/asticode/go-astits v1.13.0 // indirect
|
||||
github.com/beevik/etree v1.4.1 // indirect
|
||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -68,19 +73,16 @@ require (
|
||||
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/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/marcboeker/go-duckdb v1.0.5 // indirect
|
||||
@@ -88,41 +90,33 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mozillazg/go-pinyin v0.20.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/ice/v2 v2.3.9 // indirect
|
||||
github.com/pion/ice/v4 v4.0.3 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.35 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.15 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.2 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/samber/lo v1.44.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.61.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/gozstd v1.21.1 // indirect
|
||||
@@ -132,7 +126,7 @@ require (
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
)
|
||||
|
||||
@@ -149,12 +143,12 @@ require (
|
||||
github.com/phsym/console-slog v0.3.1
|
||||
github.com/prometheus/client_golang v1.20.4
|
||||
github.com/quangngotan95/go-m3u8 v0.1.0
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/sys v0.32.0
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
215
go.sum
215
go.sum
@@ -1,5 +1,3 @@
|
||||
github.com/Eyevinn/mp4ff v0.45.1 h1:Hlx8ZUu8agN7XrHVcZAGIa+dVZ0UW/g/SLv63Pm/+w0=
|
||||
github.com/Eyevinn/mp4ff v0.45.1/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
|
||||
github.com/IOTechSystems/onvif v1.2.0 h1:vplyPdhFhMRtIdkEbQIkTlrKjXpeDj+WUTt5UW61ZcI=
|
||||
github.com/IOTechSystems/onvif v1.2.0/go.mod h1:/dTr5BtFaGojYGJ2rEBIVWh3seGIcSuCJhcK9zwTsk0=
|
||||
github.com/VictoriaMetrics/VictoriaMetrics v1.102.0 h1:eRi6VGT7ntLG/OW8XTWUYhSvA+qGD3FHaRkzdgYHOOw=
|
||||
@@ -19,6 +17,8 @@ github.com/alchemy/rotoslog v0.2.2 h1:yzAOjaQBKgJvAdPi0sF5KSPMq5f2vNJZEnPr73CPDz
|
||||
github.com/alchemy/rotoslog v0.2.2/go.mod h1:pOHF0DKryPLaQzjcUlidLVRTksvk9yW75YIu1yYiiEQ=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/asavie/xdp v0.3.3 h1:b5Aa3EkMJYBeUO5TxPTIAa4wyUqYcsQr2s8f6YLJXhE=
|
||||
github.com/asavie/xdp v0.3.3/go.mod h1:Vv5p+3mZiDh7ImdSvdon3E78wXyre7df5V58ATdIYAY=
|
||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||
@@ -69,11 +69,9 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6 h1:x9TA+vnGEyqmWY+eA9HfgxNRkOQqwiEpFE9IPXSGuEA=
|
||||
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
|
||||
github.com/emiago/sipgo v0.22.0 h1:GaQ51m26M9QnVBVY2aDJ/mXqq/BDfZ1A+nW7XgU/4Ts=
|
||||
github.com/emiago/sipgo v0.22.0/go.mod h1:a77FgPEEjJvfYWYfP3p53u+dNhWEMb/VGVS6guvBzx0=
|
||||
github.com/emiago/sipgo v0.29.0 h1:dg/FwwhSl6hQTiOTIHzcqemZm3tB7jvGQgIlJmuD2Nw=
|
||||
github.com/emiago/sipgo v0.29.0/go.mod h1:ZQ/tl5t+3assyOjiKw/AInPkcawBJ2Or+d5buztOZsc=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
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/go-delve/delve v1.23.0 h1:jYgZISZ14KAO3ys8kD07kjrowrygE9F9SIwnpz9xXys=
|
||||
github.com/go-delve/delve v1.23.0/go.mod h1:S3SLuEE2mn7wipKilTvk1p9HdTMnXXElcEpiZ+VcuqU=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
@@ -84,11 +82,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
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=
|
||||
@@ -100,24 +95,12 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
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.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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -126,7 +109,6 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
@@ -135,7 +117,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo=
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
|
||||
@@ -160,6 +141,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -205,17 +188,8 @@ github.com/ncruces/go-sqlite3/gormlite v0.18.0/go.mod h1:RXeT1hknrz3A0tBDL6IfluD
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
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.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
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.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
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/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
|
||||
@@ -224,84 +198,36 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/phsym/console-slog v0.3.1 h1:Fuzcrjr40xTc004S9Kni8XfNsk+qrptQmyR+wZw9/7A=
|
||||
github.com/phsym/console-slog v0.3.1/go.mod h1:oJskjp/X6e6c0mGpfP8ELkfKUsrkDifYRAqJQgmdDS0=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
||||
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
|
||||
github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
|
||||
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
|
||||
github.com/pion/ice/v4 v4.0.3 h1:9s5rI1WKzF5DRqhJ+Id8bls/8PzM7mau0mj1WZb4IXE=
|
||||
github.com/pion/ice/v4 v4.0.3/go.mod h1:VfHy0beAZ5loDT7BmJ2LtMtC4dbawIkkkejHPRZNB3Y=
|
||||
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
|
||||
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
|
||||
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
|
||||
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp v1.5.2 h1:25DmvH+fqKZDqvX64vTwnycVwL9ooJxHF/gkX16bDBY=
|
||||
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
|
||||
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
||||
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
|
||||
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
|
||||
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
|
||||
github.com/pion/webrtc/v4 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
|
||||
github.com/pion/webrtc/v4 v4.0.7 h1:aeq78uVnFZd2umXW0O9A2VFQYuS7+BZxWetQvSp2jPo=
|
||||
github.com/pion/webrtc/v4 v4.0.7/go.mod h1:oFVBBVSHU3vAEwSgnk3BuKCwAUwpDwQhko1EDwyZWbU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -322,8 +248,12 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/quangngotan95/go-m3u8 v0.1.0 h1:8oseBjJn5IKHQKdRZwSNskkua3NLrRtlvXXtoVgBzMk=
|
||||
github.com/quangngotan95/go-m3u8 v0.1.0/go.mod h1:smzfWHlYpBATVNu1GapKLYiCtEo5JxridIgvvudZ+Wc=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ=
|
||||
github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
|
||||
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@@ -339,7 +269,6 @@ github.com/samber/slog-multi v1.0.0 h1:snvP/P5GLQ8TQh5WSqdRaxDANW8AAA3egwEoytLsq
|
||||
github.com/samber/slog-multi v1.0.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
|
||||
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/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
|
||||
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -355,16 +284,11 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -377,6 +301,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
|
||||
github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
@@ -393,32 +319,23 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7Zo
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7 h1:e9n2WNcfvs20aLgpDhKoaJgrU/EeAvuNnWLBm31Q5Fw=
|
||||
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
|
||||
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -426,159 +343,77 @@ golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
|
||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/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-20200323222414-85ca7c5b95cd/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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
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/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
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.2.8/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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
276
pb/auth.pb.go
276
pb/auth.pb.go
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.19.1
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.28.3
|
||||
// source: auth.proto
|
||||
|
||||
package pb
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,21 +23,18 @@ const (
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LoginRequest) Reset() {
|
||||
*x = LoginRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LoginRequest) String() string {
|
||||
@@ -47,7 +45,7 @@ func (*LoginRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -77,21 +75,18 @@ func (x *LoginRequest) GetPassword() string {
|
||||
}
|
||||
|
||||
type LoginSuccess struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
UserInfo *UserInfo `protobuf:"bytes,2,opt,name=userInfo,proto3" json:"userInfo,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
UserInfo *UserInfo `protobuf:"bytes,2,opt,name=userInfo,proto3" json:"userInfo,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LoginSuccess) Reset() {
|
||||
*x = LoginSuccess{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LoginSuccess) String() string {
|
||||
@@ -102,7 +97,7 @@ func (*LoginSuccess) ProtoMessage() {}
|
||||
|
||||
func (x *LoginSuccess) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -132,22 +127,19 @@ func (x *LoginSuccess) GetUserInfo() *UserInfo {
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data *LoginSuccess `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data *LoginSuccess `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LoginResponse) Reset() {
|
||||
*x = LoginResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LoginResponse) String() string {
|
||||
@@ -158,7 +150,7 @@ func (*LoginResponse) ProtoMessage() {}
|
||||
|
||||
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -195,20 +187,17 @@ func (x *LoginResponse) GetData() *LoginSuccess {
|
||||
}
|
||||
|
||||
type LogoutRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogoutRequest) Reset() {
|
||||
*x = LogoutRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutRequest) String() string {
|
||||
@@ -219,7 +208,7 @@ func (*LogoutRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -242,21 +231,18 @@ func (x *LogoutRequest) GetToken() string {
|
||||
}
|
||||
|
||||
type LogoutResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogoutResponse) Reset() {
|
||||
*x = LogoutResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutResponse) String() string {
|
||||
@@ -267,7 +253,7 @@ func (*LogoutResponse) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -297,20 +283,17 @@ func (x *LogoutResponse) GetMessage() string {
|
||||
}
|
||||
|
||||
type UserInfoRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserInfoRequest) Reset() {
|
||||
*x = UserInfoRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UserInfoRequest) String() string {
|
||||
@@ -321,7 +304,7 @@ func (*UserInfoRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UserInfoRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -344,21 +327,18 @@ func (x *UserInfoRequest) GetToken() string {
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
ExpiresAt int64 `protobuf:"varint,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Token expiration timestamp
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
ExpiresAt int64 `protobuf:"varint,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Token expiration timestamp
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserInfo) Reset() {
|
||||
*x = UserInfo{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UserInfo) String() string {
|
||||
@@ -369,7 +349,7 @@ func (*UserInfo) ProtoMessage() {}
|
||||
|
||||
func (x *UserInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[6]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -399,22 +379,19 @@ func (x *UserInfo) GetExpiresAt() int64 {
|
||||
}
|
||||
|
||||
type UserInfoResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data *UserInfo `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data *UserInfo `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserInfoResponse) Reset() {
|
||||
*x = UserInfoResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UserInfoResponse) String() string {
|
||||
@@ -425,7 +402,7 @@ func (*UserInfoResponse) ProtoMessage() {}
|
||||
|
||||
func (x *UserInfoResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[7]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -463,7 +440,7 @@ func (x *UserInfoResponse) GetData() *UserInfo {
|
||||
|
||||
var File_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_auth_proto_rawDesc = []byte{
|
||||
var file_auth_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62,
|
||||
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
|
||||
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46,
|
||||
@@ -506,13 +483,13 @@ var file_auth_proto_rawDesc = []byte{
|
||||
0x48, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70, 0x62, 0x2e,
|
||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82,
|
||||
0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68,
|
||||
0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x3a, 0x01, 0x2a, 0x12, 0x4c, 0x0a, 0x06, 0x4c, 0x6f, 0x67,
|
||||
0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61,
|
||||
0x75, 0x74, 0x68, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x4c, 0x0a, 0x06, 0x4c, 0x6f, 0x67,
|
||||
0x6f, 0x75, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x6f,
|
||||
0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93,
|
||||
0x02, 0x15, 0x22, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6c, 0x6f,
|
||||
0x67, 0x6f, 0x75, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x54, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73,
|
||||
0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68,
|
||||
0x2f, 0x6c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x54, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73,
|
||||
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72,
|
||||
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x62,
|
||||
0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
@@ -520,22 +497,22 @@ var file_auth_proto_rawDesc = []byte{
|
||||
0x61, 0x75, 0x74, 0x68, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x42, 0x10, 0x5a,
|
||||
0x0e, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x62, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
})
|
||||
|
||||
var (
|
||||
file_auth_proto_rawDescOnce sync.Once
|
||||
file_auth_proto_rawDescData = file_auth_proto_rawDesc
|
||||
file_auth_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_auth_proto_rawDescGZIP() []byte {
|
||||
file_auth_proto_rawDescOnce.Do(func() {
|
||||
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_proto_rawDescData)
|
||||
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)))
|
||||
})
|
||||
return file_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_auth_proto_goTypes = []interface{}{
|
||||
var file_auth_proto_goTypes = []any{
|
||||
(*LoginRequest)(nil), // 0: pb.LoginRequest
|
||||
(*LoginSuccess)(nil), // 1: pb.LoginSuccess
|
||||
(*LoginResponse)(nil), // 2: pb.LoginResponse
|
||||
@@ -567,109 +544,11 @@ func file_auth_proto_init() {
|
||||
if File_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*LoginRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*LoginSuccess); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*LoginResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*LogoutRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*LogoutResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*UserInfoRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*UserInfo); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_auth_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*UserInfoResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_auth_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
@@ -680,7 +559,6 @@ func file_auth_proto_init() {
|
||||
MessageInfos: file_auth_proto_msgTypes,
|
||||
}.Build()
|
||||
File_auth_proto = out.File
|
||||
file_auth_proto_rawDesc = nil
|
||||
file_auth_proto_goTypes = nil
|
||||
file_auth_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ func local_request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Mar
|
||||
// UnaryRPC :call AuthServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthHandlerFromEndpoint instead.
|
||||
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
||||
func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServer) error {
|
||||
|
||||
mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
@@ -206,21 +207,21 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
|
||||
// RegisterAuthHandlerFromEndpoint is same as RegisterAuthHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterAuthHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.DialContext(ctx, endpoint, opts...)
|
||||
conn, err := grpc.NewClient(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
@@ -238,7 +239,7 @@ func RegisterAuthHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.
|
||||
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "AuthClient" to call the correct interceptors.
|
||||
// "AuthClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
||||
func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthClient) error {
|
||||
|
||||
mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.1
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: auth.proto
|
||||
|
||||
package pb
|
||||
@@ -15,8 +15,14 @@ import (
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Auth_Login_FullMethodName = "/pb.Auth/Login"
|
||||
Auth_Logout_FullMethodName = "/pb.Auth/Logout"
|
||||
Auth_GetUserInfo_FullMethodName = "/pb.Auth/GetUserInfo"
|
||||
)
|
||||
|
||||
// AuthClient is the client API for Auth service.
|
||||
//
|
||||
@@ -36,8 +42,9 @@ func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
|
||||
}
|
||||
|
||||
func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(LoginResponse)
|
||||
err := c.cc.Invoke(ctx, "/pb.Auth/Login", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -45,8 +52,9 @@ func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.C
|
||||
}
|
||||
|
||||
func (c *authClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(LogoutResponse)
|
||||
err := c.cc.Invoke(ctx, "/pb.Auth/Logout", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Auth_Logout_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,8 +62,9 @@ func (c *authClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *authClient) GetUserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UserInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/pb.Auth/GetUserInfo", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Auth_GetUserInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,7 +73,7 @@ func (c *authClient) GetUserInfo(ctx context.Context, in *UserInfoRequest, opts
|
||||
|
||||
// AuthServer is the server API for Auth service.
|
||||
// All implementations must embed UnimplementedAuthServer
|
||||
// for forward compatibility
|
||||
// for forward compatibility.
|
||||
type AuthServer interface {
|
||||
Login(context.Context, *LoginRequest) (*LoginResponse, error)
|
||||
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
|
||||
@@ -72,9 +81,12 @@ type AuthServer interface {
|
||||
mustEmbedUnimplementedAuthServer()
|
||||
}
|
||||
|
||||
// UnimplementedAuthServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedAuthServer struct {
|
||||
}
|
||||
// UnimplementedAuthServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAuthServer struct{}
|
||||
|
||||
func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||
@@ -86,6 +98,7 @@ func (UnimplementedAuthServer) GetUserInfo(context.Context, *UserInfoRequest) (*
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserInfo not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
|
||||
func (UnimplementedAuthServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AuthServer will
|
||||
@@ -95,6 +108,13 @@ type UnsafeAuthServer interface {
|
||||
}
|
||||
|
||||
func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAuthServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Auth_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
@@ -108,7 +128,7 @@ func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/pb.Auth/Login",
|
||||
FullMethod: Auth_Login_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServer).Login(ctx, req.(*LoginRequest))
|
||||
@@ -126,7 +146,7 @@ func _Auth_Logout_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/pb.Auth/Logout",
|
||||
FullMethod: Auth_Logout_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServer).Logout(ctx, req.(*LogoutRequest))
|
||||
@@ -144,7 +164,7 @@ func _Auth_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/pb.Auth/GetUserInfo",
|
||||
FullMethod: Auth_GetUserInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServer).GetUserInfo(ctx, req.(*UserInfoRequest))
|
||||
|
||||
2971
pb/global.pb.go
2971
pb/global.pb.go
File diff suppressed because it is too large
Load Diff
@@ -1192,66 +1192,6 @@ func local_request_Api_GetFormily_0(ctx context.Context, marshaler runtime.Marsh
|
||||
|
||||
}
|
||||
|
||||
func request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ModifyConfigRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["name"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
||||
}
|
||||
|
||||
protoReq.Name, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
||||
}
|
||||
|
||||
msg, err := client.ModifyConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ModifyConfigRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["name"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
||||
}
|
||||
|
||||
protoReq.Name, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
||||
}
|
||||
|
||||
msg, err := server.ModifyConfig(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Api_GetPullProxyList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq emptypb.Empty
|
||||
var metadata runtime.ServerMetadata
|
||||
@@ -1904,6 +1844,7 @@ func local_request_Api_DeleteRecord_0(ctx context.Context, marshaler runtime.Mar
|
||||
// UnaryRPC :call ApiServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
|
||||
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
||||
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
|
||||
|
||||
mux.Handle("GET", pattern_Api_SysInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
@@ -2581,31 +2522,6 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Api_ModifyConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_ModifyConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -2739,7 +2655,7 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -3037,21 +2953,21 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.DialContext(ctx, endpoint, opts...)
|
||||
conn, err := grpc.NewClient(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
@@ -3069,7 +2985,7 @@ func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.C
|
||||
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "ApiClient" to call the correct interceptors.
|
||||
// "ApiClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
||||
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_Api_SysInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
@@ -3666,28 +3582,6 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Api_ModifyConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_ModifyConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -3804,7 +3698,7 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -4120,8 +4014,6 @@ var (
|
||||
|
||||
pattern_Api_GetFormily_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "formily", "name"}, ""))
|
||||
|
||||
pattern_Api_ModifyConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "modify", "name"}, ""))
|
||||
|
||||
pattern_Api_GetPullProxyList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "list"}, ""))
|
||||
|
||||
pattern_Api_GetPullProxyList_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "device", "list"}, ""))
|
||||
@@ -4132,7 +4024,7 @@ var (
|
||||
|
||||
pattern_Api_RemovePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "proxy", "pull", "remove", "id"}, ""))
|
||||
|
||||
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "add", "id"}, ""))
|
||||
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "remove", "id"}, ""))
|
||||
|
||||
pattern_Api_UpdatePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "update"}, ""))
|
||||
|
||||
@@ -4212,8 +4104,6 @@ var (
|
||||
|
||||
forward_Api_GetFormily_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_ModifyConfig_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_GetPullProxyList_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_GetPullProxyList_1 = runtime.ForwardResponseMessage
|
||||
|
||||
@@ -152,12 +152,7 @@ service api {
|
||||
get: "/api/config/formily/{name}"
|
||||
};
|
||||
}
|
||||
rpc ModifyConfig (ModifyConfigRequest) returns (SuccessResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/config/modify/{name}"
|
||||
body: "yaml"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetPullProxyList (google.protobuf.Empty) returns (PullProxyListResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/proxy/pull/list"
|
||||
@@ -181,7 +176,7 @@ service api {
|
||||
post: "/api/proxy/pull/remove/{id}"
|
||||
body: "*"
|
||||
additional_bindings {
|
||||
post: "/api/device/add/{id}"
|
||||
post: "/api/device/remove/{id}"
|
||||
body: "*"
|
||||
}
|
||||
};
|
||||
@@ -671,6 +666,7 @@ message ReqRecordList {
|
||||
uint32 pageSize = 6;
|
||||
string mode = 7;
|
||||
string type = 8;
|
||||
string eventLevel = 9;
|
||||
}
|
||||
|
||||
message RecordFile {
|
||||
@@ -679,6 +675,9 @@ message RecordFile {
|
||||
string streamPath = 3;
|
||||
google.protobuf.Timestamp startTime = 4;
|
||||
google.protobuf.Timestamp endTime = 5;
|
||||
string eventLevel = 6;
|
||||
string eventName = 7;
|
||||
string eventDesc = 8;
|
||||
}
|
||||
|
||||
message ResponseList {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.1
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: global.proto
|
||||
|
||||
package pb
|
||||
@@ -16,8 +16,51 @@ import (
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Api_SysInfo_FullMethodName = "/global.api/SysInfo"
|
||||
Api_DisabledPlugins_FullMethodName = "/global.api/DisabledPlugins"
|
||||
Api_Summary_FullMethodName = "/global.api/Summary"
|
||||
Api_Shutdown_FullMethodName = "/global.api/Shutdown"
|
||||
Api_Restart_FullMethodName = "/global.api/Restart"
|
||||
Api_TaskTree_FullMethodName = "/global.api/TaskTree"
|
||||
Api_StopTask_FullMethodName = "/global.api/StopTask"
|
||||
Api_RestartTask_FullMethodName = "/global.api/RestartTask"
|
||||
Api_StreamList_FullMethodName = "/global.api/StreamList"
|
||||
Api_WaitList_FullMethodName = "/global.api/WaitList"
|
||||
Api_StreamInfo_FullMethodName = "/global.api/StreamInfo"
|
||||
Api_PauseStream_FullMethodName = "/global.api/PauseStream"
|
||||
Api_ResumeStream_FullMethodName = "/global.api/ResumeStream"
|
||||
Api_SetStreamSpeed_FullMethodName = "/global.api/SetStreamSpeed"
|
||||
Api_SeekStream_FullMethodName = "/global.api/SeekStream"
|
||||
Api_GetSubscribers_FullMethodName = "/global.api/GetSubscribers"
|
||||
Api_AudioTrackSnap_FullMethodName = "/global.api/AudioTrackSnap"
|
||||
Api_VideoTrackSnap_FullMethodName = "/global.api/VideoTrackSnap"
|
||||
Api_ChangeSubscribe_FullMethodName = "/global.api/ChangeSubscribe"
|
||||
Api_GetStreamAlias_FullMethodName = "/global.api/GetStreamAlias"
|
||||
Api_SetStreamAlias_FullMethodName = "/global.api/SetStreamAlias"
|
||||
Api_StopPublish_FullMethodName = "/global.api/StopPublish"
|
||||
Api_StopSubscribe_FullMethodName = "/global.api/StopSubscribe"
|
||||
Api_GetConfigFile_FullMethodName = "/global.api/GetConfigFile"
|
||||
Api_UpdateConfigFile_FullMethodName = "/global.api/UpdateConfigFile"
|
||||
Api_GetConfig_FullMethodName = "/global.api/GetConfig"
|
||||
Api_GetFormily_FullMethodName = "/global.api/GetFormily"
|
||||
Api_GetPullProxyList_FullMethodName = "/global.api/GetPullProxyList"
|
||||
Api_AddPullProxy_FullMethodName = "/global.api/AddPullProxy"
|
||||
Api_RemovePullProxy_FullMethodName = "/global.api/RemovePullProxy"
|
||||
Api_UpdatePullProxy_FullMethodName = "/global.api/UpdatePullProxy"
|
||||
Api_GetPushProxyList_FullMethodName = "/global.api/GetPushProxyList"
|
||||
Api_AddPushProxy_FullMethodName = "/global.api/AddPushProxy"
|
||||
Api_RemovePushProxy_FullMethodName = "/global.api/RemovePushProxy"
|
||||
Api_UpdatePushProxy_FullMethodName = "/global.api/UpdatePushProxy"
|
||||
Api_GetRecording_FullMethodName = "/global.api/GetRecording"
|
||||
Api_GetTransformList_FullMethodName = "/global.api/GetTransformList"
|
||||
Api_GetRecordList_FullMethodName = "/global.api/GetRecordList"
|
||||
Api_GetRecordCatalog_FullMethodName = "/global.api/GetRecordCatalog"
|
||||
Api_DeleteRecord_FullMethodName = "/global.api/DeleteRecord"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
@@ -50,7 +93,6 @@ type ApiClient interface {
|
||||
UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRequest, opts ...grpc.CallOption) (*SuccessResponse, error)
|
||||
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
||||
GetFormily(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
||||
ModifyConfig(ctx context.Context, in *ModifyConfigRequest, opts ...grpc.CallOption) (*SuccessResponse, error)
|
||||
GetPullProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PullProxyListResponse, error)
|
||||
AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error)
|
||||
RemovePullProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error)
|
||||
@@ -75,8 +117,9 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
|
||||
}
|
||||
|
||||
func (c *apiClient) SysInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SysInfoResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SysInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/SysInfo", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_SysInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -84,8 +127,9 @@ func (c *apiClient) SysInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *apiClient) DisabledPlugins(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DisabledPluginsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DisabledPluginsResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/DisabledPlugins", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_DisabledPlugins_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,8 +137,9 @@ func (c *apiClient) DisabledPlugins(ctx context.Context, in *emptypb.Empty, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) Summary(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SummaryResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/Summary", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Summary_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,8 +147,9 @@ func (c *apiClient) Summary(ctx context.Context, in *emptypb.Empty, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *apiClient) Shutdown(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/Shutdown", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Shutdown_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,8 +157,9 @@ func (c *apiClient) Shutdown(ctx context.Context, in *RequestWithId, opts ...grp
|
||||
}
|
||||
|
||||
func (c *apiClient) Restart(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/Restart", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Restart_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,8 +167,9 @@ func (c *apiClient) Restart(ctx context.Context, in *RequestWithId, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *apiClient) TaskTree(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TaskTreeResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TaskTreeResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/TaskTree", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_TaskTree_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -129,8 +177,9 @@ func (c *apiClient) TaskTree(ctx context.Context, in *emptypb.Empty, opts ...grp
|
||||
}
|
||||
|
||||
func (c *apiClient) StopTask(ctx context.Context, in *RequestWithId64, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/StopTask", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StopTask_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,8 +187,9 @@ func (c *apiClient) StopTask(ctx context.Context, in *RequestWithId64, opts ...g
|
||||
}
|
||||
|
||||
func (c *apiClient) RestartTask(ctx context.Context, in *RequestWithId64, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/RestartTask", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_RestartTask_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,8 +197,9 @@ func (c *apiClient) RestartTask(ctx context.Context, in *RequestWithId64, opts .
|
||||
}
|
||||
|
||||
func (c *apiClient) StreamList(ctx context.Context, in *StreamListRequest, opts ...grpc.CallOption) (*StreamListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StreamListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/StreamList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StreamList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -156,8 +207,9 @@ func (c *apiClient) StreamList(ctx context.Context, in *StreamListRequest, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) WaitList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StreamWaitListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StreamWaitListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/WaitList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_WaitList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -165,8 +217,9 @@ func (c *apiClient) WaitList(ctx context.Context, in *emptypb.Empty, opts ...grp
|
||||
}
|
||||
|
||||
func (c *apiClient) StreamInfo(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*StreamInfoResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StreamInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/StreamInfo", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StreamInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -174,8 +227,9 @@ func (c *apiClient) StreamInfo(ctx context.Context, in *StreamSnapRequest, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) PauseStream(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/PauseStream", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_PauseStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -183,8 +237,9 @@ func (c *apiClient) PauseStream(ctx context.Context, in *StreamSnapRequest, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) ResumeStream(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/ResumeStream", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_ResumeStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -192,8 +247,9 @@ func (c *apiClient) ResumeStream(ctx context.Context, in *StreamSnapRequest, opt
|
||||
}
|
||||
|
||||
func (c *apiClient) SetStreamSpeed(ctx context.Context, in *SetStreamSpeedRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/SetStreamSpeed", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_SetStreamSpeed_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -201,8 +257,9 @@ func (c *apiClient) SetStreamSpeed(ctx context.Context, in *SetStreamSpeedReques
|
||||
}
|
||||
|
||||
func (c *apiClient) SeekStream(ctx context.Context, in *SeekStreamRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/SeekStream", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_SeekStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -210,8 +267,9 @@ func (c *apiClient) SeekStream(ctx context.Context, in *SeekStreamRequest, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) GetSubscribers(ctx context.Context, in *SubscribersRequest, opts ...grpc.CallOption) (*SubscribersResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SubscribersResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetSubscribers", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetSubscribers_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -219,8 +277,9 @@ func (c *apiClient) GetSubscribers(ctx context.Context, in *SubscribersRequest,
|
||||
}
|
||||
|
||||
func (c *apiClient) AudioTrackSnap(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*TrackSnapShotResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TrackSnapShotResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/AudioTrackSnap", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_AudioTrackSnap_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -228,8 +287,9 @@ func (c *apiClient) AudioTrackSnap(ctx context.Context, in *StreamSnapRequest, o
|
||||
}
|
||||
|
||||
func (c *apiClient) VideoTrackSnap(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*TrackSnapShotResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TrackSnapShotResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/VideoTrackSnap", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_VideoTrackSnap_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,8 +297,9 @@ func (c *apiClient) VideoTrackSnap(ctx context.Context, in *StreamSnapRequest, o
|
||||
}
|
||||
|
||||
func (c *apiClient) ChangeSubscribe(ctx context.Context, in *ChangeSubscribeRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/ChangeSubscribe", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_ChangeSubscribe_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -246,8 +307,9 @@ func (c *apiClient) ChangeSubscribe(ctx context.Context, in *ChangeSubscribeRequ
|
||||
}
|
||||
|
||||
func (c *apiClient) GetStreamAlias(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StreamAliasListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StreamAliasListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetStreamAlias", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetStreamAlias_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -255,8 +317,9 @@ func (c *apiClient) GetStreamAlias(ctx context.Context, in *emptypb.Empty, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) SetStreamAlias(ctx context.Context, in *SetStreamAliasRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/SetStreamAlias", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_SetStreamAlias_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -264,8 +327,9 @@ func (c *apiClient) SetStreamAlias(ctx context.Context, in *SetStreamAliasReques
|
||||
}
|
||||
|
||||
func (c *apiClient) StopPublish(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/StopPublish", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StopPublish_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -273,8 +337,9 @@ func (c *apiClient) StopPublish(ctx context.Context, in *StreamSnapRequest, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) StopSubscribe(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/StopSubscribe", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StopSubscribe_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -282,8 +347,9 @@ func (c *apiClient) StopSubscribe(ctx context.Context, in *RequestWithId, opts .
|
||||
}
|
||||
|
||||
func (c *apiClient) GetConfigFile(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetConfigFileResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetConfigFileResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetConfigFile", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetConfigFile_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -291,8 +357,9 @@ func (c *apiClient) GetConfigFile(ctx context.Context, in *emptypb.Empty, opts .
|
||||
}
|
||||
|
||||
func (c *apiClient) UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/UpdateConfigFile", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_UpdateConfigFile_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -300,8 +367,9 @@ func (c *apiClient) UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRe
|
||||
}
|
||||
|
||||
func (c *apiClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetConfigResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetConfig", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,17 +377,9 @@ func (c *apiClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ..
|
||||
}
|
||||
|
||||
func (c *apiClient) GetFormily(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetConfigResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetFormily", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) ModifyConfig(ctx context.Context, in *ModifyConfigRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/ModifyConfig", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetFormily_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -327,8 +387,9 @@ func (c *apiClient) ModifyConfig(ctx context.Context, in *ModifyConfigRequest, o
|
||||
}
|
||||
|
||||
func (c *apiClient) GetPullProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PullProxyListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PullProxyListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetPullProxyList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetPullProxyList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -336,8 +397,9 @@ func (c *apiClient) GetPullProxyList(ctx context.Context, in *emptypb.Empty, opt
|
||||
}
|
||||
|
||||
func (c *apiClient) AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/AddPullProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_AddPullProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -345,8 +407,9 @@ func (c *apiClient) AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ..
|
||||
}
|
||||
|
||||
func (c *apiClient) RemovePullProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/RemovePullProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_RemovePullProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -354,8 +417,9 @@ func (c *apiClient) RemovePullProxy(ctx context.Context, in *RequestWithId, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) UpdatePullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/UpdatePullProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_UpdatePullProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -363,8 +427,9 @@ func (c *apiClient) UpdatePullProxy(ctx context.Context, in *PullProxyInfo, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) GetPushProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PushProxyListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PushProxyListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetPushProxyList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetPushProxyList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -372,8 +437,9 @@ func (c *apiClient) GetPushProxyList(ctx context.Context, in *emptypb.Empty, opt
|
||||
}
|
||||
|
||||
func (c *apiClient) AddPushProxy(ctx context.Context, in *PushProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/AddPushProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_AddPushProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -381,8 +447,9 @@ func (c *apiClient) AddPushProxy(ctx context.Context, in *PushProxyInfo, opts ..
|
||||
}
|
||||
|
||||
func (c *apiClient) RemovePushProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/RemovePushProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_RemovePushProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -390,8 +457,9 @@ func (c *apiClient) RemovePushProxy(ctx context.Context, in *RequestWithId, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) UpdatePushProxy(ctx context.Context, in *PushProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SuccessResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/UpdatePushProxy", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_UpdatePushProxy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -399,8 +467,9 @@ func (c *apiClient) UpdatePushProxy(ctx context.Context, in *PushProxyInfo, opts
|
||||
}
|
||||
|
||||
func (c *apiClient) GetRecording(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*RecordingListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RecordingListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetRecording", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetRecording_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -408,8 +477,9 @@ func (c *apiClient) GetRecording(ctx context.Context, in *emptypb.Empty, opts ..
|
||||
}
|
||||
|
||||
func (c *apiClient) GetTransformList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TransformListResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TransformListResponse)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetTransformList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetTransformList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -417,8 +487,9 @@ func (c *apiClient) GetTransformList(ctx context.Context, in *emptypb.Empty, opt
|
||||
}
|
||||
|
||||
func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*ResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseList)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetRecordList", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetRecordList_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -426,8 +497,9 @@ func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts .
|
||||
}
|
||||
|
||||
func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseCatalog)
|
||||
err := c.cc.Invoke(ctx, "/global.api/GetRecordCatalog", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_GetRecordCatalog_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -435,8 +507,9 @@ func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog,
|
||||
}
|
||||
|
||||
func (c *apiClient) DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseDelete)
|
||||
err := c.cc.Invoke(ctx, "/global.api/DeleteRecord", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_DeleteRecord_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -445,7 +518,7 @@ func (c *apiClient) DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
// All implementations must embed UnimplementedApiServer
|
||||
// for forward compatibility
|
||||
// for forward compatibility.
|
||||
type ApiServer interface {
|
||||
SysInfo(context.Context, *emptypb.Empty) (*SysInfoResponse, error)
|
||||
DisabledPlugins(context.Context, *emptypb.Empty) (*DisabledPluginsResponse, error)
|
||||
@@ -474,7 +547,6 @@ type ApiServer interface {
|
||||
UpdateConfigFile(context.Context, *UpdateConfigFileRequest) (*SuccessResponse, error)
|
||||
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
||||
GetFormily(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
||||
ModifyConfig(context.Context, *ModifyConfigRequest) (*SuccessResponse, error)
|
||||
GetPullProxyList(context.Context, *emptypb.Empty) (*PullProxyListResponse, error)
|
||||
AddPullProxy(context.Context, *PullProxyInfo) (*SuccessResponse, error)
|
||||
RemovePullProxy(context.Context, *RequestWithId) (*SuccessResponse, error)
|
||||
@@ -491,9 +563,12 @@ type ApiServer interface {
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
// UnimplementedApiServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedApiServer struct {
|
||||
}
|
||||
// UnimplementedApiServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedApiServer struct{}
|
||||
|
||||
func (UnimplementedApiServer) SysInfo(context.Context, *emptypb.Empty) (*SysInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SysInfo not implemented")
|
||||
@@ -576,9 +651,6 @@ func (UnimplementedApiServer) GetConfig(context.Context, *GetConfigRequest) (*Ge
|
||||
func (UnimplementedApiServer) GetFormily(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetFormily not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) ModifyConfig(context.Context, *ModifyConfigRequest) (*SuccessResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ModifyConfig not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) GetPullProxyList(context.Context, *emptypb.Empty) (*PullProxyListResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPullProxyList not implemented")
|
||||
}
|
||||
@@ -619,6 +691,7 @@ func (UnimplementedApiServer) DeleteRecord(context.Context, *ReqRecordDelete) (*
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteRecord not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
|
||||
func (UnimplementedApiServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ApiServer will
|
||||
@@ -628,6 +701,13 @@ type UnsafeApiServer interface {
|
||||
}
|
||||
|
||||
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
|
||||
// If the following call pancis, it indicates UnimplementedApiServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Api_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
@@ -641,7 +721,7 @@ func _Api_SysInfo_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/SysInfo",
|
||||
FullMethod: Api_SysInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).SysInfo(ctx, req.(*emptypb.Empty))
|
||||
@@ -659,7 +739,7 @@ func _Api_DisabledPlugins_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/DisabledPlugins",
|
||||
FullMethod: Api_DisabledPlugins_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).DisabledPlugins(ctx, req.(*emptypb.Empty))
|
||||
@@ -677,7 +757,7 @@ func _Api_Summary_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/Summary",
|
||||
FullMethod: Api_Summary_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Summary(ctx, req.(*emptypb.Empty))
|
||||
@@ -695,7 +775,7 @@ func _Api_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interf
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/Shutdown",
|
||||
FullMethod: Api_Shutdown_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Shutdown(ctx, req.(*RequestWithId))
|
||||
@@ -713,7 +793,7 @@ func _Api_Restart_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/Restart",
|
||||
FullMethod: Api_Restart_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Restart(ctx, req.(*RequestWithId))
|
||||
@@ -731,7 +811,7 @@ func _Api_TaskTree_Handler(srv interface{}, ctx context.Context, dec func(interf
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/TaskTree",
|
||||
FullMethod: Api_TaskTree_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).TaskTree(ctx, req.(*emptypb.Empty))
|
||||
@@ -749,7 +829,7 @@ func _Api_StopTask_Handler(srv interface{}, ctx context.Context, dec func(interf
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/StopTask",
|
||||
FullMethod: Api_StopTask_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StopTask(ctx, req.(*RequestWithId64))
|
||||
@@ -767,7 +847,7 @@ func _Api_RestartTask_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/RestartTask",
|
||||
FullMethod: Api_RestartTask_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).RestartTask(ctx, req.(*RequestWithId64))
|
||||
@@ -785,7 +865,7 @@ func _Api_StreamList_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/StreamList",
|
||||
FullMethod: Api_StreamList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StreamList(ctx, req.(*StreamListRequest))
|
||||
@@ -803,7 +883,7 @@ func _Api_WaitList_Handler(srv interface{}, ctx context.Context, dec func(interf
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/WaitList",
|
||||
FullMethod: Api_WaitList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).WaitList(ctx, req.(*emptypb.Empty))
|
||||
@@ -821,7 +901,7 @@ func _Api_StreamInfo_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/StreamInfo",
|
||||
FullMethod: Api_StreamInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StreamInfo(ctx, req.(*StreamSnapRequest))
|
||||
@@ -839,7 +919,7 @@ func _Api_PauseStream_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/PauseStream",
|
||||
FullMethod: Api_PauseStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).PauseStream(ctx, req.(*StreamSnapRequest))
|
||||
@@ -857,7 +937,7 @@ func _Api_ResumeStream_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/ResumeStream",
|
||||
FullMethod: Api_ResumeStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ResumeStream(ctx, req.(*StreamSnapRequest))
|
||||
@@ -875,7 +955,7 @@ func _Api_SetStreamSpeed_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/SetStreamSpeed",
|
||||
FullMethod: Api_SetStreamSpeed_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).SetStreamSpeed(ctx, req.(*SetStreamSpeedRequest))
|
||||
@@ -893,7 +973,7 @@ func _Api_SeekStream_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/SeekStream",
|
||||
FullMethod: Api_SeekStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).SeekStream(ctx, req.(*SeekStreamRequest))
|
||||
@@ -911,7 +991,7 @@ func _Api_GetSubscribers_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetSubscribers",
|
||||
FullMethod: Api_GetSubscribers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetSubscribers(ctx, req.(*SubscribersRequest))
|
||||
@@ -929,7 +1009,7 @@ func _Api_AudioTrackSnap_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/AudioTrackSnap",
|
||||
FullMethod: Api_AudioTrackSnap_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).AudioTrackSnap(ctx, req.(*StreamSnapRequest))
|
||||
@@ -947,7 +1027,7 @@ func _Api_VideoTrackSnap_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/VideoTrackSnap",
|
||||
FullMethod: Api_VideoTrackSnap_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).VideoTrackSnap(ctx, req.(*StreamSnapRequest))
|
||||
@@ -965,7 +1045,7 @@ func _Api_ChangeSubscribe_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/ChangeSubscribe",
|
||||
FullMethod: Api_ChangeSubscribe_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ChangeSubscribe(ctx, req.(*ChangeSubscribeRequest))
|
||||
@@ -983,7 +1063,7 @@ func _Api_GetStreamAlias_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetStreamAlias",
|
||||
FullMethod: Api_GetStreamAlias_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetStreamAlias(ctx, req.(*emptypb.Empty))
|
||||
@@ -1001,7 +1081,7 @@ func _Api_SetStreamAlias_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/SetStreamAlias",
|
||||
FullMethod: Api_SetStreamAlias_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).SetStreamAlias(ctx, req.(*SetStreamAliasRequest))
|
||||
@@ -1019,7 +1099,7 @@ func _Api_StopPublish_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/StopPublish",
|
||||
FullMethod: Api_StopPublish_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StopPublish(ctx, req.(*StreamSnapRequest))
|
||||
@@ -1037,7 +1117,7 @@ func _Api_StopSubscribe_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/StopSubscribe",
|
||||
FullMethod: Api_StopSubscribe_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StopSubscribe(ctx, req.(*RequestWithId))
|
||||
@@ -1055,7 +1135,7 @@ func _Api_GetConfigFile_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetConfigFile",
|
||||
FullMethod: Api_GetConfigFile_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetConfigFile(ctx, req.(*emptypb.Empty))
|
||||
@@ -1073,7 +1153,7 @@ func _Api_UpdateConfigFile_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/UpdateConfigFile",
|
||||
FullMethod: Api_UpdateConfigFile_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).UpdateConfigFile(ctx, req.(*UpdateConfigFileRequest))
|
||||
@@ -1091,7 +1171,7 @@ func _Api_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(inter
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetConfig",
|
||||
FullMethod: Api_GetConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetConfig(ctx, req.(*GetConfigRequest))
|
||||
@@ -1109,7 +1189,7 @@ func _Api_GetFormily_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetFormily",
|
||||
FullMethod: Api_GetFormily_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetFormily(ctx, req.(*GetConfigRequest))
|
||||
@@ -1117,24 +1197,6 @@ func _Api_GetFormily_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_ModifyConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ModifyConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).ModifyConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/ModifyConfig",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ModifyConfig(ctx, req.(*ModifyConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_GetPullProxyList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -1145,7 +1207,7 @@ func _Api_GetPullProxyList_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetPullProxyList",
|
||||
FullMethod: Api_GetPullProxyList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetPullProxyList(ctx, req.(*emptypb.Empty))
|
||||
@@ -1163,7 +1225,7 @@ func _Api_AddPullProxy_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/AddPullProxy",
|
||||
FullMethod: Api_AddPullProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).AddPullProxy(ctx, req.(*PullProxyInfo))
|
||||
@@ -1181,7 +1243,7 @@ func _Api_RemovePullProxy_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/RemovePullProxy",
|
||||
FullMethod: Api_RemovePullProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).RemovePullProxy(ctx, req.(*RequestWithId))
|
||||
@@ -1199,7 +1261,7 @@ func _Api_UpdatePullProxy_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/UpdatePullProxy",
|
||||
FullMethod: Api_UpdatePullProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).UpdatePullProxy(ctx, req.(*PullProxyInfo))
|
||||
@@ -1217,7 +1279,7 @@ func _Api_GetPushProxyList_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetPushProxyList",
|
||||
FullMethod: Api_GetPushProxyList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetPushProxyList(ctx, req.(*emptypb.Empty))
|
||||
@@ -1235,7 +1297,7 @@ func _Api_AddPushProxy_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/AddPushProxy",
|
||||
FullMethod: Api_AddPushProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).AddPushProxy(ctx, req.(*PushProxyInfo))
|
||||
@@ -1253,7 +1315,7 @@ func _Api_RemovePushProxy_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/RemovePushProxy",
|
||||
FullMethod: Api_RemovePushProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).RemovePushProxy(ctx, req.(*RequestWithId))
|
||||
@@ -1271,7 +1333,7 @@ func _Api_UpdatePushProxy_Handler(srv interface{}, ctx context.Context, dec func
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/UpdatePushProxy",
|
||||
FullMethod: Api_UpdatePushProxy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).UpdatePushProxy(ctx, req.(*PushProxyInfo))
|
||||
@@ -1289,7 +1351,7 @@ func _Api_GetRecording_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetRecording",
|
||||
FullMethod: Api_GetRecording_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetRecording(ctx, req.(*emptypb.Empty))
|
||||
@@ -1307,7 +1369,7 @@ func _Api_GetTransformList_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetTransformList",
|
||||
FullMethod: Api_GetTransformList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetTransformList(ctx, req.(*emptypb.Empty))
|
||||
@@ -1325,7 +1387,7 @@ func _Api_GetRecordList_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetRecordList",
|
||||
FullMethod: Api_GetRecordList_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetRecordList(ctx, req.(*ReqRecordList))
|
||||
@@ -1343,7 +1405,7 @@ func _Api_GetRecordCatalog_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/GetRecordCatalog",
|
||||
FullMethod: Api_GetRecordCatalog_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetRecordCatalog(ctx, req.(*ReqRecordCatalog))
|
||||
@@ -1361,7 +1423,7 @@ func _Api_DeleteRecord_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/global.api/DeleteRecord",
|
||||
FullMethod: Api_DeleteRecord_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).DeleteRecord(ctx, req.(*ReqRecordDelete))
|
||||
@@ -1484,10 +1546,6 @@ var Api_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetFormily",
|
||||
Handler: _Api_GetFormily_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ModifyConfig",
|
||||
Handler: _Api_ModifyConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetPullProxyList",
|
||||
Handler: _Api_GetPullProxyList_Handler,
|
||||
|
||||
@@ -172,6 +172,7 @@ func (r *AVRingReader) ReadFrame(conf *config.Subscribe) (err error) {
|
||||
}
|
||||
}
|
||||
r.Delay = r.Track.LastValue.Sequence - r.Value.Sequence
|
||||
// fmt.Println(r.Delay)
|
||||
if r.Track.ICodecCtx != nil {
|
||||
r.Log(context.TODO(), task.TraceLevel, r.Track.FourCC().String(), "ts", r.Value.Timestamp, "delay", r.Delay, "bps", r.BPS)
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/opusparser"
|
||||
)
|
||||
@@ -58,6 +59,12 @@ func (ctx *AACCtx) GetSampleRate() int {
|
||||
func (ctx *AACCtx) GetBase() ICodecCtx {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *AACCtx) String() string {
|
||||
// https://www.w3.org/TR/webcodecs-aac-codec-registration/
|
||||
return fmt.Sprintf("mp4a.40.%d", ctx.Config.ObjectType)
|
||||
}
|
||||
|
||||
func (ctx *AACCtx) GetRecord() []byte {
|
||||
return ctx.ConfigBytes
|
||||
}
|
||||
@@ -78,9 +85,18 @@ func (ctx *PCMACtx) GetBase() ICodecCtx {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *PCMACtx) String() string {
|
||||
return "alaw"
|
||||
}
|
||||
|
||||
func (ctx *PCMUCtx) String() string {
|
||||
return "ulaw"
|
||||
}
|
||||
|
||||
func (ctx *PCMUCtx) GetBase() ICodecCtx {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (*PCMUCtx) GetRecord() []byte {
|
||||
return []byte{} //TODO
|
||||
}
|
||||
@@ -95,6 +111,11 @@ func (*OPUSCtx) FourCC() FourCC {
|
||||
func (ctx *OPUSCtx) GetBase() ICodecCtx {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *OPUSCtx) String() string {
|
||||
return "opus"
|
||||
}
|
||||
|
||||
func (ctx *OPUSCtx) GetChannels() int {
|
||||
return ctx.ChannelLayout().Count()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package codec
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
AV1_OBU_SEQUENCE_HEADER = 1
|
||||
AV1_OBU_TEMPORAL_DELIMITER = 2
|
||||
@@ -41,3 +43,7 @@ func (*AV1Ctx) FourCC() FourCC {
|
||||
func (ctx *AV1Ctx) GetRecord() []byte {
|
||||
return ctx.ConfigOBUs
|
||||
}
|
||||
|
||||
func (ctx *AV1Ctx) String() string {
|
||||
return fmt.Sprintf("av01.%02X%02X%02X", ctx.ConfigOBUs[0], ctx.ConfigOBUs[1], ctx.ConfigOBUs[2])
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
)
|
||||
|
||||
@@ -126,3 +127,7 @@ func (h264 *H264Ctx) GetBase() ICodecCtx {
|
||||
func (ctx *H264Ctx) GetRecord() []byte {
|
||||
return ctx.Record
|
||||
}
|
||||
|
||||
func (h264 *H264Ctx) String() string {
|
||||
return fmt.Sprintf("avc1.%02X%02X%02X", h264.RecordInfo.AVCProfileIndication, h264.RecordInfo.ProfileCompatibility, h264.RecordInfo.AVCLevelIndication)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package codec
|
||||
|
||||
import "fmt"
|
||||
import "github.com/deepch/vdk/codec/h265parser"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
)
|
||||
|
||||
type H265NALUType byte
|
||||
|
||||
@@ -36,3 +39,7 @@ func (h265 *H265Ctx) GetBase() ICodecCtx {
|
||||
func (h265 *H265Ctx) GetRecord() []byte {
|
||||
return h265.Record
|
||||
}
|
||||
|
||||
func (h265 *H265Ctx) String() string {
|
||||
return fmt.Sprintf("hvc1.%02X%02X%02X", h265.RecordInfo.AVCProfileIndication, h265.RecordInfo.ProfileCompatibility, h265.RecordInfo.AVCLevelIndication)
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ type ICodecCtx interface {
|
||||
GetInfo() string
|
||||
GetBase() ICodecCtx
|
||||
GetRecord() []byte
|
||||
String() string
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ func (f FourCC) String() string {
|
||||
return string(f[:])
|
||||
}
|
||||
|
||||
func (f FourCC) MatchString(str string) bool {
|
||||
return string(f[:]) == str[:4]
|
||||
}
|
||||
|
||||
func (f FourCC) Name() string {
|
||||
switch f {
|
||||
case FourCC_H264:
|
||||
|
||||
@@ -3,7 +3,6 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mcuadros/go-defaults"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os"
|
||||
@@ -12,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mcuadros/go-defaults"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -100,6 +101,12 @@ func (config *Config) Parse(s any, prefix ...string) {
|
||||
}
|
||||
|
||||
config.Ptr = v
|
||||
|
||||
if !v.IsValid() {
|
||||
fmt.Println("parse to ", prefix, config.name, s, "is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
config.Default = v.Interface()
|
||||
|
||||
if l := len(prefix); l > 0 { // 读取环境变量
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
|
||||
@@ -46,7 +42,7 @@ func (config *HTTP) GetHandler() http.Handler {
|
||||
return config.mux
|
||||
}
|
||||
|
||||
func (config *HTTP) CreateHttpMux() *http.ServeMux {
|
||||
func (config *HTTP) CreateHttpMux() http.Handler {
|
||||
config.mux = http.NewServeMux()
|
||||
return config.mux
|
||||
}
|
||||
@@ -73,10 +69,10 @@ func (config *HTTP) Handle(path string, f http.Handler, last bool) {
|
||||
config.mux = http.NewServeMux()
|
||||
}
|
||||
if config.CORS {
|
||||
f = CORS(f)
|
||||
f = util.CORS(f)
|
||||
}
|
||||
if config.UserName != "" && config.Password != "" {
|
||||
f = BasicAuth(config.UserName, config.Password, f)
|
||||
f = util.BasicAuth(config.UserName, config.Password, f)
|
||||
}
|
||||
for _, middleware := range config.middlewares {
|
||||
f = middleware(path, f)
|
||||
@@ -91,151 +87,3 @@ func (config *HTTP) GetHTTPConfig() *HTTP {
|
||||
// func (config *HTTP) Handler(r *http.Request) (h http.Handler, pattern string) {
|
||||
// return config.mux.Handler(r)
|
||||
// }
|
||||
|
||||
func (config *HTTP) CreateHTTPWork(logger *slog.Logger) *ListenHTTPWork {
|
||||
ret := &ListenHTTPWork{HTTP: config}
|
||||
ret.Logger = logger.With("addr", config.ListenAddr)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (config *HTTP) CreateHTTPSWork(logger *slog.Logger) *ListenHTTPSWork {
|
||||
ret := &ListenHTTPSWork{ListenHTTPWork{HTTP: config}}
|
||||
ret.Logger = logger.With("addr", config.ListenAddrTLS)
|
||||
return ret
|
||||
}
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := w.Header()
|
||||
header.Set("Access-Control-Allow-Credentials", "true")
|
||||
header.Set("Cross-Origin-Resource-Policy", "cross-origin")
|
||||
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization")
|
||||
header.Set("Access-Control-Allow-Private-Network", "true")
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
header.Set("Access-Control-Allow-Origin", origin[0])
|
||||
}
|
||||
if next != nil && r.Method != "OPTIONS" {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BasicAuth(u, p string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract the username and password from the request
|
||||
// Authorization header. If no Authentication header is present
|
||||
// or the header value is invalid, then the 'ok' return value
|
||||
// will be false.
|
||||
username, password, ok := r.BasicAuth()
|
||||
if ok {
|
||||
// Calculate SHA-256 hashes for the provided and expected
|
||||
// usernames and passwords.
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
expectedUsernameHash := sha256.Sum256([]byte(u))
|
||||
expectedPasswordHash := sha256.Sum256([]byte(p))
|
||||
|
||||
// 使用 subtle.ConstantTimeCompare() 进行校验
|
||||
// the provided username and password hashes equal the
|
||||
// expected username and password hashes. ConstantTimeCompare
|
||||
// 如果值相等,则返回1,否则返回0。
|
||||
// Importantly, we should to do the work to evaluate both the
|
||||
// username and password before checking the return values to
|
||||
// 避免泄露信息。
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
|
||||
// If the username and password are correct, then call
|
||||
// the next handler in the chain. Make sure to return
|
||||
// afterwards, so that none of the code below is run.
|
||||
if usernameMatch && passwordMatch {
|
||||
if next != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the Authentication header is not present, is invalid, or the
|
||||
// username or password is wrong, then set a WWW-Authenticate
|
||||
// header to inform the client that we expect them to use basic
|
||||
// authentication and send a 401 Unauthorized response.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
type ListenHTTPWork struct {
|
||||
task.Task
|
||||
*HTTP
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Start() (err error) {
|
||||
task.Server = &http.Server{
|
||||
Addr: task.ListenAddr,
|
||||
ReadTimeout: task.HTTP.ReadTimeout,
|
||||
WriteTimeout: task.HTTP.WriteTimeout,
|
||||
IdleTimeout: task.HTTP.IdleTimeout,
|
||||
Handler: task.GetHandler(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Go() error {
|
||||
task.Info("listen http")
|
||||
return task.Server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Dispose() {
|
||||
task.Info("http server stop")
|
||||
task.Server.Close()
|
||||
}
|
||||
|
||||
type ListenHTTPSWork struct {
|
||||
ListenHTTPWork
|
||||
}
|
||||
|
||||
func (task *ListenHTTPSWork) Start() (err error) {
|
||||
cer, _ := tls.X509KeyPair(LocalCert, LocalKey)
|
||||
task.Server = &http.Server{
|
||||
Addr: task.HTTP.ListenAddrTLS,
|
||||
ReadTimeout: task.HTTP.ReadTimeout,
|
||||
WriteTimeout: task.HTTP.WriteTimeout,
|
||||
IdleTimeout: task.HTTP.IdleTimeout,
|
||||
Handler: task.HTTP.GetHandler(),
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cer},
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (task *ListenHTTPSWork) Go() error {
|
||||
task.Info("listen https")
|
||||
return task.Server.ListenAndServeTLS(task.HTTP.CertFile, task.HTTP.KeyFile)
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func (task *ListenTCPWork) listen(handler TCPHandler) {
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay)
|
||||
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.DownListenAddr, err, tempDelay)
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ type (
|
||||
IdleTimeout time.Duration `desc:"空闲(无订阅)超时"` // 空闲(无订阅)超时
|
||||
PauseTimeout time.Duration `default:"30s" desc:"暂停超时时间"` // 暂停超时
|
||||
BufferTime time.Duration `desc:"缓冲时长,0代表取最近关键帧"` // 缓冲长度(单位:秒),0代表取最近关键帧
|
||||
Speed float64 `default:"0" desc:"发送速率"` // 发送速率,0 为不限速
|
||||
Speed float64 `default:"1" desc:"发送速率"` // 发送速率,0 为不限速
|
||||
Scale float64 `default:"1" desc:"缩放倍数"` // 缩放倍数
|
||||
MaxFPS int `default:"30" desc:"最大FPS"` // 最大FPS
|
||||
MaxFPS int `default:"60" desc:"最大FPS"` // 最大FPS
|
||||
Key string `desc:"发布鉴权key"` // 发布鉴权key
|
||||
RingSize util.Range[int] `default:"20-1024" desc:"RingSize范围"` // 缓冲区大小范围
|
||||
RelayMode string `default:"remux" desc:"转发模式" enum:"remux:转格式,relay:纯转发,mix:混合转发"` // 转发模式
|
||||
@@ -164,3 +164,36 @@ func (v HTTPValues) DeepClone() (ret HTTPValues) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *TransfromOutput) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
// If it's a string, assign it to Target
|
||||
return node.Decode(&r.Target)
|
||||
}
|
||||
|
||||
if node.Kind == yaml.MappingNode {
|
||||
var conf map[string]any
|
||||
if err := node.Decode(&conf); err != nil {
|
||||
return err
|
||||
}
|
||||
var normal bool
|
||||
if conf["target"] != nil {
|
||||
r.Target = conf["target"].(string)
|
||||
normal = true
|
||||
}
|
||||
if conf["streampath"] != nil {
|
||||
r.StreamPath = conf["streampath"].(string)
|
||||
normal = true
|
||||
}
|
||||
if conf["conf"] != nil {
|
||||
r.Conf = conf["conf"]
|
||||
normal = true
|
||||
}
|
||||
if !normal {
|
||||
r.Conf = conf
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported node kind: %v", node.Kind)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (task *ListenUDPWork) Go() error {
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay)
|
||||
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.DownListenAddr, err, tempDelay)
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type User struct {
|
||||
Username string `gorm:"uniqueIndex;size:64"`
|
||||
Password string `gorm:"size:60"` // bcrypt hash
|
||||
Role string `gorm:"size:20;default:'user'"` // admin or user
|
||||
LastLogin time.Time
|
||||
LastLogin time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook to hash password before saving
|
||||
|
||||
109
pkg/http_server_fasthttp.go
Normal file
109
pkg/http_server_fasthttp.go
Normal file
@@ -0,0 +1,109 @@
|
||||
//go:build fasthttp
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
func CreateHTTPWork(conf *config.HTTP, logger *slog.Logger) *ListenFastHTTPWork {
|
||||
ret := &ListenFastHTTPWork{HTTP: conf}
|
||||
ret.Logger = logger.With("addr", conf.ListenAddr)
|
||||
return ret
|
||||
}
|
||||
|
||||
func CreateHTTPSWork(conf *config.HTTP, logger *slog.Logger) *ListenFastHTTPSWork {
|
||||
ret := &ListenFastHTTPSWork{ListenFastHTTPWork{HTTP: conf}}
|
||||
ret.Logger = logger.With("addr", conf.ListenAddrTLS)
|
||||
return ret
|
||||
}
|
||||
|
||||
// ListenFastHTTPWork 用于启动 FastHTTP 服务
|
||||
type ListenFastHTTPWork struct {
|
||||
task.Task
|
||||
*config.HTTP
|
||||
server *fasthttp.Server
|
||||
}
|
||||
|
||||
// 主请求处理函数
|
||||
func (task *ListenFastHTTPWork) requestHandler(ctx *fasthttp.RequestCtx) {
|
||||
fasthttpadaptor.NewFastHTTPHandler(task.GetHandler())(ctx)
|
||||
}
|
||||
|
||||
func (task *ListenFastHTTPWork) Start() (err error) {
|
||||
|
||||
// 配置 fasthttp 服务器
|
||||
task.server = &fasthttp.Server{
|
||||
Handler: task.requestHandler,
|
||||
ReadTimeout: task.HTTP.ReadTimeout,
|
||||
WriteTimeout: task.HTTP.WriteTimeout,
|
||||
IdleTimeout: task.HTTP.IdleTimeout,
|
||||
Name: "Monibuca FastHTTP Server",
|
||||
// 启用流式响应支持
|
||||
StreamRequestBody: true,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (task *ListenFastHTTPWork) Go() error {
|
||||
task.Info("listen fasthttp")
|
||||
return task.server.ListenAndServe(task.ListenAddr)
|
||||
}
|
||||
|
||||
func (task *ListenFastHTTPWork) Dispose() {
|
||||
task.Info("fasthttp server stop")
|
||||
if task.server != nil {
|
||||
if err := task.server.Shutdown(); err != nil {
|
||||
task.Error("shutdown error", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListenFastHTTPSWork 用于启动 HTTPS FastHTTP 服务
|
||||
type ListenFastHTTPSWork struct {
|
||||
ListenFastHTTPWork
|
||||
}
|
||||
|
||||
func (task *ListenFastHTTPSWork) Start() (err error) {
|
||||
cer, _ := tls.X509KeyPair(config.LocalCert, config.LocalKey)
|
||||
// 调用基类的 Start
|
||||
if err = task.ListenFastHTTPWork.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
task.server.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cer},
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (task *ListenFastHTTPSWork) Go() error {
|
||||
task.Info("listen https fasthttp")
|
||||
return task.server.ListenAndServeTLS(task.ListenAddrTLS, task.CertFile, task.KeyFile)
|
||||
}
|
||||
96
pkg/http_server_std.go
Normal file
96
pkg/http_server_std.go
Normal file
@@ -0,0 +1,96 @@
|
||||
//go:build !fasthttp
|
||||
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
func CreateHTTPWork(conf *config.HTTP, logger *slog.Logger) *ListenHTTPWork {
|
||||
ret := &ListenHTTPWork{HTTP: conf}
|
||||
ret.Logger = logger.With("addr", conf.ListenAddr)
|
||||
return ret
|
||||
}
|
||||
|
||||
func CreateHTTPSWork(conf *config.HTTP, logger *slog.Logger) *ListenHTTPSWork {
|
||||
ret := &ListenHTTPSWork{ListenHTTPWork{HTTP: conf}}
|
||||
ret.Logger = logger.With("addr", conf.ListenAddrTLS)
|
||||
return ret
|
||||
}
|
||||
|
||||
type ListenHTTPWork struct {
|
||||
task.Task
|
||||
*config.HTTP
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Start() (err error) {
|
||||
task.Server = &http.Server{
|
||||
Addr: task.ListenAddr,
|
||||
ReadTimeout: task.HTTP.ReadTimeout,
|
||||
WriteTimeout: task.HTTP.WriteTimeout,
|
||||
IdleTimeout: task.HTTP.IdleTimeout,
|
||||
Handler: task.GetHandler(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Go() error {
|
||||
task.Info("listen http")
|
||||
return task.Server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (task *ListenHTTPWork) Dispose() {
|
||||
task.Info("http server stop")
|
||||
task.Server.Close()
|
||||
}
|
||||
|
||||
type ListenHTTPSWork struct {
|
||||
ListenHTTPWork
|
||||
}
|
||||
|
||||
func (task *ListenHTTPSWork) Start() (err error) {
|
||||
cer, _ := tls.X509KeyPair(config.LocalCert, config.LocalKey)
|
||||
task.Server = &http.Server{
|
||||
Addr: task.HTTP.ListenAddrTLS,
|
||||
ReadTimeout: task.HTTP.ReadTimeout,
|
||||
WriteTimeout: task.HTTP.WriteTimeout,
|
||||
IdleTimeout: task.HTTP.IdleTimeout,
|
||||
Handler: task.HTTP.GetHandler(),
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cer},
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (task *ListenHTTPSWork) Go() error {
|
||||
task.Info("listen https")
|
||||
return task.Server.ListenAndServeTLS(task.HTTP.CertFile, task.HTTP.KeyFile)
|
||||
}
|
||||
@@ -4,6 +4,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ITickTask interface {
|
||||
IChannelTask
|
||||
GetTickInterval() time.Duration
|
||||
GetTicker() *time.Ticker
|
||||
}
|
||||
|
||||
type ChannelTask struct {
|
||||
Task
|
||||
SignalChan any
|
||||
@@ -25,16 +31,36 @@ type TickTask struct {
|
||||
Ticker *time.Ticker
|
||||
}
|
||||
|
||||
func (t *TickTask) GetTicker() *time.Ticker {
|
||||
return t.Ticker
|
||||
}
|
||||
|
||||
func (t *TickTask) GetTickInterval() time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
|
||||
func (t *TickTask) Start() (err error) {
|
||||
t.Ticker = time.NewTicker(t.handler.(interface{ GetTickInterval() time.Duration }).GetTickInterval())
|
||||
t.Ticker = time.NewTicker(t.handler.(ITickTask).GetTickInterval())
|
||||
t.SignalChan = t.Ticker.C
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TickTask) Dispose() {
|
||||
t.Ticker.Stop()
|
||||
type AsyncTickTask struct {
|
||||
TickTask
|
||||
}
|
||||
|
||||
func (t *AsyncTickTask) GetSignal() any {
|
||||
return t.Task.GetSignal()
|
||||
}
|
||||
|
||||
func (t *AsyncTickTask) Go() error {
|
||||
t.handler.(ITickTask).Tick(nil)
|
||||
for {
|
||||
select {
|
||||
case c := <-t.Ticker.C:
|
||||
t.handler.(ITickTask).Tick(c)
|
||||
case <-t.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,17 @@ func (mt *Job) Blocked() ITask {
|
||||
}
|
||||
|
||||
func (mt *Job) waitChildrenDispose() {
|
||||
defer func() {
|
||||
// 忽略由于在任务关闭过程中可能存在竞态条件,当父任务关闭时子任务可能已经被释放。
|
||||
if err := recover(); err != nil {
|
||||
mt.Debug("waitChildrenDispose panic", "err", err)
|
||||
}
|
||||
mt.addSub <- nil
|
||||
<-mt.childrenDisposed
|
||||
}()
|
||||
if blocked := mt.blocked; blocked != nil {
|
||||
blocked.Stop(mt.StopReason())
|
||||
}
|
||||
mt.addSub <- nil
|
||||
<-mt.childrenDisposed
|
||||
}
|
||||
|
||||
func (mt *Job) OnChildDispose(listener func(ITask)) {
|
||||
@@ -214,20 +220,28 @@ func (mt *Job) run() {
|
||||
mt.blocked = mt.children[taskIndex]
|
||||
switch tt := mt.blocked.(type) {
|
||||
case IChannelTask:
|
||||
tt.Tick(rev.Interface())
|
||||
if tt.IsStopped() {
|
||||
mt.onChildDispose(mt.blocked)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
if mt.onChildDispose(mt.blocked); mt.blocked.checkRetry(mt.blocked.StopReason()) {
|
||||
if mt.blocked.reset(); mt.blocked.start() {
|
||||
mt.cases[chosen].Chan = reflect.ValueOf(mt.blocked.GetSignal())
|
||||
continue
|
||||
switch ttt := tt.(type) {
|
||||
case ITickTask:
|
||||
ttt.GetTicker().Stop()
|
||||
}
|
||||
mt.onChildDispose(mt.blocked)
|
||||
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
|
||||
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
|
||||
} else {
|
||||
tt.Tick(rev.Interface())
|
||||
}
|
||||
default:
|
||||
if !ok {
|
||||
if mt.onChildDispose(mt.blocked); mt.blocked.checkRetry(mt.blocked.StopReason()) {
|
||||
if mt.blocked.reset(); mt.blocked.start() {
|
||||
mt.cases[chosen].Chan = reflect.ValueOf(mt.blocked.GetSignal())
|
||||
continue
|
||||
}
|
||||
}
|
||||
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
|
||||
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
|
||||
}
|
||||
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
|
||||
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
|
||||
}
|
||||
}
|
||||
if !mt.handler.keepalive() && len(mt.children) == 0 {
|
||||
|
||||
@@ -36,3 +36,28 @@ func (m *Manager[K, T]) Add(ctx T, opt ...any) *Task {
|
||||
})
|
||||
return m.AddTask(ctx, opt...)
|
||||
}
|
||||
|
||||
// SafeGet 用于不同协程获取元素,防止并发请求
|
||||
func (m *Manager[K, T]) SafeGet(key K) (item T, ok bool) {
|
||||
if m.L == nil {
|
||||
m.Call(func() error {
|
||||
item, ok = m.Collection.Get(key)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
item, ok = m.Collection.Get(key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SafeRange 用于不同协程获取元素,防止并发请求
|
||||
func (m *Manager[K, T]) SafeRange(f func(T) bool) {
|
||||
if m.L == nil {
|
||||
m.Call(func() error {
|
||||
m.Collection.Range(f)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
m.Collection.Range(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ type (
|
||||
OnDispose(func())
|
||||
GetState() TaskState
|
||||
GetLevel() byte
|
||||
WaitStopped() error
|
||||
WaitStarted() error
|
||||
}
|
||||
IJob interface {
|
||||
ITask
|
||||
@@ -324,6 +326,9 @@ func (task *Task) start() bool {
|
||||
task.ResetRetryCount()
|
||||
if runHandler, ok := task.handler.(TaskBlock); ok {
|
||||
task.state = TASK_STATE_RUNNING
|
||||
if task.Logger != nil {
|
||||
task.Debug("task run", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
||||
}
|
||||
err = runHandler.Run()
|
||||
if err == nil {
|
||||
err = ErrTaskComplete
|
||||
@@ -334,6 +339,9 @@ func (task *Task) start() bool {
|
||||
if err == nil {
|
||||
if goHandler, ok := task.handler.(TaskGo); ok {
|
||||
task.state = TASK_STATE_GOING
|
||||
if task.Logger != nil {
|
||||
task.Debug("task go", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
||||
}
|
||||
go task.run(goHandler.Go)
|
||||
}
|
||||
return true
|
||||
|
||||
115
pkg/track.go
115
pkg/track.go
@@ -14,6 +14,11 @@ import (
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
const threshold = 10 * time.Millisecond
|
||||
const DROP_FRAME_LEVEL_NODROP = 0
|
||||
const DROP_FRAME_LEVEL_DROP_P = 1
|
||||
const DROP_FRAME_LEVEL_DROP_ALL = 2
|
||||
|
||||
type (
|
||||
Track struct {
|
||||
*slog.Logger
|
||||
@@ -30,8 +35,23 @@ type (
|
||||
Track
|
||||
}
|
||||
TsTamer struct {
|
||||
BaseTs, LastTs time.Duration
|
||||
BaseTs, LastTs, BeforeScaleChangedTs time.Duration
|
||||
LastScale float64
|
||||
}
|
||||
SpeedController struct {
|
||||
speed float64
|
||||
pausedTime time.Duration
|
||||
beginTime time.Time
|
||||
beginTimestamp time.Duration
|
||||
Delta time.Duration
|
||||
}
|
||||
DropController struct {
|
||||
acceptFrameCount int
|
||||
accpetFPS int
|
||||
LastDropLevelChange time.Time
|
||||
DropFrameLevel int // 0: no drop, 1: drop P-frame, 2: drop all
|
||||
}
|
||||
|
||||
AVTrack struct {
|
||||
Track
|
||||
*RingWriter
|
||||
@@ -40,6 +60,8 @@ type (
|
||||
SequenceFrame IAVFrame
|
||||
WrapIndex int
|
||||
TsTamer
|
||||
SpeedController
|
||||
DropController
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,7 +89,7 @@ func NewAVTrack(args ...any) (t *AVTrack) {
|
||||
}
|
||||
}
|
||||
//t.ready = util.NewPromise(struct{}{})
|
||||
t.Info("create")
|
||||
t.Info("create", "dropFrameLevel", t.DropFrameLevel)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,6 +109,58 @@ func (t *Track) AddBytesIn(n int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AVTrack) AddBytesIn(n int) {
|
||||
dur := time.Since(t.lastBPSTime)
|
||||
t.Track.AddBytesIn(n)
|
||||
if t.frameCount == 0 {
|
||||
t.accpetFPS = int(float64(t.acceptFrameCount) / dur.Seconds())
|
||||
t.acceptFrameCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AVTrack) AcceptFrame(data IAVFrame) {
|
||||
t.acceptFrameCount++
|
||||
t.Value.Wraps = append(t.Value.Wraps, data)
|
||||
}
|
||||
|
||||
func (t *AVTrack) changeDropFrameLevel(newLevel int) {
|
||||
t.Warn("change drop frame level", "from", t.DropFrameLevel, "to", newLevel)
|
||||
t.DropFrameLevel = newLevel
|
||||
t.LastDropLevelChange = time.Now()
|
||||
}
|
||||
|
||||
func (t *AVTrack) CheckIfNeedDropFrame(maxFPS int) (drop bool) {
|
||||
drop = maxFPS > 0 && (t.accpetFPS > maxFPS)
|
||||
if drop {
|
||||
defer func() {
|
||||
if time.Since(t.LastDropLevelChange) > time.Second && t.DropFrameLevel > 0 {
|
||||
t.changeDropFrameLevel(t.DropFrameLevel + 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Enhanced frame dropping strategy based on DropFrameLevel
|
||||
switch t.DropFrameLevel {
|
||||
case DROP_FRAME_LEVEL_NODROP:
|
||||
if drop {
|
||||
t.changeDropFrameLevel(DROP_FRAME_LEVEL_DROP_P)
|
||||
}
|
||||
case DROP_FRAME_LEVEL_DROP_P: // Drop P-frame
|
||||
if !t.Value.IDR {
|
||||
return true
|
||||
} else if !drop {
|
||||
t.changeDropFrameLevel(DROP_FRAME_LEVEL_NODROP)
|
||||
}
|
||||
return false
|
||||
default:
|
||||
if !drop {
|
||||
t.changeDropFrameLevel(DROP_FRAME_LEVEL_DROP_P)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *AVTrack) Ready(err error) {
|
||||
if t.ready.IsPending() {
|
||||
if err != nil {
|
||||
@@ -133,13 +207,46 @@ func (t *TsTamer) Tame(ts time.Duration, fps int, scale float64) (result time.Du
|
||||
result = max(1*time.Millisecond, t.BaseTs+ts)
|
||||
if fps > 0 {
|
||||
frameDur := float64(time.Second) / float64(fps)
|
||||
if math.Abs(float64(result-t.LastTs)) > 10*frameDur { //时间戳突变
|
||||
if math.Abs(float64(result-t.LastTs)) > 10*frameDur*scale { //时间戳突变
|
||||
// t.Warn("timestamp mutation", "fps", t.FPS, "lastTs", uint32(t.LastTs/time.Millisecond), "ts", uint32(frame.Timestamp/time.Millisecond), "frameDur", time.Duration(frameDur))
|
||||
result = t.LastTs + time.Duration(frameDur)
|
||||
t.BaseTs = result - ts
|
||||
}
|
||||
}
|
||||
t.LastTs = result
|
||||
result = time.Duration(float64(result) / scale)
|
||||
if t.LastScale != scale {
|
||||
t.BeforeScaleChangedTs = result
|
||||
t.LastScale = scale
|
||||
}
|
||||
result = t.BeforeScaleChangedTs + time.Duration(float64(result-t.BeforeScaleChangedTs)/scale)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *AVTrack) SpeedControl(speed float64) {
|
||||
t.speedControl(speed, t.LastTs)
|
||||
}
|
||||
|
||||
func (t *AVTrack) AddPausedTime(d time.Duration) {
|
||||
t.pausedTime += d
|
||||
}
|
||||
|
||||
func (s *SpeedController) speedControl(speed float64, ts time.Duration) {
|
||||
if speed != s.speed || s.beginTime.IsZero() {
|
||||
s.speed = speed
|
||||
s.beginTime = time.Now()
|
||||
s.beginTimestamp = ts
|
||||
s.pausedTime = 0
|
||||
} else {
|
||||
elapsed := time.Since(s.beginTime) - s.pausedTime
|
||||
if speed == 0 {
|
||||
s.Delta = ts - elapsed
|
||||
return
|
||||
}
|
||||
should := time.Duration(float64(ts-s.beginTimestamp) / speed)
|
||||
s.Delta = should - elapsed
|
||||
// fmt.Println(speed, elapsed, should, s.Delta)
|
||||
if s.Delta > threshold {
|
||||
time.Sleep(min(s.Delta, time.Millisecond*500))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
var (
|
||||
UnixTimeReg = regexp.MustCompile(`^\d+$`)
|
||||
UnixTimeRangeReg = regexp.MustCompile(`^(\d+)(~|-)(\d+)$`)
|
||||
TimeStrRangeReg = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$`)
|
||||
TimeStrRangeReg = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?)~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?)$`)
|
||||
)
|
||||
|
||||
func TimeRangeQueryParse(query url.Values) (startTime, endTime time.Time, err error) {
|
||||
@@ -100,6 +100,9 @@ func TimeQueryParseRefer(query string, refer time.Time) (time.Time, error) {
|
||||
if !strings.Contains(query, "T") {
|
||||
query = refer.Format("2006-01-02") + "T" + query
|
||||
}
|
||||
if strings.Contains(query, "Z") {
|
||||
return time.Parse(time.RFC3339, query)
|
||||
}
|
||||
return time.ParseInLocation("2006-01-02T15:04:05", query, time.Local)
|
||||
}
|
||||
|
||||
|
||||
@@ -150,16 +150,18 @@ func ReturnFetchValue[T any](fetch func() T, rw http.ResponseWriter, r *http.Req
|
||||
tickDur = time.Second
|
||||
}
|
||||
if r.Header.Get("Accept") == "text/event-stream" {
|
||||
sse := NewSSE(rw, r.Context())
|
||||
tick := time.NewTicker(tickDur)
|
||||
defer tick.Stop()
|
||||
writer := Conditional(isYaml, sse.WriteYAML, sse.WriteJSON)
|
||||
writer(fetch())
|
||||
for range tick.C {
|
||||
if writer(fetch()) != nil {
|
||||
return
|
||||
NewSSE(rw, r.Context(), func(sse *SSE) {
|
||||
tick := time.NewTicker(tickDur)
|
||||
defer tick.Stop()
|
||||
writer := Conditional(isYaml, sse.WriteYAML, sse.WriteJSON)
|
||||
err := writer(fetch())
|
||||
for range tick.C {
|
||||
if err = writer(fetch()); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
data := fetch()
|
||||
rw.Header().Set("Content-Type", Conditional(isYaml, "text/yaml", "application/json"))
|
||||
@@ -217,7 +219,8 @@ func CORS(next http.Handler) http.Handler {
|
||||
header := w.Header()
|
||||
header.Set("Access-Control-Allow-Credentials", "true")
|
||||
header.Set("Cross-Origin-Resource-Policy", "cross-origin")
|
||||
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token")
|
||||
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization")
|
||||
header.Set("Access-Control-Allow-Private-Network", "true")
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !fasthttp
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
@@ -16,6 +18,7 @@ var (
|
||||
sseEnd = []byte("\n\n")
|
||||
)
|
||||
|
||||
// SSE 标准库实现
|
||||
type SSE struct {
|
||||
http.ResponseWriter
|
||||
context.Context
|
||||
@@ -45,7 +48,7 @@ func (sse *SSE) WriteEvent(event string, data []byte) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
|
||||
func NewSSE(w http.ResponseWriter, ctx context.Context, block func(sse *SSE)) (sse *SSE) {
|
||||
header := w.Header()
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
@@ -56,10 +59,12 @@ func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
|
||||
// rw.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
|
||||
// rw.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
// rw.Header().Set("Transfer-Encoding", "chunked")
|
||||
return &SSE{
|
||||
sse = &SSE{
|
||||
ResponseWriter: w,
|
||||
Context: ctx,
|
||||
}
|
||||
block(sse)
|
||||
return sse
|
||||
}
|
||||
|
||||
func (sse *SSE) WriteJSON(data any) error {
|
||||
|
||||
87
pkg/util/sse_fasthttp.go
Normal file
87
pkg/util/sse_fasthttp.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build fasthttp
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 定义 SSE 常量,与 sse.go 中保持一致
|
||||
var (
|
||||
// 这些变量需要在这里重新定义,因为使用构建标签后无法共享
|
||||
sseEent = []byte("event: ")
|
||||
sseBegin = []byte("data: ")
|
||||
sseEnd = []byte("\n\n")
|
||||
)
|
||||
|
||||
// SSE 结构体在 fasthttp 构建模式下的实现
|
||||
type SSE struct {
|
||||
Writer *bufio.Writer
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (sse *SSE) Write(data []byte) (n int, err error) {
|
||||
if err = sse.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
buffers := net.Buffers{sseBegin, data, sseEnd}
|
||||
nn, err := buffers.WriteTo(sse.Writer)
|
||||
if err == nil {
|
||||
sse.Writer.Flush()
|
||||
}
|
||||
return int(nn), err
|
||||
}
|
||||
|
||||
func (sse *SSE) WriteEvent(event string, data []byte) (err error) {
|
||||
if err = sse.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
buffers := net.Buffers{sseEent, []byte(event + "\n"), sseBegin, data, sseEnd}
|
||||
_, err = buffers.WriteTo(sse.Writer)
|
||||
if err == nil {
|
||||
sse.Writer.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewSSE(w http.ResponseWriter, ctx context.Context, block func(sse *SSE)) (sse *SSE) {
|
||||
reqCtx := ctx.(*fasthttp.RequestCtx)
|
||||
header := w.Header()
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("X-Accel-Buffering", "no")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
sse = &SSE{
|
||||
Context: ctx,
|
||||
}
|
||||
reqCtx.Response.SetBodyStreamWriter(func(w *bufio.Writer) {
|
||||
sse.Writer = w
|
||||
block(sse)
|
||||
<-ctx.Done()
|
||||
})
|
||||
return sse
|
||||
}
|
||||
|
||||
func (sse *SSE) WriteJSON(data any) error {
|
||||
return json.NewEncoder(sse).Encode(data)
|
||||
}
|
||||
|
||||
func (sse *SSE) WriteYAML(data any) error {
|
||||
return yaml.NewEncoder(sse).Encode(data)
|
||||
}
|
||||
|
||||
// WriteExec 执行命令并将输出写入 SSE 流
|
||||
func (sse *SSE) WriteExec(cmd *exec.Cmd) error {
|
||||
cmd.Stderr = sse
|
||||
cmd.Stdout = sse
|
||||
return cmd.Run()
|
||||
}
|
||||
148
plugin.go
148
plugin.go
@@ -43,13 +43,15 @@ type (
|
||||
Name string
|
||||
Version string //插件版本
|
||||
Type reflect.Type
|
||||
defaultYaml DefaultYaml //默认配置
|
||||
DefaultYaml DefaultYaml //默认配置
|
||||
ServiceDesc *grpc.ServiceDesc
|
||||
RegisterGRPCHandler func(context.Context, *gatewayRuntime.ServeMux, *grpc.ClientConn) error
|
||||
Puller Puller
|
||||
Pusher Pusher
|
||||
Recorder Recorder
|
||||
Transformer Transformer
|
||||
NewPuller PullerFactory
|
||||
NewPusher PusherFactory
|
||||
NewRecorder RecorderFactory
|
||||
NewTransformer TransformerFactory
|
||||
NewPullProxy PullProxyFactory
|
||||
NewPushProxy PushProxyFactory
|
||||
OnExit OnExitHandler
|
||||
OnAuthPub AuthPublisher
|
||||
OnAuthSub AuthSubscriber
|
||||
@@ -88,12 +90,6 @@ type (
|
||||
IQUICPlugin interface {
|
||||
OnQUICConnect(quic.Connection) task.ITask
|
||||
}
|
||||
IPullProxyPlugin interface {
|
||||
OnPullProxyAdd(pullProxy *PullProxy) any
|
||||
}
|
||||
IPushProxyPlugin interface {
|
||||
OnPushProxyAdd(pushProxy *PushProxy) any
|
||||
}
|
||||
)
|
||||
|
||||
var plugins []PluginMeta
|
||||
@@ -121,9 +117,9 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
p.Config.Get(name).ParseGlobal(s.Config.Get(name))
|
||||
}
|
||||
}
|
||||
if plugin.defaultYaml != "" {
|
||||
if plugin.DefaultYaml != "" {
|
||||
var defaultConf map[string]any
|
||||
if err := yaml.Unmarshal([]byte(plugin.defaultYaml), &defaultConf); err != nil {
|
||||
if err := yaml.Unmarshal([]byte(plugin.DefaultYaml), &defaultConf); err != nil {
|
||||
p.Error("parsing default config", "error", err)
|
||||
} else {
|
||||
p.Config.ParseDefaultYaml(defaultConf)
|
||||
@@ -150,7 +146,11 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
p.Warn("plugin disabled")
|
||||
return
|
||||
} else {
|
||||
p.assign()
|
||||
var handlers map[string]http.HandlerFunc
|
||||
if v, ok := instance.(IRegisterHandler); ok {
|
||||
handlers = v.RegisterHandler()
|
||||
}
|
||||
p.registerHandler(handlers)
|
||||
}
|
||||
p.Info("init", "version", plugin.Version)
|
||||
var err error
|
||||
@@ -166,7 +166,7 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.DB != nil && p.Meta.Recorder != nil {
|
||||
if p.DB != nil && p.Meta.NewRecorder != nil {
|
||||
if err = p.DB.AutoMigrate(&RecordStream{}); err != nil {
|
||||
p.disable(fmt.Sprintf("auto migrate record stream failed %v", err))
|
||||
return
|
||||
@@ -178,35 +178,40 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
|
||||
// InstallPlugin 安装插件
|
||||
func InstallPlugin[C iPlugin](options ...any) error {
|
||||
var c *C
|
||||
t := reflect.TypeOf(c).Elem()
|
||||
meta := PluginMeta{
|
||||
Name: strings.TrimSuffix(t.Name(), "Plugin"),
|
||||
Type: t,
|
||||
var meta PluginMeta
|
||||
for _, option := range options {
|
||||
if m, ok := option.(PluginMeta); ok {
|
||||
meta = m
|
||||
}
|
||||
}
|
||||
var c *C
|
||||
meta.Type = reflect.TypeOf(c).Elem()
|
||||
if meta.Name == "" {
|
||||
meta.Name = strings.TrimSuffix(meta.Type.Name(), "Plugin")
|
||||
}
|
||||
|
||||
_, pluginFilePath, _, _ := runtime.Caller(1)
|
||||
configDir := filepath.Dir(pluginFilePath)
|
||||
|
||||
if _, after, found := strings.Cut(configDir, "@"); found {
|
||||
meta.Version = after
|
||||
} else {
|
||||
meta.Version = "dev"
|
||||
if meta.Version == "" {
|
||||
if _, after, found := strings.Cut(configDir, "@"); found {
|
||||
meta.Version = after
|
||||
} else {
|
||||
meta.Version = "dev"
|
||||
}
|
||||
}
|
||||
for _, option := range options {
|
||||
switch v := option.(type) {
|
||||
case OnExitHandler:
|
||||
meta.OnExit = v
|
||||
case DefaultYaml:
|
||||
meta.defaultYaml = v
|
||||
case Puller:
|
||||
meta.Puller = v
|
||||
case Pusher:
|
||||
meta.Pusher = v
|
||||
case Recorder:
|
||||
meta.Recorder = v
|
||||
case Transformer:
|
||||
meta.Transformer = v
|
||||
meta.DefaultYaml = v
|
||||
case PullerFactory:
|
||||
meta.NewPuller = v
|
||||
case PusherFactory:
|
||||
meta.NewPusher = v
|
||||
case RecorderFactory:
|
||||
meta.NewRecorder = v
|
||||
case TransformerFactory:
|
||||
meta.NewTransformer = v
|
||||
case AuthPublisher:
|
||||
meta.OnAuthPub = v
|
||||
case AuthSubscriber:
|
||||
@@ -269,34 +274,12 @@ func (p *Plugin) GetPublicIP(netcardIP string) string {
|
||||
return localIp
|
||||
}
|
||||
|
||||
func (p *Plugin) settingPath() string {
|
||||
return filepath.Join(p.Server.SettingDir, strings.ToLower(p.Meta.Name)+".yaml")
|
||||
}
|
||||
|
||||
func (p *Plugin) disable(reason string) {
|
||||
p.Disabled = true
|
||||
p.SetDescription("disableReason", reason)
|
||||
p.Server.disabledPlugins = append(p.Server.disabledPlugins, p)
|
||||
}
|
||||
|
||||
func (p *Plugin) assign() {
|
||||
f, err := os.Open(p.settingPath())
|
||||
defer f.Close()
|
||||
if err == nil {
|
||||
var modifyConfig map[string]any
|
||||
err = yaml.NewDecoder(f).Decode(&modifyConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p.Config.ParseModifyFile(modifyConfig)
|
||||
}
|
||||
var handlerMap map[string]http.HandlerFunc
|
||||
if v, ok := p.handler.(IRegisterHandler); ok {
|
||||
handlerMap = v.RegisterHandler()
|
||||
}
|
||||
p.registerHandler(handlerMap)
|
||||
}
|
||||
|
||||
func (p *Plugin) Start() (err error) {
|
||||
s := p.Server
|
||||
if p.Meta.ServiceDesc != nil && s.grpcServer != nil {
|
||||
@@ -337,12 +320,12 @@ func (p *Plugin) listen() (err error) {
|
||||
|
||||
if httpConf.ListenAddrTLS != "" && (httpConf.ListenAddrTLS != p.Server.config.HTTP.ListenAddrTLS) {
|
||||
p.SetDescription("httpTLS", strings.TrimPrefix(httpConf.ListenAddrTLS, ":"))
|
||||
p.AddDependTask(httpConf.CreateHTTPSWork(p.Logger))
|
||||
p.AddDependTask(CreateHTTPSWork(httpConf, p.Logger))
|
||||
}
|
||||
|
||||
if httpConf.ListenAddr != "" && (httpConf.ListenAddr != p.Server.config.HTTP.ListenAddr) {
|
||||
p.SetDescription("http", strings.TrimPrefix(httpConf.ListenAddr, ":"))
|
||||
p.AddDependTask(httpConf.CreateHTTPWork(p.Logger))
|
||||
p.AddDependTask(CreateHTTPWork(httpConf, p.Logger))
|
||||
}
|
||||
|
||||
if tcphandler, ok := p.handler.(ITCPPlugin); ok {
|
||||
@@ -467,14 +450,14 @@ func (p *Plugin) SendWebhook(hookType config.HookType, conf config.Webhook, data
|
||||
// TODO: use alias stream
|
||||
func (p *Plugin) OnPublish(pub *Publisher) {
|
||||
onPublish := p.config.OnPub
|
||||
if p.Meta.Pusher != nil {
|
||||
if p.Meta.NewPusher != nil {
|
||||
for r, pushConf := range onPublish.Push {
|
||||
if pushConf.URL = r.Replace(pub.StreamPath, pushConf.URL); pushConf.URL != "" {
|
||||
p.Push(pub.StreamPath, pushConf, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Meta.Recorder != nil {
|
||||
if p.Meta.NewRecorder != nil {
|
||||
for r, recConf := range onPublish.Record {
|
||||
if recConf.FilePath = r.Replace(pub.StreamPath, recConf.FilePath); recConf.FilePath != "" {
|
||||
p.Record(pub, recConf, nil)
|
||||
@@ -486,7 +469,7 @@ func (p *Plugin) OnPublish(pub *Publisher) {
|
||||
if owner != nil {
|
||||
_, isTransformer = owner.(ITransformer)
|
||||
}
|
||||
if p.Meta.Transformer != nil && !isTransformer {
|
||||
if p.Meta.NewTransformer != nil && !isTransformer {
|
||||
for r, tranConf := range onPublish.Transform {
|
||||
if group := r.FindStringSubmatch(pub.StreamPath); group != nil {
|
||||
for j, to := range tranConf.Output {
|
||||
@@ -531,7 +514,7 @@ func (p *Plugin) OnSubscribe(streamPath string, args url.Values) {
|
||||
// }
|
||||
// }
|
||||
for reg, conf := range p.config.OnSub.Pull {
|
||||
if p.Meta.Puller != nil && reg.MatchString(streamPath) {
|
||||
if p.Meta.NewPuller != nil && reg.MatchString(streamPath) {
|
||||
conf.Args = config.HTTPValues(args)
|
||||
conf.URL = reg.Replace(streamPath, conf.URL)
|
||||
p.handler.Pull(streamPath, conf, nil)
|
||||
@@ -631,7 +614,7 @@ func (p *Plugin) Subscribe(ctx context.Context, streamPath string) (subscriber *
|
||||
}
|
||||
|
||||
func (p *Plugin) Pull(streamPath string, conf config.Pull, pubConf *config.Publish) {
|
||||
puller := p.Meta.Puller(conf)
|
||||
puller := p.Meta.NewPuller(conf)
|
||||
if puller == nil {
|
||||
return
|
||||
}
|
||||
@@ -639,20 +622,20 @@ func (p *Plugin) Pull(streamPath string, conf config.Pull, pubConf *config.Publi
|
||||
}
|
||||
|
||||
func (p *Plugin) Push(streamPath string, conf config.Push, subConf *config.Subscribe) {
|
||||
pusher := p.Meta.Pusher()
|
||||
pusher := p.Meta.NewPusher()
|
||||
pusher.GetPushJob().Init(pusher, p, streamPath, conf, subConf)
|
||||
}
|
||||
|
||||
func (p *Plugin) Record(pub *Publisher, conf config.Record, subConf *config.Subscribe) *RecordJob {
|
||||
recorder := p.Meta.Recorder(conf)
|
||||
recorder := p.Meta.NewRecorder(conf)
|
||||
job := recorder.GetRecordJob().Init(recorder, p, pub.StreamPath, conf, subConf)
|
||||
job.Depend(pub)
|
||||
return job
|
||||
}
|
||||
|
||||
func (p *Plugin) Transform(pub *Publisher, conf config.Transform) {
|
||||
transformer := p.Meta.Transformer()
|
||||
job := transformer.GetTransformJob().Init(transformer, p, pub.StreamPath, conf)
|
||||
transformer := p.Meta.NewTransformer()
|
||||
job := transformer.GetTransformJob().Init(transformer, p, pub, conf)
|
||||
job.Depend(pub)
|
||||
}
|
||||
|
||||
@@ -732,35 +715,6 @@ func (p *Plugin) handle(pattern string, handler http.Handler) {
|
||||
p.Server.apiList = append(p.Server.apiList, pattern)
|
||||
}
|
||||
|
||||
func (p *Plugin) SaveConfig() (err error) {
|
||||
return Servers.AddTask(&SaveConfig{Plugin: p}).WaitStopped()
|
||||
}
|
||||
|
||||
type SaveConfig struct {
|
||||
task.Task
|
||||
Plugin *Plugin
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (s *SaveConfig) Start() (err error) {
|
||||
if s.Plugin.Modify == nil {
|
||||
err = os.Remove(s.Plugin.settingPath())
|
||||
if err == nil {
|
||||
err = task.ErrTaskComplete
|
||||
}
|
||||
}
|
||||
s.file, err = os.OpenFile(s.Plugin.settingPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SaveConfig) Run() (err error) {
|
||||
return yaml.NewEncoder(s.file).Encode(s.Plugin.Modify)
|
||||
}
|
||||
|
||||
func (s *SaveConfig) Dispose() {
|
||||
s.file.Close()
|
||||
}
|
||||
|
||||
func (p *Plugin) sendPublishWebhook(pub *Publisher) {
|
||||
if p.config.Hook == nil {
|
||||
return
|
||||
|
||||
@@ -24,7 +24,14 @@ type FLVPlugin struct {
|
||||
const defaultConfig m7s.DefaultYaml = `publish:
|
||||
speed: 1`
|
||||
|
||||
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPuller, NewRecorder, pb.RegisterApiServer, &pb.Api_ServiceDesc)
|
||||
var _ = m7s.InstallPlugin[FLVPlugin](m7s.PluginMeta{
|
||||
DefaultYaml: defaultConfig,
|
||||
NewPuller: NewPuller,
|
||||
NewRecorder: NewRecorder,
|
||||
RegisterGRPCHandler: pb.RegisterApiHandler,
|
||||
ServiceDesc: &pb.Api_ServiceDesc,
|
||||
NewPullProxy: m7s.NewHTTPPullPorxy,
|
||||
})
|
||||
|
||||
func (plugin *FLVPlugin) OnInit() (err error) {
|
||||
_, port, _ := strings.Cut(plugin.GetCommonConf().HTTP.ListenAddr, ":")
|
||||
@@ -96,10 +103,3 @@ func (plugin *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
err = live.Run()
|
||||
}
|
||||
|
||||
func (plugin *FLVPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any {
|
||||
d := &m7s.HTTPPullProxy{}
|
||||
d.PullProxy = pullProxy
|
||||
d.Plugin = &plugin.Plugin
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -175,10 +175,10 @@ func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
40
plugin/gb28181/catalogsub.go
Normal file
40
plugin/gb28181/catalogsub.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
// CatalogSubscribeTask 目录订阅任务
|
||||
type CatalogSubscribeTask struct {
|
||||
task.TickTask
|
||||
device *Device
|
||||
}
|
||||
|
||||
// NewCatalogSubscribeTask 创建新的目录订阅任务
|
||||
func NewCatalogSubscribeTask(device *Device) *CatalogSubscribeTask {
|
||||
return &CatalogSubscribeTask{
|
||||
device: device,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTickInterval 获取定时间隔
|
||||
func (c *CatalogSubscribeTask) GetTickInterval() time.Duration {
|
||||
// 如果设备配置了订阅周期,则使用设备配置的周期,否则使用默认值3600秒
|
||||
if c.device.SubscribeCatalog > 0 {
|
||||
return time.Second * time.Duration(c.device.SubscribeCatalog)
|
||||
}
|
||||
return time.Second * 3600
|
||||
}
|
||||
|
||||
// Tick 定时执行的方法
|
||||
func (c *CatalogSubscribeTask) Tick(any) {
|
||||
// 执行目录订阅
|
||||
response, err := c.device.subscribeCatalog()
|
||||
if err != nil {
|
||||
c.Error("subCatalog", "err", err)
|
||||
} else {
|
||||
c.Debug("subCatalog", "response", response.String())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package plugin_gb28181
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||||
)
|
||||
|
||||
type RecordRequest struct {
|
||||
SN, SumNum int
|
||||
Response []gb28181.Record
|
||||
SN, SumNum int
|
||||
ReceivedNum int // 已接收的记录数
|
||||
Response []gb28181.Message
|
||||
*util.Promise
|
||||
}
|
||||
|
||||
@@ -20,24 +23,61 @@ func (r *RecordRequest) GetKey() int {
|
||||
return r.SN
|
||||
}
|
||||
|
||||
// AddResponse 添加响应并检查是否完成
|
||||
func (r *RecordRequest) AddResponse(msg gb28181.Message) bool {
|
||||
r.Response = append(r.Response, msg)
|
||||
r.ReceivedNum += msg.RecordList.Num
|
||||
// 当接收到的记录数等于总数时,表示接收完成
|
||||
return r.ReceivedNum >= msg.SumNum
|
||||
}
|
||||
|
||||
// PresetRequest 预置位请求结构体
|
||||
type PresetRequest struct {
|
||||
SN int
|
||||
Response []gb28181.PresetItem
|
||||
*util.Promise
|
||||
}
|
||||
|
||||
func (r *PresetRequest) GetKey() int {
|
||||
return r.SN
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
PullProxyTask *PullProxy // 拉流任务
|
||||
Device *Device // 所属设备
|
||||
State atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放/对讲
|
||||
GpsTime time.Time // gps时间
|
||||
Longitude, Latitude string // 经度
|
||||
RecordReqs util.Collection[int, *RecordRequest]
|
||||
PresetReqs util.Collection[int, *PresetRequest] // 预置位请求集合
|
||||
*slog.Logger
|
||||
gb28181.ChannelInfo
|
||||
AbstractDevice *m7s.PullProxy
|
||||
gb28181.DeviceChannel
|
||||
}
|
||||
|
||||
func (c *Channel) GetKey() string {
|
||||
return c.DeviceID
|
||||
return c.ChannelID
|
||||
}
|
||||
|
||||
func (c *Channel) Pull() {
|
||||
pubConf := c.Device.plugin.GetCommonConf().Publish
|
||||
pubConf.PubAudio = c.AbstractDevice.Audio
|
||||
pubConf.DelayCloseTimeout = util.Conditional(c.AbstractDevice.StopOnIdle, time.Second*5, 0)
|
||||
c.Device.plugin.Pull(c.AbstractDevice.GetStreamPath(), c.AbstractDevice.Pull, &pubConf)
|
||||
type PullProxy struct {
|
||||
task.Task
|
||||
m7s.BasePullProxy
|
||||
}
|
||||
|
||||
func NewPullProxy() m7s.IPullProxy {
|
||||
return &PullProxy{}
|
||||
}
|
||||
|
||||
func (p *PullProxy) GetKey() uint {
|
||||
return p.PullProxyConfig.ID
|
||||
}
|
||||
|
||||
func (p *PullProxy) Start() error {
|
||||
streamPaths := strings.Split(p.GetStreamPath(), "/")
|
||||
deviceId, channelId := streamPaths[0], streamPaths[1]
|
||||
if device, ok := p.Plugin.GetHandler().(*GB28181Plugin).devices.Get(deviceId); ok {
|
||||
if _, ok := device.channels.Get(channelId); ok {
|
||||
p.ChangeStatus(m7s.PullProxyStatusOnline)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugin_gb28181
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -91,7 +91,6 @@ func (c *Client) Start() (err error) {
|
||||
cred, _ := digest.Digest(chal, digest.Options{
|
||||
Method: req.Method.String(),
|
||||
URI: c.recipient.Host,
|
||||
Username: c.conf.Username,
|
||||
Password: c.conf.Password,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package plugin_gb28181
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"m7s.live/v5"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||||
@@ -24,66 +32,352 @@ const (
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
task.Task `gorm:"-:all"`
|
||||
ID string `gorm:"primaryKey"`
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
UpdateTime time.Time
|
||||
LastKeepaliveAt time.Time
|
||||
task.Job `gorm:"-:all"`
|
||||
DeviceId string `gorm:"primaryKey"` // 设备国标编号
|
||||
Name string // 设备名
|
||||
Manufacturer string // 生产厂商
|
||||
Model string // 型号
|
||||
Firmware string // 固件版本
|
||||
Transport string // 传输协议(UDP/TCP)
|
||||
StreamMode string // 数据流传输模式(UDP:udp传输/TCP-ACTIVE:tcp主动模式/TCP-PASSIVE:tcp被动模式)
|
||||
IP string // wan地址_ip
|
||||
Port int // wan地址_port
|
||||
HostAddress string // wan地址
|
||||
Online bool // 是否在线,true为在线,false为离线
|
||||
RegisterTime time.Time // 注册时间
|
||||
KeepaliveTime time.Time // 心跳时间
|
||||
KeepaliveInterval int `gorm:"default:60"` // 心跳间隔
|
||||
ChannelCount int // 通道个数
|
||||
Expires int // 注册有效期
|
||||
CreateTime time.Time // 创建时间
|
||||
UpdateTime time.Time // 更新时间
|
||||
Charset string // 字符集, 支持 UTF-8 与 GB2312
|
||||
SubscribeCatalog int `gorm:"default:0"` // 目录订阅周期,0为不订阅
|
||||
SubscribePosition int `gorm:"default:0"` // 移动设备位置订阅周期,0为不订阅
|
||||
PositionInterval int // 移动设备位置信息上报时间间隔,单位:秒,默认值5
|
||||
SubscribeAlarm int // 报警订阅周期,0为不订阅
|
||||
SSRCCheck bool // 是否开启ssrc校验,默认关闭,开启可以防止串流
|
||||
GeoCoordSys string // 地理坐标系, 目前支持 WGS84,GCJ02
|
||||
Password string // 密码
|
||||
SipIp string // SIP交互IP(设备访问平台的IP)
|
||||
AsMessageChannel bool // 是否作为消息通道
|
||||
BroadcastPushAfterAck bool // 控制语音对讲流程,释放收到ACK后发流
|
||||
DeletedAt gorm.DeletedAt `yaml:"-"`
|
||||
// 删除强关联字段
|
||||
// channels []gb28181.DeviceChannel `gorm:"foreignKey:DeviceDBID;references:ID"` // 设备通道列表
|
||||
|
||||
// 保留原有字段
|
||||
Status DeviceStatus
|
||||
SN int
|
||||
Recipient sip.Uri `gorm:"-:all"`
|
||||
Transport string
|
||||
channels util.Collection[string, *Channel]
|
||||
mediaIp string
|
||||
GpsTime time.Time //gps时间
|
||||
Longitude, Latitude string //经度,纬度
|
||||
Recipient sip.Uri `gorm:"-:all"`
|
||||
channels util.Collection[string, *Channel] `gorm:"-:all"`
|
||||
catalogReqs util.Collection[int, *CatalogRequest] `gorm:"-:all"`
|
||||
MediaIp string `desc:"收流IP"`
|
||||
Longitude, Latitude string // 经度,纬度
|
||||
eventChan chan any
|
||||
client *sipgo.Client
|
||||
dialogClient *sipgo.DialogClient
|
||||
contactHDR sip.ContactHeader
|
||||
fromHDR sip.FromHeader
|
||||
toHDR sip.ToHeader
|
||||
plugin *GB28181Plugin
|
||||
localPort int
|
||||
}
|
||||
|
||||
func (d *Device) TableName() string {
|
||||
return "device_gb28181"
|
||||
return "gb28181_device"
|
||||
}
|
||||
|
||||
func (d *Device) Dispose() {
|
||||
if d.plugin.DB != nil {
|
||||
d.plugin.DB.Save(d)
|
||||
if d.channels.Length > 0 {
|
||||
d.channels.Range(func(channel *Channel) bool {
|
||||
d.plugin.DB.Save(channel.DeviceChannel)
|
||||
//d.plugin.DB.Model(&gb28181.DeviceChannel{}).Where("device_id = ? AND device_db_id = ?", channel.DeviceId, d.ID).Updates(channel.DeviceChannel)
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
// 如果没有通道,则直接更新通道状态为 OFF
|
||||
d.plugin.DB.Model(&gb28181.DeviceChannel{}).Where("device_db_id = ?", d.ID).Update("status", "OFF")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) GetKey() string {
|
||||
return d.ID
|
||||
return d.DeviceId
|
||||
}
|
||||
|
||||
// CatalogRequest 目录请求结构体
|
||||
type CatalogRequest struct {
|
||||
SN, SumNum int
|
||||
FirstResponse bool // 是否为第一个响应
|
||||
*util.Promise
|
||||
sync.Mutex // 保护并发访问
|
||||
}
|
||||
|
||||
func (r *CatalogRequest) GetKey() int {
|
||||
return r.SN
|
||||
}
|
||||
|
||||
// AddResponse 处理响应并返回是否是第一个响应
|
||||
func (r *CatalogRequest) AddResponse() bool {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
wasFirst := r.FirstResponse
|
||||
r.FirstResponse = false
|
||||
return wasFirst
|
||||
}
|
||||
|
||||
// IsComplete 检查是否完成接收
|
||||
func (r *CatalogRequest) IsComplete(channelsLength int) bool {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return channelsLength >= r.SumNum
|
||||
}
|
||||
|
||||
func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) (err error) {
|
||||
var body []byte
|
||||
if d.Status == DeviceRecoverStatus {
|
||||
d.Status = DeviceOnlineStatus
|
||||
source := req.Source()
|
||||
hostname, portStr, _ := net.SplitHostPort(source)
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
if d.IP != hostname || d.Port != port {
|
||||
d.Recipient.Host = hostname
|
||||
d.Recipient.Port = port
|
||||
}
|
||||
d.Debug("OnMessage", "cmdType", msg.CmdType, "body", string(req.Body()))
|
||||
d.IP = hostname
|
||||
d.Port = port
|
||||
d.HostAddress = hostname + ":" + portStr
|
||||
var body []byte
|
||||
//d.Online = true
|
||||
//if d.Status != DeviceOnlineStatus {
|
||||
// d.Status = DeviceOnlineStatus
|
||||
//}
|
||||
//d.Debug("OnMessage", "cmdType", msg.CmdType, "body", string(req.Body()))
|
||||
switch msg.CmdType {
|
||||
case "Keepalive":
|
||||
d.LastKeepaliveAt = time.Now()
|
||||
d.KeepaliveInterval = int(time.Since(d.KeepaliveTime).Seconds())
|
||||
d.KeepaliveTime = time.Now()
|
||||
if d.plugin.DB != nil {
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"keepalive_interval": d.KeepaliveInterval,
|
||||
"keepalive_time": d.KeepaliveTime,
|
||||
}).Error; err != nil {
|
||||
d.Error("update keepalive info failed", "error", err)
|
||||
}
|
||||
}
|
||||
case "Catalog":
|
||||
d.eventChan <- msg.DeviceList
|
||||
// 处理目录信息
|
||||
catalogReq, exists := d.catalogReqs.Get(msg.SN)
|
||||
if !exists {
|
||||
// 创建新的目录请求
|
||||
catalogReq = &CatalogRequest{
|
||||
SN: msg.SN,
|
||||
SumNum: msg.SumNum,
|
||||
FirstResponse: true,
|
||||
Promise: util.NewPromise(context.Background()),
|
||||
}
|
||||
d.catalogReqs.Set(catalogReq)
|
||||
}
|
||||
|
||||
// 添加响应并获取是否是第一个响应
|
||||
isFirst := catalogReq.AddResponse()
|
||||
|
||||
// 更新设备信息到数据库
|
||||
if d.plugin.DB != nil {
|
||||
// 如果是第一个响应,先清空现有通道
|
||||
if isFirst {
|
||||
d.Debug("清空现有通道", "deviceId", d.DeviceId)
|
||||
if err := d.plugin.DB.Where("device_id = ?", d.DeviceId).Delete(&gb28181.DeviceChannel{}).Error; err != nil {
|
||||
d.Error("删除通道失败", "error", err, "deviceId", d.DeviceId)
|
||||
}
|
||||
// 清空内存中的通道缓存
|
||||
d.channels.Clear()
|
||||
}
|
||||
|
||||
// 更新通道信息
|
||||
for _, c := range msg.DeviceChannelList {
|
||||
// 设置关联的设备数据库ID
|
||||
c.ChannelID = c.DeviceID
|
||||
c.DeviceID = d.DeviceId
|
||||
c.ID = d.DeviceId + "_" + c.ChannelID
|
||||
// 使用 Save 进行 upsert 操作
|
||||
if err := d.plugin.DB.Save(&c).Error; err != nil {
|
||||
d.Error("save channel failed", "error", err)
|
||||
}
|
||||
d.addOrUpdateChannel(c)
|
||||
}
|
||||
|
||||
// 更新当前设备的通道数
|
||||
d.ChannelCount = msg.SumNum
|
||||
d.UpdateTime = time.Now()
|
||||
d.Debug("save channel", "deviceid", d.DeviceId, "channels count", d.channels.Length)
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"channel_count": d.ChannelCount,
|
||||
"update_time": d.UpdateTime,
|
||||
}).Error; err != nil {
|
||||
d.Error("save device failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 在所有通道都添加完成后,检查是否完成接收
|
||||
if catalogReq.IsComplete(d.channels.Length) {
|
||||
catalogReq.Resolve()
|
||||
d.catalogReqs.RemoveByKey(msg.SN)
|
||||
}
|
||||
case "RecordInfo":
|
||||
if channel, ok := d.channels.Get(msg.DeviceID); ok {
|
||||
if req, ok := channel.RecordReqs.Get(msg.SN); ok {
|
||||
req.Response = msg.RecordList
|
||||
// 添加响应并检查是否完成
|
||||
if req.AddResponse(*msg) {
|
||||
req.Resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
case "PresetQuery":
|
||||
if channel, ok := d.channels.Get(msg.DeviceID); ok {
|
||||
if req, ok := channel.PresetReqs.Get(msg.SN); ok {
|
||||
// 添加预置位响应
|
||||
req.Response = msg.PresetList.Item
|
||||
req.Resolve()
|
||||
}
|
||||
}
|
||||
// 查询平台信息
|
||||
type Result struct {
|
||||
PlatformServerGBID string `gorm:"column:platform_server_gb_id"`
|
||||
}
|
||||
var result Result
|
||||
if d.plugin.DB != nil {
|
||||
if err := d.plugin.DB.Table("gb28181_platform_channel gpc").
|
||||
Select("gpc.platform_server_gb_id").
|
||||
Joins("LEFT JOIN gb28181_channel gc on gpc.channel_db_id= gc.id").
|
||||
Where("gc.channel_id = ?", msg.DeviceID).
|
||||
First(&result).Error; err != nil {
|
||||
d.Error("查询平台信息失败", "error", err)
|
||||
return err
|
||||
}
|
||||
// 从platforms集合中获取平台实例
|
||||
if platform, ok := d.plugin.platforms.Get(result.PlatformServerGBID); ok {
|
||||
// 创建并发送响应消息
|
||||
request := platform.CreateRequest("MESSAGE")
|
||||
fromTag, _ := req.From().Params.Get("tag")
|
||||
// 设置From头部
|
||||
fromHeader := sip.FromHeader{
|
||||
Address: sip.Uri{
|
||||
User: platform.PlatformModel.DeviceGBID,
|
||||
Host: platform.PlatformModel.ServerGBDomain,
|
||||
},
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
fromHeader.Params.Add("tag", fromTag)
|
||||
request.AppendHeader(&fromHeader)
|
||||
|
||||
// 添加To头部
|
||||
toHeader := sip.ToHeader{
|
||||
Address: sip.Uri{
|
||||
User: platform.PlatformModel.ServerGBID,
|
||||
Host: platform.PlatformModel.ServerGBDomain,
|
||||
},
|
||||
}
|
||||
request.AppendHeader(&toHeader)
|
||||
|
||||
// 添加Via头部
|
||||
viaHeader := sip.ViaHeader{
|
||||
ProtocolName: "SIP",
|
||||
ProtocolVersion: "2.0",
|
||||
Transport: platform.PlatformModel.Transport,
|
||||
Host: platform.PlatformModel.DeviceIP,
|
||||
Port: platform.PlatformModel.DevicePort,
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||||
request.AppendHeader(&viaHeader)
|
||||
|
||||
// 设置Content-Type
|
||||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||||
request.AppendHeader(&contentTypeHeader)
|
||||
|
||||
// 直接使用原始消息体
|
||||
request.SetBody(req.Body())
|
||||
|
||||
// 发送请求
|
||||
_, err = platform.Client.Do(platform.ctx, request)
|
||||
if err != nil {
|
||||
d.Error("发送预置位查询响应失败", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case "DeviceStatus":
|
||||
if d.plugin.DB != nil {
|
||||
d.UpdateTime = time.Now()
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"update_time": d.UpdateTime,
|
||||
"longitude": msg.Longitude,
|
||||
"latitude": msg.Latitude,
|
||||
}).Error; err != nil {
|
||||
d.Error("save device status failed", "error", err)
|
||||
}
|
||||
}
|
||||
case "DeviceInfo":
|
||||
// 主设备信息
|
||||
d.Name = msg.DeviceName
|
||||
d.Manufacturer = msg.Manufacturer
|
||||
d.Model = msg.Model
|
||||
d.Firmware = msg.Firmware
|
||||
d.UpdateTime = time.Now()
|
||||
d.Latitude = msg.Latitude
|
||||
d.Longitude = msg.Longitude
|
||||
// 更新设备信息到数据库
|
||||
if d.plugin.DB != nil {
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"name": d.Name,
|
||||
"manufacturer": d.Manufacturer,
|
||||
"model": d.Model,
|
||||
"firmware": d.Firmware,
|
||||
"update_time": d.UpdateTime,
|
||||
"longitude": d.Longitude,
|
||||
"latitude": d.Latitude,
|
||||
}).Error; err != nil {
|
||||
d.Error("save device info failed", "error", err)
|
||||
}
|
||||
}
|
||||
case "Alarm":
|
||||
d.Status = DeviceAlarmedStatus
|
||||
body = []byte(gb28181.BuildAlarmResponseXML(d.ID))
|
||||
// 创建报警记录
|
||||
alarm := &gb28181.DeviceAlarm{
|
||||
DeviceID: d.DeviceId, // 使用当前设备的ID
|
||||
DeviceName: d.Name,
|
||||
ChannelID: msg.DeviceID, // 使用消息中的DeviceID作为通道ID
|
||||
AlarmPriority: msg.AlarmPriority,
|
||||
AlarmMethod: msg.AlarmMethod,
|
||||
AlarmType: msg.Info.AlarmType,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 尝试解析报警时间
|
||||
if alarmTime, err := time.Parse("2006-01-02T15:04:05", msg.AlarmTime); err == nil {
|
||||
alarm.AlarmTime = alarmTime
|
||||
} else {
|
||||
alarm.AlarmTime = time.Now()
|
||||
d.Error("解析报警时间失败", "error", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if d.plugin.DB != nil {
|
||||
if err := d.plugin.DB.Create(alarm).Error; err != nil {
|
||||
d.Error("保存报警信息失败", "error", err)
|
||||
} else {
|
||||
d.Info("保存报警信息成功",
|
||||
"deviceId", alarm.DeviceID,
|
||||
"channelId", alarm.ChannelID,
|
||||
"alarmType", alarm.GetAlarmTypeDescription(),
|
||||
"alarmMethod", alarm.GetAlarmMethodDescription(),
|
||||
"alarmPriority", alarm.GetAlarmPriorityDescription())
|
||||
}
|
||||
}
|
||||
case "Broadcast":
|
||||
d.Info("broadcast message", "body", req.Body())
|
||||
d.Info("Broadcast message", "body", req.Body())
|
||||
case "DeviceControl":
|
||||
d.Info("DeviceControl message", "body", req.Body())
|
||||
default:
|
||||
d.Warn("Not supported CmdType", "CmdType", msg.CmdType, "body", req.Body())
|
||||
err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil))
|
||||
@@ -96,128 +390,307 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
func (d *Device) send(req *sip.Request) (*sip.Response, error) {
|
||||
d.SN++
|
||||
d.Debug("send", "req", req.String())
|
||||
return d.client.Do(d, req)
|
||||
return d.client.Do(context.Background(), req)
|
||||
}
|
||||
|
||||
func (d *Device) Go() (err error) {
|
||||
var response *sip.Response
|
||||
|
||||
// 初始化catalogReqs
|
||||
d.catalogReqs.L = new(sync.RWMutex)
|
||||
|
||||
response, err = d.queryDeviceInfo()
|
||||
if err != nil {
|
||||
d.Error("queryDeviceInfo", "err", err)
|
||||
}
|
||||
response, err = d.queryDeviceStatus()
|
||||
if err != nil {
|
||||
d.Error("queryDeviceStatus", "err", err)
|
||||
}
|
||||
response, err = d.catalog()
|
||||
if err != nil {
|
||||
d.Error("catalog", "err", err)
|
||||
} else {
|
||||
d.Debug("catalog", "response", response.String())
|
||||
}
|
||||
response, err = d.queryDeviceInfo()
|
||||
if err != nil {
|
||||
d.Error("deviceInfo", "err", err)
|
||||
} else {
|
||||
d.Debug("deviceInfo", "response", response.String())
|
||||
|
||||
// 创建并启动目录订阅任务
|
||||
if d.SubscribeCatalog > 0 {
|
||||
catalogSubTask := NewCatalogSubscribeTask(d)
|
||||
d.AddTask(catalogSubTask)
|
||||
}
|
||||
subTick := time.NewTicker(time.Second * 3600)
|
||||
defer subTick.Stop()
|
||||
catalogTick := time.NewTicker(time.Second * 60)
|
||||
|
||||
// 创建并启动位置订阅任务
|
||||
if d.SubscribePosition > 0 {
|
||||
positionSubTask := NewPositionSubscribeTask(d)
|
||||
d.AddTask(positionSubTask)
|
||||
}
|
||||
|
||||
catalogTick := time.NewTicker(time.Minute * 10)
|
||||
keepaliveSeconds := 60
|
||||
if d.KeepaliveInterval >= 5 {
|
||||
keepaliveSeconds = d.KeepaliveInterval
|
||||
}
|
||||
keepLiveTick := time.NewTicker(time.Second * 10)
|
||||
defer keepLiveTick.Stop()
|
||||
defer catalogTick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-d.Done():
|
||||
case <-subTick.C:
|
||||
response, err = d.subscribeCatalog()
|
||||
if err != nil {
|
||||
d.Error("subCatalog", "err", err)
|
||||
} else {
|
||||
d.Debug("subCatalog", "response", response.String())
|
||||
}
|
||||
response, err = d.subscribePosition(int(6))
|
||||
if err != nil {
|
||||
d.Error("subPosition", "err", err)
|
||||
} else {
|
||||
d.Debug("subPosition", "response", response.String())
|
||||
case <-keepLiveTick.C:
|
||||
if timeDiff := time.Since(d.KeepaliveTime); timeDiff > time.Duration(3*keepaliveSeconds)*time.Second {
|
||||
d.Online = false
|
||||
d.Status = DeviceOfflineStatus
|
||||
// 设置所有通道状态为off
|
||||
d.channels.Range(func(channel *Channel) bool {
|
||||
channel.Status = "OFF"
|
||||
return true
|
||||
})
|
||||
d.Stop(fmt.Errorf("device keepalive timeout after %v", timeDiff))
|
||||
return
|
||||
}
|
||||
case <-catalogTick.C:
|
||||
if time.Since(d.LastKeepaliveAt) > time.Second*3600 {
|
||||
d.Error("keepalive timeout", "lastKeepaliveAt", d.LastKeepaliveAt)
|
||||
if time.Since(d.KeepaliveTime) > time.Second*time.Duration(d.Expires) {
|
||||
d.Error("keepalive timeout", "keepaliveTime", d.KeepaliveTime)
|
||||
return
|
||||
}
|
||||
response, err = d.catalog()
|
||||
if err != nil {
|
||||
d.Error("catalog", "err", err)
|
||||
} else {
|
||||
d.Debug("catalog", "response", response.String())
|
||||
}
|
||||
case event := <-d.eventChan:
|
||||
switch v := event.(type) {
|
||||
case []gb28181.ChannelInfo:
|
||||
for _, c := range v {
|
||||
//当父设备非空且存在时、父设备节点增加通道
|
||||
if c.ParentID != "" {
|
||||
path := strings.Split(c.ParentID, "/")
|
||||
parentId := path[len(path)-1]
|
||||
//如果父ID并非本身所属设备,一般情况下这是因为下级设备上传了目录信息,该信息通常不需要处理。
|
||||
// 暂时不考虑级联目录的实现
|
||||
if d.ID != parentId {
|
||||
if parent, ok := d.plugin.devices.Get(parentId); ok {
|
||||
parent.addOrUpdateChannel(c)
|
||||
continue
|
||||
} else {
|
||||
c.Model = "Directory " + c.Model
|
||||
c.Status = "NoParent"
|
||||
}
|
||||
}
|
||||
}
|
||||
d.addOrUpdateChannel(c)
|
||||
}
|
||||
d.Debug("catalogTick", "response", response.String())
|
||||
}
|
||||
//case event := <-d.eventChan:
|
||||
// d.Debug("eventChan", "event", event)
|
||||
// switch v := event.(type) {
|
||||
// case []gb28181.DeviceChannel:
|
||||
// for _, c := range v {
|
||||
// //当父设备非空且存在时、父设备节点增加通道
|
||||
// if c.ParentID != "" {
|
||||
// path := strings.Split(c.ParentID, "/")
|
||||
// parentId := path[len(path)-1]
|
||||
// //如果父ID并非本身所属设备,一般情况下这是因为下级设备上传了目录信息,该信息通常不需要处理。
|
||||
// // 暂时不考虑级联目录的实现
|
||||
// if d.DeviceId != parentId {
|
||||
// if parent, ok := d.plugin.devices.Get(parentId); ok {
|
||||
// parent.addOrUpdateChannel(c)
|
||||
// continue
|
||||
// } else {
|
||||
// c.Model = "Directory " + c.Model
|
||||
// c.Status = "NoParent"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// d.addOrUpdateChannel(c)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) createRequest(Method sip.RequestMethod) (req *sip.Request) {
|
||||
req = sip.NewRequest(Method, d.Recipient)
|
||||
req.AppendHeader(&d.fromHDR)
|
||||
func (d *Device) CreateRequest(Method sip.RequestMethod, Recipient any) *sip.Request {
|
||||
var req *sip.Request
|
||||
if recipient, ok := Recipient.(sip.Uri); ok {
|
||||
req = sip.NewRequest(Method, recipient)
|
||||
} else {
|
||||
req = sip.NewRequest(Method, d.Recipient)
|
||||
}
|
||||
fromHDR := d.fromHDR
|
||||
fromHDR.Params.Add("tag", sip.GenerateTagN(32))
|
||||
req.AppendHeader(&fromHDR)
|
||||
contentType := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||||
req.AppendHeader(sip.NewHeader("User-Agent", "M7S/"+m7s.Version))
|
||||
req.AppendHeader(&contentType)
|
||||
req.AppendHeader(&d.contactHDR)
|
||||
return
|
||||
toHeader := sip.ToHeader{
|
||||
Address: sip.Uri{User: d.DeviceId, Host: d.HostAddress},
|
||||
}
|
||||
req.AppendHeader(&toHeader)
|
||||
//viaHeader := sip.ViaHeader{
|
||||
// ProtocolName: "SIP",
|
||||
// ProtocolVersion: "2.0",
|
||||
// Transport: "UDP",
|
||||
// Host: d.SipIp,
|
||||
// Port: d.localPort,
|
||||
// Params: sip.HeaderParams(sip.NewParams()),
|
||||
//}
|
||||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(10)).Add("rport", "")
|
||||
//req.AppendHeader(&viaHeader)
|
||||
//req.AppendHeader(&d.contactHDR)
|
||||
return req
|
||||
}
|
||||
|
||||
func (d *Device) catalog() (*sip.Response, error) {
|
||||
request := d.createRequest(sip.MESSAGE)
|
||||
request := d.CreateRequest(sip.MESSAGE, nil)
|
||||
//d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
|
||||
request.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||
request.SetBody(gb28181.BuildCatalogXML(d.SN, d.ID))
|
||||
request.SetBody(gb28181.BuildCatalogXML(d.Charset, d.SN, d.DeviceId))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
func (d *Device) subscribeCatalog() (*sip.Response, error) {
|
||||
request := d.createRequest(sip.SUBSCRIBE)
|
||||
request.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||
request.SetBody(gb28181.BuildCatalogXML(d.SN, d.ID))
|
||||
request := d.CreateRequest(sip.SUBSCRIBE, nil)
|
||||
request.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(d.SubscribeCatalog)))
|
||||
request.SetBody(gb28181.BuildCatalogXML(d.Charset, d.SN, d.DeviceId))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
func (d *Device) queryDeviceInfo() (*sip.Response, error) {
|
||||
request := d.createRequest(sip.MESSAGE)
|
||||
request.SetBody(gb28181.BuildDeviceInfoXML(d.SN, d.ID))
|
||||
request := d.CreateRequest(sip.MESSAGE, nil)
|
||||
request.SetBody(gb28181.BuildDeviceInfoXML(d.SN, d.DeviceId, d.Charset))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
func (d *Device) queryDeviceStatus() (*sip.Response, error) {
|
||||
request := d.CreateRequest(sip.MESSAGE, nil)
|
||||
request.SetBody(gb28181.BuildDeviceStatusXML(d.SN, d.DeviceId, d.Charset))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
func (d *Device) subscribePosition(interval int) (*sip.Response, error) {
|
||||
request := d.createRequest(sip.SUBSCRIBE)
|
||||
request.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||
request.SetBody(gb28181.BuildDevicePositionXML(d.SN, d.ID, interval))
|
||||
request := d.CreateRequest(sip.SUBSCRIBE, nil)
|
||||
request.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(d.SubscribePosition)))
|
||||
request.SetBody(gb28181.BuildDevicePositionXML(d.SN, d.DeviceId, interval))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
func (d *Device) addOrUpdateChannel(c gb28181.ChannelInfo) {
|
||||
if channel, ok := d.channels.Get(c.DeviceID); ok {
|
||||
channel.ChannelInfo = c
|
||||
// frontEndCmd 前端控制命令,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
|
||||
func (d *Device) frontEndCmd(channelId string, cmdStr string) (*sip.Response, error) {
|
||||
// 构建前端控制指令字符串
|
||||
//cmdStr := d.frontEndCmdString(cmdCode, parameter1, parameter2, combineCode2)
|
||||
|
||||
// 构建XML消息体
|
||||
ptzXml := strings.Builder{}
|
||||
ptzXml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
|
||||
ptzXml.WriteString("<Control>\r\n")
|
||||
ptzXml.WriteString("<CmdType>DeviceControl</CmdType>\r\n")
|
||||
ptzXml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", int(time.Now().UnixNano()/1e6%1000000)))
|
||||
ptzXml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelId))
|
||||
ptzXml.WriteString(fmt.Sprintf("<PTZCmd>%s</PTZCmd>\r\n", cmdStr))
|
||||
ptzXml.WriteString("<Info>\r\n")
|
||||
ptzXml.WriteString("<ControlPriority>5</ControlPriority>\r\n")
|
||||
ptzXml.WriteString("</Info>\r\n")
|
||||
ptzXml.WriteString("</Control>\r\n")
|
||||
|
||||
// 创建并发送请求
|
||||
request := d.CreateRequest(sip.MESSAGE, nil)
|
||||
request.SetBody([]byte(ptzXml.String()))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
// frontEndCmdString 生成前端控制指令字符串
|
||||
func (d *Device) frontEndCmdString(cmdCode int32, parameter1 int32, parameter2 int32, combineCode2 int32) string {
|
||||
// 构建指令字符串
|
||||
var builder strings.Builder
|
||||
builder.WriteString("A50F01")
|
||||
|
||||
// 添加指令码
|
||||
builder.WriteString(fmt.Sprintf("%02X", cmdCode))
|
||||
|
||||
// 添加参数1
|
||||
builder.WriteString(fmt.Sprintf("%02X", parameter1))
|
||||
|
||||
// 添加参数2
|
||||
builder.WriteString(fmt.Sprintf("%02X", parameter2))
|
||||
|
||||
// 添加组合码2(左移4位)
|
||||
builder.WriteString(fmt.Sprintf("%02X", combineCode2<<4))
|
||||
|
||||
// 计算校验码
|
||||
checkCode := (0xA5 + 0x0F + 0x01 + int(cmdCode) + int(parameter1) + int(parameter2) + int(combineCode2<<4)) % 0x100
|
||||
builder.WriteString(fmt.Sprintf("%02X", checkCode))
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (d *Device) addOrUpdateChannel(c gb28181.DeviceChannel) {
|
||||
if channel, ok := d.channels.Get(c.ChannelID); ok {
|
||||
channel.DeviceChannel = c
|
||||
} else {
|
||||
channel = &Channel{
|
||||
Device: d,
|
||||
Logger: d.Logger.With("channel", c.DeviceID),
|
||||
ChannelInfo: c,
|
||||
Device: d,
|
||||
Logger: d.Logger.With("channel", c.ChannelID),
|
||||
DeviceChannel: c,
|
||||
}
|
||||
d.channels.Set(channel)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) GetID() string {
|
||||
return d.DeviceId
|
||||
}
|
||||
|
||||
func (d *Device) GetIP() string {
|
||||
return d.IP
|
||||
}
|
||||
|
||||
func (d *Device) GetStreamMode() string {
|
||||
return d.StreamMode
|
||||
}
|
||||
|
||||
func (d *Device) Send(req *sip.Request) (*sip.Response, error) {
|
||||
return d.send(req)
|
||||
}
|
||||
|
||||
func (d *Device) CreateSSRC(serial string) uint16 {
|
||||
// 使用简单的 hash 函数将设备 ID 转换为 uint16
|
||||
var hash uint16
|
||||
for i := 0; i < len(d.DeviceId); i++ {
|
||||
hash = hash*31 + uint16(d.DeviceId[i])
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// recordCmd 录制控制命令
|
||||
func (d *Device) recordCmd(channelId string, cmdType string) (*sip.Response, error) {
|
||||
// 构建XML消息体
|
||||
recordXml := strings.Builder{}
|
||||
recordXml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
|
||||
recordXml.WriteString("<Control>\r\n")
|
||||
recordXml.WriteString("<CmdType>DeviceControl</CmdType>\r\n")
|
||||
recordXml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", int(time.Now().UnixNano()/1e6%1000000)))
|
||||
recordXml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelId))
|
||||
recordXml.WriteString(fmt.Sprintf("<RecordCmd>%s</RecordCmd>\r\n", cmdType))
|
||||
recordXml.WriteString("</Control>\r\n")
|
||||
|
||||
// 创建并发送请求
|
||||
request := d.CreateRequest(sip.MESSAGE, nil)
|
||||
request.SetBody([]byte(recordXml.String()))
|
||||
return d.send(request)
|
||||
}
|
||||
|
||||
// SnapshotConfig 抓拍配置结构体
|
||||
type SnapshotConfig struct {
|
||||
SnapNum int `json:"snapNum"` // 连拍张数(1-10张)
|
||||
Interval int `json:"interval"` // 单张抓拍间隔(单位:秒,最小1秒)
|
||||
UploadURL string `json:"uploadUrl"` // 抓拍图片上传路径
|
||||
SessionID string `json:"sessionId"` // 会话ID,用于标识抓拍会话
|
||||
}
|
||||
|
||||
// BuildSnapshotConfigXML 生成抓拍配置XML
|
||||
func (d *Device) BuildSnapshotConfigXML(config SnapshotConfig, channelID string) string {
|
||||
// 参数验证和限制
|
||||
if config.SnapNum < 1 {
|
||||
config.SnapNum = 1
|
||||
} else if config.SnapNum > 10 {
|
||||
config.SnapNum = 10
|
||||
}
|
||||
if config.Interval < 1 {
|
||||
config.Interval = 1
|
||||
}
|
||||
|
||||
xml := strings.Builder{}
|
||||
xml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
|
||||
xml.WriteString("<Control>\r\n")
|
||||
xml.WriteString("<CmdType>DeviceConfig</CmdType>\r\n")
|
||||
xml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", d.SN))
|
||||
xml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelID))
|
||||
xml.WriteString("<SnapShotConfig>\r\n")
|
||||
xml.WriteString(fmt.Sprintf("<SnapNum>%d</SnapNum>\r\n", config.SnapNum))
|
||||
xml.WriteString(fmt.Sprintf("<Interval>%d</Interval>\r\n", config.Interval))
|
||||
xml.WriteString(fmt.Sprintf("<UploadURL>%s</UploadURL>\r\n", config.UploadURL))
|
||||
xml.WriteString(fmt.Sprintf("<SessionID>%s</SessionID>\r\n", config.SessionID))
|
||||
xml.WriteString("</SnapShotConfig>\r\n")
|
||||
xml.WriteString("</Control>\r\n")
|
||||
|
||||
return xml.String()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package plugin_gb28181
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"m7s.live/v5/pkg/util"
|
||||
|
||||
sipgo "github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
m7s "m7s.live/v5"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||||
)
|
||||
|
||||
@@ -17,9 +22,14 @@ type Dialog struct {
|
||||
task.Job
|
||||
Channel *Channel
|
||||
gb28181.InviteOptions
|
||||
gb *GB28181Plugin
|
||||
session *sipgo.DialogClientSession
|
||||
pullCtx m7s.PullJob
|
||||
gb *GB28181Plugin
|
||||
session *sipgo.DialogClientSession
|
||||
pullCtx m7s.PullJob
|
||||
start string
|
||||
end string
|
||||
StreamMode string // 数据流传输模式(UDP:udp传输/TCP-ACTIVE:tcp主动模式/TCP-PASSIVE:tcp被动模式)
|
||||
targetIP string // 目标设备的IP地址
|
||||
targetPort int // 目标设备的端口
|
||||
}
|
||||
|
||||
func (d *Dialog) GetCallID() string {
|
||||
@@ -30,8 +40,23 @@ func (d *Dialog) GetPullJob() *m7s.PullJob {
|
||||
return &d.pullCtx
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
func GenerateCallID(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (d *Dialog) Start() (err error) {
|
||||
if !d.IsLive() {
|
||||
// 处理时间范围
|
||||
d.InviteOptions.Start = d.start
|
||||
d.InviteOptions.End = d.End
|
||||
if d.IsLive() {
|
||||
d.pullCtx.PublishConfig.PubType = m7s.PublishTypeVod
|
||||
}
|
||||
err = d.pullCtx.Publish()
|
||||
@@ -39,59 +64,172 @@ func (d *Dialog) Start() (err error) {
|
||||
return
|
||||
}
|
||||
sss := strings.Split(d.pullCtx.RemoteURL, "/")
|
||||
deviceId, channelId := sss[0], sss[1]
|
||||
if len(sss) == 2 {
|
||||
if device, ok := d.gb.devices.Get(deviceId); ok {
|
||||
if channel, ok := device.channels.Get(channelId); ok {
|
||||
d.Channel = channel
|
||||
} else {
|
||||
return fmt.Errorf("channel %s not found", channelId)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("device %s not found", deviceId)
|
||||
}
|
||||
} else if len(sss) == 3 {
|
||||
var recordRange util.Range[int]
|
||||
err = recordRange.Resolve(sss[2])
|
||||
if len(sss) < 2 {
|
||||
d.Info("remote url is invalid", d.pullCtx.RemoteURL)
|
||||
return
|
||||
}
|
||||
ssrc := d.CreateSSRC(d.gb.Serial)
|
||||
deviceId, channelId := sss[len(sss)-2], sss[len(sss)-1]
|
||||
var device *Device
|
||||
if deviceTmp, ok := d.gb.devices.Get(deviceId); ok {
|
||||
device = deviceTmp
|
||||
if channel, ok := deviceTmp.channels.Get(channelId); ok {
|
||||
d.Channel = channel
|
||||
d.StreamMode = device.StreamMode
|
||||
} else {
|
||||
return fmt.Errorf("channel %s not found", channelId)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("device %s not found", deviceId)
|
||||
}
|
||||
|
||||
d.gb.dialogs.Set(d)
|
||||
defer d.gb.dialogs.Remove(d)
|
||||
//defer d.gb.dialogs.Remove(d)
|
||||
if d.gb.MediaPort.Valid() {
|
||||
select {
|
||||
case d.MediaPort = <-d.gb.tcpPorts:
|
||||
defer func() {
|
||||
d.gb.tcpPorts <- d.MediaPort
|
||||
}()
|
||||
default:
|
||||
return fmt.Errorf("no available tcp port")
|
||||
}
|
||||
} else {
|
||||
d.MediaPort = d.gb.MediaPort[0]
|
||||
}
|
||||
ssrc := d.CreateSSRC(d.gb.Serial)
|
||||
d.Info("MediaIp is ", device.MediaIp)
|
||||
|
||||
// 构建 SDP 内容
|
||||
sdpInfo := []string{
|
||||
"v=0",
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", d.Channel.DeviceID, d.Channel.Device.mediaIp),
|
||||
"s=" + util.Conditional(d.IsLive(), "Play", "Playback"),
|
||||
"u=" + d.Channel.DeviceID + ":0",
|
||||
"c=IN IP4 " + d.Channel.Device.mediaIp,
|
||||
d.String(),
|
||||
fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort),
|
||||
"a=recvonly",
|
||||
"a=rtpmap:96 PS/90000",
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
"y=" + ssrc,
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channelId, device.MediaIp),
|
||||
fmt.Sprintf("s=%s", util.Conditional(d.IsLive(), "Play", "Playback")), // 根据是否有时间参数决定
|
||||
}
|
||||
contentTypeHeader := sip.ContentTypeHeader("application/sdp")
|
||||
fromHeader := d.Channel.Device.fromHDR
|
||||
subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", d.Channel.DeviceID, ssrc, d.gb.Serial))
|
||||
d.session, err = d.Channel.Device.dialogClient.Invite(d.gb, d.Channel.Device.Recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &contentTypeHeader, subjectHeader, &fromHeader, sip.NewHeader("Allow", "INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY, BYE"))
|
||||
|
||||
// 非直播模式下添加u行,保持在s=和c=之间
|
||||
//if !d.IsLive() {
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
|
||||
//}
|
||||
|
||||
// 添加c行
|
||||
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
|
||||
|
||||
// 将字符串时间转换为 Unix 时间戳
|
||||
if !d.IsLive() {
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"start": []string{d.start}, "end": []string{d.end}})
|
||||
if err != nil {
|
||||
d.Stop(errors.New("parse end time error"))
|
||||
}
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", startTime.Unix(), endTime.Unix()))
|
||||
} else {
|
||||
sdpInfo = append(sdpInfo, "t=0 0")
|
||||
}
|
||||
|
||||
// 添加媒体行和相关属性
|
||||
var mediaLine string
|
||||
switch strings.ToUpper(device.StreamMode) {
|
||||
case "TCP-PASSIVE", "TCP-ACTIVE":
|
||||
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
|
||||
case "UDP":
|
||||
mediaLine = fmt.Sprintf("m=video %d RTP/AVP 96", d.MediaPort)
|
||||
default:
|
||||
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
|
||||
}
|
||||
|
||||
sdpInfo = append(sdpInfo, mediaLine)
|
||||
sdpInfo = append(sdpInfo, "a=recvonly")
|
||||
|
||||
//根据传输模式添加 setup 和 connection 属性
|
||||
switch strings.ToUpper(device.StreamMode) {
|
||||
case "TCP-PASSIVE":
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
)
|
||||
case "TCP-ACTIVE":
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:active",
|
||||
"a=connection:new",
|
||||
)
|
||||
case "UDP":
|
||||
d.Stop(errors.New("do not support udp mode"))
|
||||
default:
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
)
|
||||
}
|
||||
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
|
||||
|
||||
// 添加 SSRC
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("y=%s", ssrc))
|
||||
|
||||
// 创建 INVITE 请求
|
||||
recipient := sip.Uri{
|
||||
Host: device.IP,
|
||||
Port: device.Port,
|
||||
User: channelId,
|
||||
}
|
||||
// 设置必需的头部
|
||||
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
|
||||
subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", channelId, ssrc, d.gb.Serial))
|
||||
//allowHeader := sip.NewHeader("Allow", "INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY, BYE")
|
||||
//Toheader里需要放入目录通道的id
|
||||
toHeader := sip.ToHeader{
|
||||
Address: sip.Uri{User: channelId, Host: channelId[0:10]},
|
||||
}
|
||||
userAgentHeader := sip.NewHeader("User-Agent", "M7S/"+m7s.Version)
|
||||
|
||||
//customCallID := fmt.Sprintf("%s-%s-%d@%s", device.DeviceId, channelId, time.Now().Unix(), device.SipIp)
|
||||
customCallID := fmt.Sprintf("%s@%s", GenerateCallID(32), device.MediaIp)
|
||||
callID := sip.CallIDHeader(customCallID)
|
||||
viaHeader := sip.ViaHeader{
|
||||
ProtocolName: "SIP",
|
||||
ProtocolVersion: "2.0",
|
||||
Transport: "UDP",
|
||||
Host: device.MediaIp,
|
||||
Port: device.localPort,
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
viaHeader.Params.Add("branch", sip.GenerateBranchN(10)).Add("rport", "")
|
||||
maxforward := sip.MaxForwardsHeader(70)
|
||||
//contentLengthHeader := sip.ContentLengthHeader(len(strings.Join(sdpInfo, "\r\n") + "\r\n"))
|
||||
csqHeader := sip.CSeqHeader{
|
||||
SeqNo: uint32(device.SN),
|
||||
MethodName: "INVITE",
|
||||
}
|
||||
//request.AppendHeader(&contentLengthHeader)
|
||||
contactHDR := sip.ContactHeader{
|
||||
Address: sip.Uri{
|
||||
User: d.gb.Serial,
|
||||
Host: device.SipIp,
|
||||
Port: device.localPort,
|
||||
},
|
||||
}
|
||||
|
||||
fromHDR := sip.FromHeader{
|
||||
Address: sip.Uri{
|
||||
User: d.gb.Serial,
|
||||
Host: device.MediaIp,
|
||||
Port: device.localPort,
|
||||
},
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
fromHDR.Params.Add("tag", sip.GenerateTagN(32))
|
||||
dialogClientCache := sipgo.NewDialogClientCache(device.client, device.contactHDR)
|
||||
// 创建会话
|
||||
d.gb.Info("start to invite,recipient:", recipient, " viaHeader:", viaHeader, " fromHDR:", fromHDR, " toHeader:", toHeader, " device.contactHDR:", device.contactHDR, "contactHDR:", contactHDR)
|
||||
// 判断当前系统类型
|
||||
//if runtime.GOOS == "windows" {
|
||||
// d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
|
||||
//} else {
|
||||
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
|
||||
//}
|
||||
// 最后添加Content-Length头部
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dialog) Run() (err error) {
|
||||
d.Channel.Info("before WaitAnswer")
|
||||
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
|
||||
d.Channel.Info("after WaitAnswer")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -100,27 +238,48 @@ func (d *Dialog) Run() (err error) {
|
||||
ds := strings.Split(inviteResponseBody, "\r\n")
|
||||
for _, l := range ds {
|
||||
if ls := strings.Split(l, "="); len(ls) > 1 {
|
||||
if ls[0] == "y" && len(ls[1]) > 0 {
|
||||
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
|
||||
d.SSRC = uint32(_ssrc)
|
||||
} else {
|
||||
d.gb.Error("read invite response y ", "err", err)
|
||||
switch ls[0] {
|
||||
case "y":
|
||||
if len(ls[1]) > 0 {
|
||||
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
|
||||
d.SSRC = uint32(_ssrc)
|
||||
} else {
|
||||
d.gb.Error("read invite response y ", "err", err)
|
||||
}
|
||||
}
|
||||
// break
|
||||
}
|
||||
if ls[0] == "m" && len(ls[1]) > 0 {
|
||||
netinfo := strings.Split(ls[1], " ")
|
||||
if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" {
|
||||
d.gb.Debug("device support tcp")
|
||||
} else {
|
||||
return fmt.Errorf("device not support tcp")
|
||||
case "c":
|
||||
// 解析 c=IN IP4 xxx.xxx.xxx.xxx 格式
|
||||
parts := strings.Split(ls[1], " ")
|
||||
if len(parts) >= 3 {
|
||||
d.targetIP = parts[len(parts)-1]
|
||||
}
|
||||
case "m":
|
||||
// 解析 m=video port xxx 格式
|
||||
parts := strings.Split(ls[1], " ")
|
||||
if len(parts) >= 2 {
|
||||
if port, err := strconv.Atoi(parts[1]); err == nil {
|
||||
d.targetPort = port
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.session.InviteResponse.Contact() != nil {
|
||||
if &d.session.InviteRequest.Recipient != &d.session.InviteResponse.Contact().Address {
|
||||
d.session.InviteResponse.Contact().Address = d.session.InviteRequest.Recipient
|
||||
}
|
||||
}
|
||||
err = d.session.Ack(d.gb)
|
||||
if err != nil {
|
||||
d.gb.Error("ack session err", err)
|
||||
}
|
||||
pub := gb28181.NewPSPublisher(d.pullCtx.Publisher)
|
||||
pub.Receiver.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
|
||||
if d.StreamMode == "TCP-ACTIVE" {
|
||||
pub.Receiver.ListenAddr = fmt.Sprintf("%s:%d", d.targetIP, d.targetPort)
|
||||
} else {
|
||||
pub.Receiver.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
|
||||
}
|
||||
pub.Receiver.StreamMode = d.StreamMode
|
||||
d.AddTask(&pub.Receiver)
|
||||
pub.Demux()
|
||||
return
|
||||
@@ -131,5 +290,14 @@ func (d *Dialog) GetKey() uint32 {
|
||||
}
|
||||
|
||||
func (d *Dialog) Dispose() {
|
||||
d.session.Close()
|
||||
d.gb.tcpPorts <- d.MediaPort
|
||||
err := d.session.Bye(d)
|
||||
if err != nil {
|
||||
d.Error("dialog bye bye err", err)
|
||||
}
|
||||
err = d.session.Close()
|
||||
if err != nil {
|
||||
d.Error("dialog close session err", err)
|
||||
}
|
||||
d.gb.dialogs.Remove(d)
|
||||
}
|
||||
|
||||
332
plugin/gb28181/forwarddialog.go
Normal file
332
plugin/gb28181/forwarddialog.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
sipgo "github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
m7s "m7s.live/v5"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ForwardDialog 是用于转发RTP流的会话结构体
|
||||
type ForwardDialog struct {
|
||||
task.Job
|
||||
channel *Channel
|
||||
gb28181.InviteOptions
|
||||
gb *GB28181Plugin
|
||||
session *sipgo.DialogClientSession
|
||||
pullCtx m7s.PullJob
|
||||
forwarder *gb28181.RTPForwarder
|
||||
upIP string //上级平台主动模式时接收数据的IP
|
||||
upPort uint16 //上级平台主动模式时接收数据的端口
|
||||
platformIP string //上级平台被动模式时发送数据的IP
|
||||
platformPort int //上级平台被动模式时发送数据的端口
|
||||
platformSSRC string // 上级平台的SSRC
|
||||
platformCallId string //上级平台发起invite的callid
|
||||
// 是否为TCP传输
|
||||
TCP bool
|
||||
// 是否为TCP主动模式
|
||||
TCPActive bool
|
||||
start int64
|
||||
end int64
|
||||
downIP string
|
||||
downPort int
|
||||
}
|
||||
|
||||
// GetCallID 获取会话的CallID
|
||||
func (d *ForwardDialog) GetCallID() string {
|
||||
return d.session.InviteRequest.CallID().Value()
|
||||
}
|
||||
|
||||
// GetPullJob 获取拉流任务
|
||||
func (d *ForwardDialog) GetPullJob() *m7s.PullJob {
|
||||
return &d.pullCtx
|
||||
}
|
||||
|
||||
// GetKey 获取会话标识符
|
||||
func (d *ForwardDialog) GetKey() uint32 {
|
||||
return d.SSRC
|
||||
}
|
||||
|
||||
// Start 启动会话
|
||||
func (d *ForwardDialog) Start() (err error) {
|
||||
// 处理时间范围
|
||||
isLive := true
|
||||
if d.start > 0 && d.end > 0 {
|
||||
isLive = false
|
||||
d.pullCtx.PublishConfig.PubType = m7s.PublishTypeVod
|
||||
}
|
||||
//err = d.pullCtx.Publish()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sss := strings.Split(d.pullCtx.RemoteURL, "/")
|
||||
deviceId, channelId := sss[0], sss[1]
|
||||
var device *Device
|
||||
if deviceTmp, ok := d.gb.devices.Get(deviceId); ok {
|
||||
device = deviceTmp
|
||||
if channel, ok := deviceTmp.channels.Get(channelId); ok {
|
||||
d.channel = channel
|
||||
} else {
|
||||
return fmt.Errorf("channel %s not found", channelId)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("device %s not found", deviceId)
|
||||
}
|
||||
|
||||
// 注册对话到集合,使用类型转换
|
||||
d.gb.forwardDialogs.Set(d)
|
||||
//defer d.gb.forwardDialogs.Remove(d)
|
||||
|
||||
if d.gb.MediaPort.Valid() {
|
||||
select {
|
||||
case d.MediaPort = <-d.gb.tcpPorts:
|
||||
defer func() {
|
||||
d.gb.tcpPorts <- d.MediaPort
|
||||
}()
|
||||
default:
|
||||
return fmt.Errorf("no available tcp port")
|
||||
}
|
||||
} else {
|
||||
d.MediaPort = d.gb.MediaPort[0]
|
||||
}
|
||||
|
||||
// 使用上级平台的SSRC(如果有)或者设备的CreateSSRC方法
|
||||
var ssrcValue uint16
|
||||
if d.platformSSRC != "" {
|
||||
// 使用上级平台的SSRC
|
||||
if ssrcInt, err := strconv.ParseUint(d.platformSSRC, 10, 32); err == nil {
|
||||
d.SSRC = uint32(ssrcInt)
|
||||
} else {
|
||||
d.gb.Error("parse platform ssrc error", "err", err)
|
||||
// 使用设备的CreateSSRC方法作为备选
|
||||
ssrcValue = device.CreateSSRC(d.gb.Serial)
|
||||
d.SSRC = uint32(ssrcValue)
|
||||
}
|
||||
} else {
|
||||
// 使用设备的CreateSSRC方法
|
||||
ssrcValue = device.CreateSSRC(d.gb.Serial)
|
||||
d.SSRC = uint32(ssrcValue)
|
||||
}
|
||||
|
||||
// 构建 SDP 内容
|
||||
sdpInfo := []string{
|
||||
"v=0",
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", device.DeviceId, device.MediaIp),
|
||||
fmt.Sprintf("s=%s", util.Conditional(isLive, "Play", "Playback")), // 根据是否有时间参数决定
|
||||
}
|
||||
|
||||
// 非直播模式下添加u行,保持在s=和c=之间
|
||||
if !isLive {
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
|
||||
}
|
||||
|
||||
// 添加c行
|
||||
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
|
||||
|
||||
// 将字符串时间转换为 Unix 时间戳
|
||||
if !isLive {
|
||||
// 直接使用字符串格式的日期时间转换为秒级时间戳,不考虑时区问题
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", d.start, d.end))
|
||||
} else {
|
||||
sdpInfo = append(sdpInfo, "t=0 0")
|
||||
}
|
||||
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort))
|
||||
sdpInfo = append(sdpInfo, "a=recvonly")
|
||||
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
|
||||
//sdpInfo = append(sdpInfo, "a=rtpmap:98 H264/90000")
|
||||
//sdpInfo = append(sdpInfo, "a=rtpmap:97 MPEG4/90000")
|
||||
|
||||
//根据传输模式添加 setup 和 connection 属性
|
||||
switch strings.ToUpper(device.StreamMode) {
|
||||
case "TCP-PASSIVE":
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
)
|
||||
case "TCP-ACTIVE":
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:active",
|
||||
"a=connection:new",
|
||||
)
|
||||
case "UDP":
|
||||
d.Stop(errors.New("do not support udp mode"))
|
||||
default:
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
)
|
||||
}
|
||||
if d.SSRC == 0 {
|
||||
d.SSRC = uint32(ssrcValue)
|
||||
}
|
||||
|
||||
// 将SSRC转换为字符串格式
|
||||
ssrcStr := strconv.FormatUint(uint64(d.SSRC), 10)
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("y=%s", ssrcStr))
|
||||
|
||||
// 创建INVITE请求
|
||||
request := sip.NewRequest(sip.INVITE, sip.Uri{User: channelId, Host: device.IP})
|
||||
// 使用字符串格式的SSRC
|
||||
subject := fmt.Sprintf("%s:%s,%s:0", channelId, ssrcStr, deviceId)
|
||||
|
||||
// 创建自定义头部
|
||||
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
|
||||
subjectHeader := sip.NewHeader("Subject", subject)
|
||||
|
||||
// 设置请求体
|
||||
request.SetBody([]byte(strings.Join(sdpInfo, "\r\n") + "\r\n"))
|
||||
|
||||
recipient := device.Recipient
|
||||
recipient.User = channelId
|
||||
|
||||
viaHeader := sip.ViaHeader{
|
||||
ProtocolName: "SIP",
|
||||
ProtocolVersion: "2.0",
|
||||
Transport: "UDP",
|
||||
Host: device.SipIp,
|
||||
Port: device.localPort,
|
||||
Params: sip.HeaderParams(sip.NewParams()),
|
||||
}
|
||||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||||
fromHDR := sip.FromHeader{
|
||||
Address: sip.Uri{
|
||||
User: d.gb.Serial,
|
||||
Host: device.MediaIp,
|
||||
Port: device.localPort,
|
||||
},
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
toHeader := sip.ToHeader{
|
||||
Address: sip.Uri{User: channelId, Host: channelId[0:10]},
|
||||
}
|
||||
fromHDR.Params.Add("tag", sip.GenerateTagN(16))
|
||||
// 创建会话 - 使用device的dialogClient创建
|
||||
dialogClientCache := sipgo.NewDialogClientCache(device.client, device.contactHDR)
|
||||
//d.session, err = dialogClientCache.Invite(d.gb, recipient, request.Body(), &fromHDR, &toHeader, &viaHeader, subjectHeader, &contentTypeHeader)
|
||||
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &fromHDR, &toHeader, subjectHeader, &contentTypeHeader)
|
||||
return
|
||||
}
|
||||
|
||||
// Run 运行会话
|
||||
func (d *ForwardDialog) Run() (err error) {
|
||||
d.channel.Info("before WaitAnswer")
|
||||
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
|
||||
d.channel.Info("after WaitAnswer")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
inviteResponseBody := string(d.session.InviteResponse.Body())
|
||||
d.channel.Info("inviteResponse", "body", inviteResponseBody)
|
||||
ds := strings.Split(inviteResponseBody, "\r\n")
|
||||
for _, l := range ds {
|
||||
if ls := strings.Split(l, "="); len(ls) > 1 {
|
||||
switch ls[0] {
|
||||
case "y":
|
||||
if len(ls[1]) > 0 {
|
||||
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
|
||||
d.SSRC = uint32(_ssrc)
|
||||
} else {
|
||||
d.gb.Error("read invite response y ", "err", err)
|
||||
}
|
||||
}
|
||||
case "c":
|
||||
// 解析 c=IN IP4 xxx.xxx.xxx.xxx 格式
|
||||
parts := strings.Split(ls[1], " ")
|
||||
if len(parts) >= 3 {
|
||||
d.downIP = parts[len(parts)-1]
|
||||
}
|
||||
case "m":
|
||||
// 解析 m=video port xxx 格式
|
||||
parts := strings.Split(ls[1], " ")
|
||||
if len(parts) >= 2 {
|
||||
if port, err := strconv.Atoi(parts[1]); err == nil {
|
||||
d.downPort = port
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.session.InviteResponse.Contact() != nil {
|
||||
if &d.session.InviteRequest.Recipient != &d.session.InviteResponse.Contact().Address {
|
||||
d.session.InviteResponse.Contact().Address = d.session.InviteRequest.Recipient
|
||||
}
|
||||
}
|
||||
err = d.session.Ack(d.gb)
|
||||
if err != nil {
|
||||
d.gb.Error("ack session err", err)
|
||||
d.Stop(errors.New("ack session err" + err.Error()))
|
||||
}
|
||||
// 创建并初始化RTPForwarder
|
||||
//d.forwarder = gb28181.NewRTPForwarder()
|
||||
//d.forwarder.TCP = d.TCP
|
||||
//d.forwarder.TCPActive = d.TCPActive
|
||||
//d.forwarder.StreamMode = d.channel.Device.StreamMode
|
||||
//
|
||||
//if d.TCPActive {
|
||||
// d.forwarder.UpListenAddr = fmt.Sprintf(":%d", d.upPort)
|
||||
//} else {
|
||||
// d.forwarder.UpListenAddr = fmt.Sprintf("%s:%d", d.upIP, d.platformPort)
|
||||
//}
|
||||
//
|
||||
// 设置监听地址和端口
|
||||
if strings.ToUpper(d.channel.Device.StreamMode) == "TCP-ACTIVE" {
|
||||
d.forwarder.DownListenAddr = fmt.Sprintf("%s:%d", d.downIP, d.downPort)
|
||||
} else {
|
||||
d.forwarder.DownListenAddr = fmt.Sprintf(":%d", d.MediaPort)
|
||||
}
|
||||
//
|
||||
//// 设置转发目标
|
||||
//if d.platformIP != "" && d.platformPort > 0 {
|
||||
// err = d.forwarder.SetTarget(d.platformIP, d.platformPort)
|
||||
// if err != nil {
|
||||
// d.Error("set target error", "err", err)
|
||||
// return err
|
||||
// }
|
||||
//} else {
|
||||
// d.Error("no target set, will only receive but not forward")
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//// 设置目标SSRC
|
||||
//if d.platformSSRC != "" {
|
||||
// d.forwarder.TargetSSRC = d.platformSSRC
|
||||
// d.Info("set target ssrc", "ssrc", d.platformSSRC)
|
||||
//}
|
||||
|
||||
// 将forwarder添加到任务中
|
||||
d.AddTask(d.forwarder)
|
||||
|
||||
d.Info("forwarder started successfully",
|
||||
"d.forwarder.UpListenAddr", d.forwarder.UpListenAddr,
|
||||
"TCP", d.forwarder.TCP,
|
||||
"TCPActive", d.forwarder.TCPActive,
|
||||
"listen", d.forwarder.DownListenAddr,
|
||||
"target", fmt.Sprintf("%s:%d", d.platformIP, d.platformPort),
|
||||
"ssrc", d.platformSSRC)
|
||||
|
||||
// 使用goroutine启动Demux,避免阻塞
|
||||
d.forwarder.Demux()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Dispose 释放会话资源
|
||||
func (d *ForwardDialog) Dispose() {
|
||||
if d.session != nil {
|
||||
err := d.session.Bye(d)
|
||||
if err != nil {
|
||||
d.Error("forwarddialog bye bye err", err)
|
||||
}
|
||||
err = d.session.Close()
|
||||
if err != nil {
|
||||
d.Error("forwarddialog close session err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
27
plugin/gb28181/pkg/civilcode.go
Normal file
27
plugin/gb28181/pkg/civilcode.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package gb28181
|
||||
|
||||
// CivilCode 行政区划编码信息
|
||||
type CivilCode struct {
|
||||
Code string `json:"code"` // 行政区划编码
|
||||
Name string `json:"name"` // 行政区划名称
|
||||
ParentCode string `json:"parentCode"` // 父级行政区划编码
|
||||
}
|
||||
|
||||
// NewCivilCodeFromArray 从字符串数组创建 CivilCode 实例
|
||||
func NewCivilCodeFromArray(infoArray []string) *CivilCode {
|
||||
if len(infoArray) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
civilCode := &CivilCode{
|
||||
Code: infoArray[0],
|
||||
Name: infoArray[1],
|
||||
}
|
||||
|
||||
// 如果有父级编码
|
||||
if len(infoArray) > 2 && infoArray[2] != "" {
|
||||
civilCode.ParentCode = infoArray[2]
|
||||
}
|
||||
|
||||
return civilCode
|
||||
}
|
||||
126
plugin/gb28181/pkg/civilcodeutil.go
Normal file
126
plugin/gb28181/pkg/civilcodeutil.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CivilCodeUtil 行政区划编码工具类
|
||||
type CivilCodeUtil struct {
|
||||
// 用于消息的缓存
|
||||
civilCodeMap sync.Map // map[string]*CivilCode
|
||||
}
|
||||
|
||||
var (
|
||||
instance *CivilCodeUtil
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetInstance 获取单例实例
|
||||
func GetInstance() *CivilCodeUtil {
|
||||
once.Do(func() {
|
||||
instance = &CivilCodeUtil{}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// Add 添加多个行政区划编码
|
||||
func (c *CivilCodeUtil) Add(civilCodeList []*CivilCode) {
|
||||
if len(civilCodeList) > 0 {
|
||||
for _, civilCode := range civilCodeList {
|
||||
c.civilCodeMap.Store(civilCode.Code, civilCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddOne 添加单个行政区划编码
|
||||
func (c *CivilCodeUtil) AddOne(civilCode *CivilCode) {
|
||||
c.civilCodeMap.Store(civilCode.Code, civilCode)
|
||||
}
|
||||
|
||||
// GetParentCode 获取父级编码
|
||||
func (c *CivilCodeUtil) GetParentCode(code string) *CivilCode {
|
||||
if len(code) > 8 {
|
||||
return nil
|
||||
}
|
||||
if len(code) == 8 {
|
||||
parentCode := code[:6]
|
||||
if value, ok := c.civilCodeMap.Load(parentCode); ok {
|
||||
return value.(*CivilCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if value, ok := c.civilCodeMap.Load(code); ok {
|
||||
civilCode := value.(*CivilCode)
|
||||
if civilCode.ParentCode == "" {
|
||||
return nil
|
||||
}
|
||||
if parentValue, ok := c.civilCodeMap.Load(civilCode.ParentCode); ok {
|
||||
return parentValue.(*CivilCode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCivilCode 获取行政区划编码对象
|
||||
func (c *CivilCodeUtil) GetCivilCode(code string) *CivilCode {
|
||||
if len(code) > 8 {
|
||||
return nil
|
||||
}
|
||||
if value, ok := c.civilCodeMap.Load(code); ok {
|
||||
return value.(*CivilCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllParentCode 获取所有父级编码
|
||||
func (c *CivilCodeUtil) GetAllParentCode(civilCode string) []*CivilCode {
|
||||
var civilCodeList []*CivilCode
|
||||
parentCode := c.GetParentCode(civilCode)
|
||||
if parentCode != nil {
|
||||
civilCodeList = append(civilCodeList, parentCode)
|
||||
allParentCode := c.GetAllParentCode(parentCode.Code)
|
||||
if len(allParentCode) > 0 {
|
||||
civilCodeList = append(civilCodeList, allParentCode...)
|
||||
}
|
||||
}
|
||||
return civilCodeList
|
||||
}
|
||||
|
||||
// IsEmpty 判断是否为空
|
||||
func (c *CivilCodeUtil) IsEmpty() bool {
|
||||
empty := true
|
||||
c.civilCodeMap.Range(func(key, value interface{}) bool {
|
||||
empty = false
|
||||
return false
|
||||
})
|
||||
return empty
|
||||
}
|
||||
|
||||
// Size 获取大小
|
||||
func (c *CivilCodeUtil) Size() int {
|
||||
size := 0
|
||||
c.civilCodeMap.Range(func(key, value interface{}) bool {
|
||||
size++
|
||||
return true
|
||||
})
|
||||
return size
|
||||
}
|
||||
|
||||
// GetAllChild 获取所有子节点
|
||||
func (c *CivilCodeUtil) GetAllChild(parent string) []*Region {
|
||||
var result []*Region
|
||||
c.civilCodeMap.Range(func(key, value interface{}) bool {
|
||||
civilCode := value.(*CivilCode)
|
||||
if parent == "" {
|
||||
if strings.TrimSpace(civilCode.ParentCode) == "" {
|
||||
result = append(result, NewRegion(civilCode.Code, civilCode.Name, civilCode.ParentCode))
|
||||
}
|
||||
} else if civilCode.ParentCode == parent {
|
||||
result = append(result, NewRegion(civilCode.Code, civilCode.Name, civilCode.ParentCode))
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
365
plugin/gb28181/pkg/commongbchannel.go
Normal file
365
plugin/gb28181/pkg/commongbchannel.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CommonGBChannel 通用国标通道信息
|
||||
type CommonGBChannel struct {
|
||||
// 数据库自增ID
|
||||
GbID int `json:"gb_id" gorm:"column:gb_id"`
|
||||
|
||||
// 国标编码
|
||||
GbDeviceID string `json:"gb_device_id" gorm:"column:gb_device_id;default:null"`
|
||||
|
||||
// 国标名称
|
||||
GbName string `json:"gb_name" gorm:"column:gb_name;default:null"`
|
||||
|
||||
// 国标设备厂商
|
||||
GbManufacturer string `json:"gb_manufacturer" gorm:"column:gb_manufacturer;default:null"`
|
||||
|
||||
// 国标设备型号
|
||||
GbModel string `json:"gb_model" gorm:"column:gb_model;default:null"`
|
||||
|
||||
// 国标设备归属
|
||||
GbOwner string `json:"gb_owner" gorm:"column:gb_owner;default:null"`
|
||||
|
||||
// 国标行政区域
|
||||
GbCivilCode string `json:"gb_civil_code" gorm:"column:gb_civil_code"`
|
||||
|
||||
// 国标警区
|
||||
GbBlock string `json:"gb_block" gorm:"column:gb_block"`
|
||||
|
||||
// 国标安装地址
|
||||
GbAddress string `json:"gb_address" gorm:"column:gb_address;default:null"`
|
||||
|
||||
// 国标是否有子设备
|
||||
GbParental int `json:"gb_parental" gorm:"column:gb_parental"`
|
||||
|
||||
// 国标父节点ID
|
||||
GbParentID string `json:"gb_parent_id" gorm:"column:gb_parent_id"`
|
||||
|
||||
// 国标信令安全模式
|
||||
GbSafetyWay int `json:"gb_safety_way" gorm:"column:gb_safety_way"`
|
||||
|
||||
// 国标注册方式
|
||||
GbRegisterWay int `json:"gb_register_way" gorm:"column:gb_register_way"`
|
||||
|
||||
// 国标证书序列号
|
||||
GbCertNum string `json:"gb_cert_num" gorm:"column:gb_cert_num"`
|
||||
|
||||
// 国标证书有效标识
|
||||
GbCertifiable int `json:"gb_certifiable" gorm:"column:gb_certifiable"`
|
||||
|
||||
// 国标无效原因码
|
||||
GbErrCode int `json:"gb_err_code" gorm:"column:gb_err_code"`
|
||||
|
||||
// 国标证书终止有效期
|
||||
GbEndTime string `json:"gb_end_time" gorm:"column:gb_end_time"`
|
||||
|
||||
// 国标保密属性
|
||||
GbSecrecy int `json:"gb_secrecy" gorm:"column:gb_secrecy"`
|
||||
|
||||
// 国标IP地址
|
||||
GbIPAddress string `json:"gb_ip_address" gorm:"column:gb_ip_address"`
|
||||
|
||||
// 国标端口
|
||||
GbPort int `json:"gb_port" gorm:"column:gb_port"`
|
||||
|
||||
// 国标密码
|
||||
GbPassword string `json:"gb_password" gorm:"column:gb_password"`
|
||||
|
||||
// 国标状态
|
||||
GbStatus string `json:"gb_status" gorm:"column:gb_status"`
|
||||
|
||||
// 国标经度
|
||||
GbLongitude float64 `json:"gb_longitude" gorm:"column:gb_longitude"`
|
||||
|
||||
// 国标纬度
|
||||
GbLatitude float64 `json:"gb_latitude" gorm:"column:gb_latitude"`
|
||||
|
||||
// 国标业务分组ID
|
||||
GbBusinessGroupID string `json:"gb_business_group_id" gorm:"column:gb_business_group_id"`
|
||||
|
||||
// 国标云台类型
|
||||
GbPTZType int `json:"gb_ptz_type" gorm:"column:gb_ptz_type"`
|
||||
|
||||
// 国标位置类型
|
||||
GbPositionType int `json:"gb_position_type" gorm:"column:gb_position_type"`
|
||||
|
||||
// 国标房间类型
|
||||
GbRoomType int `json:"gb_room_type" gorm:"column:gb_room_type"`
|
||||
|
||||
// 国标用途类型
|
||||
GbUseType int `json:"gb_use_type" gorm:"column:gb_use_type"`
|
||||
|
||||
// 国标补光类型
|
||||
GbSupplyLightType int `json:"gb_supply_light_type" gorm:"column:gb_supply_light_type"`
|
||||
|
||||
// 国标方向类型
|
||||
GbDirectionType int `json:"gb_direction_type" gorm:"column:gb_direction_type"`
|
||||
|
||||
// 国标分辨率
|
||||
GbResolution string `json:"gb_resolution" gorm:"column:gb_resolution"`
|
||||
|
||||
// 国标下载速度
|
||||
GbDownloadSpeed string `json:"gb_download_speed" gorm:"column:gb_download_speed"`
|
||||
|
||||
// 国标空域编码能力
|
||||
GbSvcSpaceSupportMod int `json:"gb_svc_space_support_mod" gorm:"column:gb_svc_space_support_mod"`
|
||||
|
||||
// 国标时域编码能力
|
||||
GbSvcTimeSupportMode int `json:"gb_svc_time_support_mode" gorm:"column:gb_svc_time_support_mode"`
|
||||
|
||||
// 关联的国标设备数据库ID
|
||||
GbDeviceDbID int `json:"gb_device_db_id" gorm:"column:gb_device_db_id"`
|
||||
|
||||
// 二进制保存的录制计划
|
||||
RecordPlan int64 `json:"record_plan" gorm:"column:record_plan"`
|
||||
|
||||
// 关联的推流ID
|
||||
StreamPushID int `json:"stream_push_id" gorm:"column:stream_push_id"`
|
||||
|
||||
// 关联的拉流代理ID
|
||||
StreamProxyID int `json:"stream_proxy_id" gorm:"column:stream_proxy_id"`
|
||||
|
||||
// 创建时间
|
||||
CreateTime string `json:"create_time" gorm:"column:create_time"`
|
||||
|
||||
// 更新时间
|
||||
UpdateTime string `json:"update_time" gorm:"column:update_time"`
|
||||
|
||||
// 流ID,存在表示正在推流
|
||||
StreamID string `json:"stream_id" xml:"-"`
|
||||
|
||||
// 是否含有音频
|
||||
HasAudio bool `json:"has_audio" xml:"-"`
|
||||
}
|
||||
|
||||
// Build 构建通道信息
|
||||
func (c *CommonGBChannel) Build(deviceID string, name string, manufacturer string, model string, owner string,
|
||||
civilCode string, block string, address string, parentID string) {
|
||||
// TODO: 实现构建逻辑
|
||||
}
|
||||
|
||||
// GetFullContent 获取完整的通道信息内容
|
||||
func (c *CommonGBChannel) GetFullContent(deviceID string, name string, parentID string, event string) string {
|
||||
content := "<Item>\n"
|
||||
content += fmt.Sprintf("<DeviceId>%s</DeviceId>\n", deviceID)
|
||||
content += fmt.Sprintf("<Name>%s</Name>\n", name)
|
||||
|
||||
if len(deviceID) > 8 {
|
||||
deviceType := deviceID[10:13]
|
||||
switch deviceType {
|
||||
case "200":
|
||||
// 业务分组目录项
|
||||
if c.GbManufacturer != "" {
|
||||
content += fmt.Sprintf("<Manufacturer>%s</Manufacturer>\n", c.GbManufacturer)
|
||||
}
|
||||
if c.GbModel != "" {
|
||||
content += fmt.Sprintf("<Model>%s</Model>\n", c.GbModel)
|
||||
}
|
||||
if c.GbOwner != "" {
|
||||
content += fmt.Sprintf("<Owner>%s</Owner>\n", c.GbOwner)
|
||||
}
|
||||
if c.GbCivilCode != "" {
|
||||
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
|
||||
}
|
||||
if c.GbAddress != "" {
|
||||
content += fmt.Sprintf("<Address>%s</Address>\n", c.GbAddress)
|
||||
}
|
||||
if c.GbRegisterWay != 0 {
|
||||
content += fmt.Sprintf("<RegisterWay>%d</RegisterWay>\n", c.GbRegisterWay)
|
||||
}
|
||||
content += fmt.Sprintf("<Secrecy>%d</Secrecy>\n", c.GbSecrecy)
|
||||
|
||||
case "215":
|
||||
// 业务分组
|
||||
if c.GbCivilCode != "" {
|
||||
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
|
||||
}
|
||||
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", parentID)
|
||||
|
||||
case "216":
|
||||
// 虚拟组织目录项
|
||||
if c.GbCivilCode != "" {
|
||||
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
|
||||
}
|
||||
if c.GbParentID != "" {
|
||||
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", c.GbParentID)
|
||||
}
|
||||
content += fmt.Sprintf("<BusinessGroupID>%s</BusinessGroupID>\n", c.GbBusinessGroupID)
|
||||
|
||||
default:
|
||||
// 其他类型
|
||||
if c.GbManufacturer != "" {
|
||||
content += fmt.Sprintf("<Manufacturer>%s</Manufacturer>\n", c.GbManufacturer)
|
||||
}
|
||||
if c.GbModel != "" {
|
||||
content += fmt.Sprintf("<Model>%s</Model>\n", c.GbModel)
|
||||
}
|
||||
if c.GbOwner != "" {
|
||||
content += fmt.Sprintf("<Owner>%s</Owner>\n", c.GbOwner)
|
||||
}
|
||||
if c.GbCivilCode != "" {
|
||||
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
|
||||
}
|
||||
if c.GbAddress != "" {
|
||||
content += fmt.Sprintf("<Address>%s</Address>\n", c.GbAddress)
|
||||
}
|
||||
if c.GbParentID != "" {
|
||||
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", c.GbParentID)
|
||||
}
|
||||
content += fmt.Sprintf("<Parental>%d</Parental>\n", c.GbParental)
|
||||
if c.GbSafetyWay != 0 {
|
||||
content += fmt.Sprintf("<SafetyWay>%d</SafetyWay>\n", c.GbSafetyWay)
|
||||
}
|
||||
if c.GbRegisterWay != 0 {
|
||||
content += fmt.Sprintf("<RegisterWay>%d</RegisterWay>\n", c.GbRegisterWay)
|
||||
}
|
||||
if c.GbCertNum != "" {
|
||||
content += fmt.Sprintf("<CertNum>%s</CertNum>\n", c.GbCertNum)
|
||||
}
|
||||
if c.GbCertifiable != 0 {
|
||||
content += fmt.Sprintf("<Certifiable>%d</Certifiable>\n", c.GbCertifiable)
|
||||
}
|
||||
if c.GbErrCode != 0 {
|
||||
content += fmt.Sprintf("<ErrCode>%d</ErrCode>\n", c.GbErrCode)
|
||||
}
|
||||
if c.GbEndTime != "" {
|
||||
content += fmt.Sprintf("<EndTime>%s</EndTime>\n", c.GbEndTime)
|
||||
}
|
||||
content += fmt.Sprintf("<Secrecy>%d</Secrecy>\n", c.GbSecrecy)
|
||||
if c.GbIPAddress != "" {
|
||||
content += fmt.Sprintf("<IPAddress>%s</IPAddress>\n", c.GbIPAddress)
|
||||
}
|
||||
if c.GbPort != 0 {
|
||||
content += fmt.Sprintf("<Port>%d</Port>\n", c.GbPort)
|
||||
}
|
||||
if c.GbPassword != "" {
|
||||
content += fmt.Sprintf("<Password>%s</Password>\n", c.GbPassword)
|
||||
}
|
||||
if c.GbStatus != "" {
|
||||
content += fmt.Sprintf("<Status>%s</Status>\n", c.GbStatus)
|
||||
}
|
||||
if c.GbLongitude != 0 {
|
||||
content += fmt.Sprintf("<Longitude>%f</Longitude>\n", c.GbLongitude)
|
||||
}
|
||||
if c.GbLatitude != 0 {
|
||||
content += fmt.Sprintf("<Latitude>%f</Latitude>\n", c.GbLatitude)
|
||||
}
|
||||
|
||||
// Info 部分
|
||||
content += "<Info>\n"
|
||||
if c.GbPTZType != 0 {
|
||||
content += fmt.Sprintf(" <PTZType>%d</PTZType>\n", c.GbPTZType)
|
||||
}
|
||||
if c.GbPositionType != 0 {
|
||||
content += fmt.Sprintf(" <PositionType>%d</PositionType>\n", c.GbPositionType)
|
||||
}
|
||||
if c.GbRoomType != 0 {
|
||||
content += fmt.Sprintf(" <RoomType>%d</RoomType>\n", c.GbRoomType)
|
||||
}
|
||||
if c.GbUseType != 0 {
|
||||
content += fmt.Sprintf(" <UseType>%d</UseType>\n", c.GbUseType)
|
||||
}
|
||||
if c.GbSupplyLightType != 0 {
|
||||
content += fmt.Sprintf(" <SupplyLightType>%d</SupplyLightType>\n", c.GbSupplyLightType)
|
||||
}
|
||||
if c.GbDirectionType != 0 {
|
||||
content += fmt.Sprintf(" <DirectionType>%d</DirectionType>\n", c.GbDirectionType)
|
||||
}
|
||||
if c.GbResolution != "" {
|
||||
content += fmt.Sprintf(" <Resolution>%s</Resolution>\n", c.GbResolution)
|
||||
}
|
||||
if c.GbBusinessGroupID != "" {
|
||||
content += fmt.Sprintf(" <BusinessGroupID>%s</BusinessGroupID>\n", c.GbBusinessGroupID)
|
||||
}
|
||||
if c.GbDownloadSpeed != "" {
|
||||
content += fmt.Sprintf(" <DownloadSpeed>%s</DownloadSpeed>\n", c.GbDownloadSpeed)
|
||||
}
|
||||
if c.GbSvcSpaceSupportMod != 0 {
|
||||
content += fmt.Sprintf(" <SVCSpaceSupportMode>%d</SVCSpaceSupportMode>\n", c.GbSvcSpaceSupportMod)
|
||||
}
|
||||
if c.GbSvcTimeSupportMode != 0 {
|
||||
content += fmt.Sprintf(" <SVCTimeSupportMode>%d</SVCTimeSupportMode>\n", c.GbSvcTimeSupportMode)
|
||||
}
|
||||
content += "</Info>\n"
|
||||
}
|
||||
}
|
||||
|
||||
if event != "" {
|
||||
content += fmt.Sprintf("<Event>%s</Event>\n", event)
|
||||
}
|
||||
content += "</Item>\n"
|
||||
return content
|
||||
}
|
||||
|
||||
// Encode 编码通道信息
|
||||
func (c *CommonGBChannel) Encode(deviceID string, event string) string {
|
||||
if event == "" {
|
||||
return c.GetFullContent(deviceID, c.GbName, "", "")
|
||||
}
|
||||
|
||||
switch event {
|
||||
case "DEL", "DEFECT", "VLOST", "ON", "OFF":
|
||||
return fmt.Sprintf("<Item>\n<DeviceId>%s</DeviceId>\n<Event>%s</Event>\n</Item>\n", deviceID, event)
|
||||
case "ADD", "UPDATE":
|
||||
return c.GetFullContent(deviceID, c.GbName, "", event)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFromGroup 从 Group 构建 CommonGBChannel 实例
|
||||
func BuildFromGroup(group *Group) *CommonGBChannel {
|
||||
gbCode := DecodeGBCode(group.DeviceID)
|
||||
if gbCode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel := &CommonGBChannel{
|
||||
GbName: group.Name,
|
||||
GbDeviceID: group.DeviceID,
|
||||
GbCivilCode: group.CivilCode,
|
||||
}
|
||||
|
||||
if gbCode.TypeCode == "215" {
|
||||
// 业务分组
|
||||
channel.GbCivilCode = group.CivilCode
|
||||
} else if gbCode.TypeCode == "216" {
|
||||
// 虚拟组织
|
||||
channel.GbParentID = group.ParentDeviceID
|
||||
channel.GbBusinessGroupID = group.BusinessGroup
|
||||
channel.GbCivilCode = group.CivilCode
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// BuildFromPlatform 从 PlatformModel 构建 CommonGBChannel 实例
|
||||
func BuildFromPlatform(platform *PlatformModel) *CommonGBChannel {
|
||||
status := "OFF"
|
||||
if platform.Status {
|
||||
status = "ON"
|
||||
}
|
||||
return &CommonGBChannel{
|
||||
GbDeviceID: platform.DeviceGBID,
|
||||
GbName: platform.Name,
|
||||
GbManufacturer: platform.Manufacturer,
|
||||
GbModel: platform.Model,
|
||||
GbCivilCode: platform.CivilCode,
|
||||
GbAddress: platform.Address,
|
||||
GbRegisterWay: platform.RegisterWay,
|
||||
GbSecrecy: platform.Secrecy,
|
||||
GbStatus: status,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFromRegion 从 Region 构建 CommonGBChannel 实例
|
||||
func BuildFromRegion(region *Region) *CommonGBChannel {
|
||||
return &CommonGBChannel{
|
||||
GbDeviceID: region.DeviceID,
|
||||
GbName: region.Name,
|
||||
}
|
||||
}
|
||||
127
plugin/gb28181/pkg/devicealarm.go
Normal file
127
plugin/gb28181/pkg/devicealarm.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package gb28181
|
||||
|
||||
import "time"
|
||||
|
||||
// DeviceAlarm 报警信息结构体
|
||||
type DeviceAlarm struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement" json:"id" description:"数据库id"`
|
||||
DeviceID string `json:"deviceId" description:"设备的国标编号"`
|
||||
DeviceName string `json:"deviceName" description:"设备名称"`
|
||||
ChannelID string `json:"channelId" description:"通道的国标编号"`
|
||||
AlarmPriority string `json:"alarmPriority" description:"报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情"`
|
||||
AlarmMethod string `json:"alarmMethod" description:"报警方式,1为电话报警,2为设备报警,3为短信报警,4为GPS报警,5为视频报警,6为设备故障报警,7其他报警"`
|
||||
AlarmTime time.Time `json:"alarmTime" description:"报警时间"`
|
||||
AlarmDescription string `json:"alarmDescription" description:"报警内容描述"`
|
||||
Longitude float64 `json:"longitude" description:"经度"`
|
||||
Latitude float64 `json:"latitude" description:"纬度"`
|
||||
AlarmType string `json:"alarmType" description:"报警类型"`
|
||||
CreateTime time.Time `json:"createTime" description:"创建时间"`
|
||||
}
|
||||
|
||||
// GetAlarmPriorityDescription 获取报警级别描述
|
||||
func (a *DeviceAlarm) GetAlarmPriorityDescription() string {
|
||||
switch a.AlarmPriority {
|
||||
case "1":
|
||||
return "一级警情"
|
||||
case "2":
|
||||
return "二级警情"
|
||||
case "3":
|
||||
return "三级警情"
|
||||
case "4":
|
||||
return "四级警情"
|
||||
default:
|
||||
return a.AlarmPriority
|
||||
}
|
||||
}
|
||||
|
||||
// GetAlarmMethodDescription 获取报警方式描述
|
||||
func (a *DeviceAlarm) GetAlarmMethodDescription() string {
|
||||
var desc []rune
|
||||
for _, c := range a.AlarmMethod {
|
||||
switch c {
|
||||
case '1':
|
||||
desc = append(desc, []rune("-电话报警")...)
|
||||
case '2':
|
||||
desc = append(desc, []rune("-设备报警")...)
|
||||
case '3':
|
||||
desc = append(desc, []rune("-短信报警")...)
|
||||
case '4':
|
||||
desc = append(desc, []rune("-GPS报警")...)
|
||||
case '5':
|
||||
desc = append(desc, []rune("-视频报警")...)
|
||||
case '6':
|
||||
desc = append(desc, []rune("-设备故障报警")...)
|
||||
case '7':
|
||||
desc = append(desc, []rune("-其他报警")...)
|
||||
}
|
||||
}
|
||||
if len(desc) > 0 {
|
||||
return string(desc[1:]) // 去掉第一个'-'
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAlarmTypeDescription 获取报警类型描述
|
||||
func (a *DeviceAlarm) GetAlarmTypeDescription() string {
|
||||
if a.AlarmType == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 检查报警方式
|
||||
methodMap := make(map[string]bool)
|
||||
for _, c := range a.AlarmMethod {
|
||||
methodMap[string(c)] = true
|
||||
}
|
||||
|
||||
// 根据不同的报警方式返回对应的描述
|
||||
if methodMap["2"] { // 设备报警
|
||||
switch a.AlarmType {
|
||||
case "1":
|
||||
return "视频丢失报警"
|
||||
case "2":
|
||||
return "设备防拆报警"
|
||||
case "3":
|
||||
return "存储设备磁盘满报警"
|
||||
case "4":
|
||||
return "设备高温报警"
|
||||
case "5":
|
||||
return "设备低温报警"
|
||||
}
|
||||
}
|
||||
|
||||
if methodMap["5"] || methodMap["6"] { // 视频报警或设备故障报警
|
||||
switch a.AlarmType {
|
||||
case "1":
|
||||
return "人工视频报警"
|
||||
case "2":
|
||||
return "运动目标检测报警"
|
||||
case "3":
|
||||
return "遗留物检测报警"
|
||||
case "4":
|
||||
return "物体移除检测报警"
|
||||
case "5":
|
||||
return "绊线检测报警"
|
||||
case "6":
|
||||
return "入侵检测报警"
|
||||
case "7":
|
||||
return "逆行检测报警"
|
||||
case "8":
|
||||
return "徘徊检测报警"
|
||||
case "9":
|
||||
return "流量统计报警"
|
||||
case "10":
|
||||
return "密度检测报警"
|
||||
case "11":
|
||||
return "视频异常检测报警"
|
||||
case "12":
|
||||
return "快速移动报警"
|
||||
}
|
||||
}
|
||||
|
||||
return a.AlarmType
|
||||
}
|
||||
|
||||
// TableName 返回数据库表名
|
||||
func (DeviceAlarm) TableName() string {
|
||||
return "gb28181_devicealarm"
|
||||
}
|
||||
302
plugin/gb28181/pkg/devicechannel.go
Normal file
302
plugin/gb28181/pkg/devicechannel.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ChannelStatus 通道状态类型
|
||||
type ChannelStatus string
|
||||
|
||||
const (
|
||||
ChannelOnStatus ChannelStatus = "ON"
|
||||
ChannelOffStatus ChannelStatus = "OFF"
|
||||
)
|
||||
|
||||
// DeviceChannel 设备通道信息
|
||||
type DeviceChannel struct {
|
||||
//CommonGBChannel // 通过组合继承 CommonGBChannel 的字段
|
||||
|
||||
ID string `gorm:"primaryKey" json:"ID"` // 数据库自增长ID
|
||||
ChannelID string `json:"channelID" xml:"ChannelID"`
|
||||
DeviceID string `json:"deviceID" xml:"DeviceID"` // 设备国标编号
|
||||
ParentID string `json:"parentId" xml:"ParentID"` // 父节点ID
|
||||
Name string `json:"name" xml:"Name"` // 通道名称
|
||||
Manufacturer string `json:"manufacturer" xml:"Manufacturer"` // 设备厂商
|
||||
Model string `json:"model" xml:"Model"` // 设备型号
|
||||
Owner string `json:"owner" xml:"Owner"` // 设备归属
|
||||
CivilCode string `json:"civilCode" xml:"CivilCode"` // 行政区域
|
||||
Block string `json:"block" xml:"Block"` // 警区
|
||||
Address string `json:"address" xml:"Address"` // 安装地址
|
||||
Port int `json:"port" xml:"Port"` // 端口
|
||||
Parental int `json:"parental" xml:"Parental"` // 是否有子设备
|
||||
SafetyWay int `json:"safetyWay" xml:"SafetyWay"` // 信令安全模式
|
||||
RegisterWay int `json:"registerWay" xml:"RegisterWay"` // 注册方式
|
||||
CertNum string `json:"certNum" xml:"CertNum"` // 证书序列号
|
||||
Certifiable int `json:"certifiable" xml:"Certifiable"` // 证书有效标识
|
||||
ErrCode int `json:"errCode" xml:"ErrCode"` // 无效原因码
|
||||
EndTime string `json:"endTime" xml:"EndTime"` // 证书终止有效期
|
||||
Secrecy int `json:"secrecy" xml:"Secrecy"` // 保密属性
|
||||
IPAddress string `json:"ipAddress" xml:"IPAddress"` // 设备/系统IP地址
|
||||
Password string `json:"password" xml:"Password"` // 设备口令
|
||||
PTZType int `json:"ptzType" xml:"Info>PTZType"` // 摄像机类型
|
||||
PositionType int `json:"positionType" xml:"Info>PositionType"` // 摄像机位置类型
|
||||
RoomType int `json:"roomType" xml:"Info>RoomType"` // 安装位置室内外属性
|
||||
UseType int `json:"useType" xml:"Info>UseType"` // 用途属性
|
||||
SupplyLightType int `json:"supplyLightType" xml:"Info>SupplyLightType"` // 摄像机补光属性
|
||||
DirectionType int `json:"directionType" xml:"Info>DirectionType"` // 摄像机监视方位属性
|
||||
Resolution string `json:"resolution" xml:"Info>Resolution"` // 摄像机支持的分辨率
|
||||
BusinessGroupID string `json:"businessGroupId" xml:"Info>BusinessGroupID"` // 虚拟组织所属的业务分组ID
|
||||
DownloadSpeed string `json:"downloadSpeed" xml:"Info>DownloadSpeed"` // 下载倍速
|
||||
SVCSpaceSupportMod int `json:"svcSpaceSupportMod" xml:"Info>SVCSpaceSupportMode"` // 空域编码能力
|
||||
SVCTimeSupportMode int `json:"svcTimeSupportMode" xml:"Info>SVCTimeSupportMode"` // 时域编码能力
|
||||
StreamPushID int `json:"streamPushId"` // 关联的推流ID
|
||||
StreamProxyID int `json:"streamProxyId"` // 关联的拉流代理ID
|
||||
CreateTime string `json:"createTime"` // 创建时间
|
||||
Status ChannelStatus `json:"status" xml:"Status"` // 设备状态
|
||||
Longitude float64
|
||||
Latitude float64
|
||||
DeletedAt gorm.DeletedAt `yaml:"-"`
|
||||
|
||||
PTZTypeText string `json:"ptzTypeText"` // 云台类型描述字符串
|
||||
GbLongitude float64 `json:"gbLongitude"`
|
||||
GbLatitude float64 `json:"gbLatitude"`
|
||||
}
|
||||
|
||||
// SetPTZType 设置云台类型并更新描述文本
|
||||
func (d *DeviceChannel) SetPTZType(ptzType int) {
|
||||
d.PTZType = ptzType
|
||||
switch ptzType {
|
||||
case 0:
|
||||
d.PTZTypeText = "未知"
|
||||
case 1:
|
||||
d.PTZTypeText = "球机"
|
||||
case 2:
|
||||
d.PTZTypeText = "半球"
|
||||
case 3:
|
||||
d.PTZTypeText = "固定枪机"
|
||||
case 4:
|
||||
d.PTZTypeText = "遥控枪机"
|
||||
case 5:
|
||||
d.PTZTypeText = "遥控半球"
|
||||
case 6:
|
||||
d.PTZTypeText = "多目设备的全景/拼接通道"
|
||||
case 7:
|
||||
d.PTZTypeText = "多目设备的分割通道"
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeFromXML 从 XML 元素解码设备通道信息
|
||||
func DecodeFromXML(element interface{}) (*DeviceChannel, error) {
|
||||
// TODO: 实现 XML 解码逻辑
|
||||
// 这部分需要参考 Java 版本的 decode 方法实现
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DecodeWithOnlyDeviceID 仅解码设备ID
|
||||
func DecodeWithOnlyDeviceID(element interface{}) (*DeviceChannel, error) {
|
||||
// TODO: 实现仅解码设备ID的逻辑
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (d *DeviceChannel) TableName() string {
|
||||
return "gb28181_channel"
|
||||
}
|
||||
|
||||
// NewDeviceChannel 创建新的设备通道实例
|
||||
func NewDeviceChannel() *DeviceChannel {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
return &DeviceChannel{
|
||||
CreateTime: now,
|
||||
Status: ChannelOffStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// Encode 生成通道信息的XML内容
|
||||
func (d *DeviceChannel) Encode(event string, serverDeviceID string) string {
|
||||
if event == "" {
|
||||
return d.getFullContent("", serverDeviceID)
|
||||
}
|
||||
|
||||
switch event {
|
||||
case "DEL", "DEFECT", "VLOST":
|
||||
return "<Item>\n" +
|
||||
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
|
||||
"<Event>" + event + "</Event>\n" +
|
||||
"</Item>\n"
|
||||
case "ON", "OFF":
|
||||
return "<Item>\n" +
|
||||
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
|
||||
"<Event>" + event + "</Event>\n" +
|
||||
"</Item>\n"
|
||||
case "ADD", "UPDATE":
|
||||
return d.getFullContent(event, serverDeviceID)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// getFullContent 生成完整的通道信息XML内容
|
||||
func (d *DeviceChannel) getFullContent(event string, serverDeviceID string) string {
|
||||
content := "<Item>\n" +
|
||||
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
|
||||
"<Name>" + d.Name + "</Name>\n"
|
||||
|
||||
if len(d.DeviceID) > 8 {
|
||||
typeCode := d.DeviceID[10:13]
|
||||
switch typeCode {
|
||||
case "200":
|
||||
// 业务分组目录项
|
||||
if d.Manufacturer != "" {
|
||||
content += "<Manufacturer>" + d.Manufacturer + "</Manufacturer>\n"
|
||||
}
|
||||
if d.Model != "" {
|
||||
content += "<Model>" + d.Model + "</Model>\n"
|
||||
}
|
||||
if d.Owner != "" {
|
||||
content += "<Owner>" + d.Owner + "</Owner>\n"
|
||||
}
|
||||
if d.CivilCode != "" {
|
||||
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
|
||||
}
|
||||
if d.Address != "" {
|
||||
content += "<Address>" + d.Address + "</Address>\n"
|
||||
}
|
||||
if d.RegisterWay != 0 {
|
||||
content += "<RegisterWay>" + strconv.Itoa(d.RegisterWay) + "</RegisterWay>\n"
|
||||
}
|
||||
if d.Secrecy != 0 {
|
||||
content += "<Secrecy>" + strconv.Itoa(d.Secrecy) + "</Secrecy>\n"
|
||||
}
|
||||
case "215":
|
||||
// 业务分组
|
||||
if d.CivilCode != "" {
|
||||
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
|
||||
}
|
||||
content += "<ParentID>" + serverDeviceID + "</ParentID>\n"
|
||||
case "216":
|
||||
// 虚拟组织目录项
|
||||
if d.CivilCode != "" {
|
||||
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
|
||||
}
|
||||
if d.ParentID != "" {
|
||||
content += "<ParentID>" + d.ParentID + "</ParentID>\n"
|
||||
}
|
||||
content += "<BusinessGroupID>" + d.BusinessGroupID + "</BusinessGroupID>\n"
|
||||
default:
|
||||
// 其他类型
|
||||
d.appendCommonInfo(&content)
|
||||
}
|
||||
}
|
||||
|
||||
if event != "" {
|
||||
content += "<Event>" + event + "</Event>\n"
|
||||
}
|
||||
content += "</Item>\n"
|
||||
return content
|
||||
}
|
||||
|
||||
// appendCommonInfo 添加通用信息到XML内容
|
||||
func (d *DeviceChannel) appendCommonInfo(content *string) {
|
||||
if d.Manufacturer != "" {
|
||||
*content += "<Manufacturer>" + d.Manufacturer + "</Manufacturer>\n"
|
||||
}
|
||||
if d.Model != "" {
|
||||
*content += "<Model>" + d.Model + "</Model>\n"
|
||||
}
|
||||
if d.Owner != "" {
|
||||
*content += "<Owner>" + d.Owner + "</Owner>\n"
|
||||
}
|
||||
if d.CivilCode != "" {
|
||||
*content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
|
||||
}
|
||||
if d.Block != "" {
|
||||
*content += "<Block>" + d.Block + "</Block>\n"
|
||||
}
|
||||
if d.Address != "" {
|
||||
*content += "<Address>" + d.Address + "</Address>\n"
|
||||
}
|
||||
if d.ParentID != "" {
|
||||
*content += "<ParentID>" + d.ParentID + "</ParentID>\n"
|
||||
}
|
||||
if d.SafetyWay != 0 {
|
||||
*content += "<SafetyWay>" + strconv.Itoa(d.SafetyWay) + "</SafetyWay>\n"
|
||||
}
|
||||
if d.RegisterWay != 0 {
|
||||
*content += "<RegisterWay>" + strconv.Itoa(d.RegisterWay) + "</RegisterWay>\n"
|
||||
}
|
||||
if d.CertNum != "" {
|
||||
*content += "<CertNum>" + d.CertNum + "</CertNum>\n"
|
||||
}
|
||||
if d.Certifiable != 0 {
|
||||
*content += "<Certifiable>" + strconv.Itoa(d.Certifiable) + "</Certifiable>\n"
|
||||
}
|
||||
if d.ErrCode != 0 {
|
||||
*content += "<ErrCode>" + strconv.Itoa(d.ErrCode) + "</ErrCode>\n"
|
||||
}
|
||||
if d.EndTime != "" {
|
||||
*content += "<EndTime>" + d.EndTime + "</EndTime>\n"
|
||||
}
|
||||
if d.IPAddress != "" {
|
||||
*content += "<IPAddress>" + d.IPAddress + "</IPAddress>\n"
|
||||
}
|
||||
if d.Port != 0 {
|
||||
*content += "<Port>" + strconv.Itoa(d.Port) + "</Port>\n"
|
||||
}
|
||||
if d.Password != "" {
|
||||
*content += "<Password>" + d.Password + "</Password>\n"
|
||||
}
|
||||
if d.Status != "" {
|
||||
*content += "<Status>" + string(d.Status) + "</Status>\n"
|
||||
}
|
||||
if d.GbLongitude != 0 {
|
||||
*content += "<Longitude>" + strconv.FormatFloat(d.GbLongitude, 'f', -1, 64) + "</Longitude>\n"
|
||||
}
|
||||
if d.GbLatitude != 0 {
|
||||
*content += "<Latitude>" + strconv.FormatFloat(d.GbLatitude, 'f', -1, 64) + "</Latitude>\n"
|
||||
}
|
||||
|
||||
// 添加Info标签内的信息
|
||||
*content += "<Info>\n"
|
||||
d.appendInfoContent(content)
|
||||
*content += "</Info>\n"
|
||||
}
|
||||
|
||||
// appendInfoContent 添加Info标签内的信息到XML内容
|
||||
func (d *DeviceChannel) appendInfoContent(content *string) {
|
||||
if d.PTZType != 0 {
|
||||
*content += " <PTZType>" + strconv.Itoa(d.PTZType) + "</PTZType>\n"
|
||||
}
|
||||
if d.PositionType != 0 {
|
||||
*content += " <PositionType>" + strconv.Itoa(d.PositionType) + "</PositionType>\n"
|
||||
}
|
||||
if d.RoomType != 0 {
|
||||
*content += " <RoomType>" + strconv.Itoa(d.RoomType) + "</RoomType>\n"
|
||||
}
|
||||
if d.UseType != 0 {
|
||||
*content += " <UseType>" + strconv.Itoa(d.UseType) + "</UseType>\n"
|
||||
}
|
||||
if d.SupplyLightType != 0 {
|
||||
*content += " <SupplyLightType>" + strconv.Itoa(d.SupplyLightType) + "</SupplyLightType>\n"
|
||||
}
|
||||
if d.DirectionType != 0 {
|
||||
*content += " <DirectionType>" + strconv.Itoa(d.DirectionType) + "</DirectionType>\n"
|
||||
}
|
||||
if d.Resolution != "" {
|
||||
*content += " <Resolution>" + d.Resolution + "</Resolution>\n"
|
||||
}
|
||||
if d.BusinessGroupID != "" {
|
||||
*content += " <BusinessGroupID>" + d.BusinessGroupID + "</BusinessGroupID>\n"
|
||||
}
|
||||
if d.DownloadSpeed != "" {
|
||||
*content += " <DownloadSpeed>" + d.DownloadSpeed + "</DownloadSpeed>\n"
|
||||
}
|
||||
if d.SVCSpaceSupportMod != 0 {
|
||||
*content += " <SVCSpaceSupportMode>" + strconv.Itoa(d.SVCSpaceSupportMod) + "</SVCSpaceSupportMode>\n"
|
||||
}
|
||||
if d.SVCTimeSupportMode != 0 {
|
||||
*content += " <SVCTimeSupportMode>" + strconv.Itoa(d.SVCTimeSupportMode) + "</SVCTimeSupportMode>\n"
|
||||
}
|
||||
}
|
||||
393
plugin/gb28181/pkg/forwarder.go
Normal file
393
plugin/gb28181/pkg/forwarder.go
Normal file
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
RTPForwarder 是一个RTP包转发器,主要功能包括:
|
||||
|
||||
1. 可通过TCP或UDP协议接收RTP包
|
||||
2. 接收RTP包后不进行解析,直接转发到指定的IP和端口
|
||||
3. 支持限流控制,可设置发送间隔
|
||||
4. 提供与Monibuca系统集成的Publisher接口
|
||||
5. 提供了UDP和TCP两种模式的使用示例
|
||||
|
||||
使用场景:
|
||||
1. 作为GB28181协议中的媒体接收和转发节点
|
||||
2. 在不需要解析媒体内容的情况下,实现RTP流的中转
|
||||
3. 可用于搭建分发网络,将接收到的RTP流转发到多个目标
|
||||
|
||||
注意事项:
|
||||
1. 默认使用TCP协议,可通过设置Protocol字段切换为UDP模式
|
||||
2. 使用前需设置监听地址(DownListenAddr)和转发目标(SetTarget)
|
||||
3. 资源使用完毕后需调用Dispose方法释放资源
|
||||
*/
|
||||
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
rtp2 "m7s.live/v5/plugin/rtp/pkg"
|
||||
)
|
||||
|
||||
// RTPForwarder 接收RTP数据包并转发到指定目标的结构体
|
||||
type RTPForwarder struct {
|
||||
task.Task
|
||||
rtp.Packet
|
||||
FeedChan chan []byte // 接收RTP数据的通道
|
||||
RTPReader *rtp2.TCP // RTP TCP读取器
|
||||
UpListenAddr string //用于发送上级设备的监听地址
|
||||
upListener net.Listener //用于发送上级设备的TCP监听器
|
||||
DownListenAddr string // 用于接收下级摄像头数据监听地址
|
||||
downListener net.Listener // 用于接收下级摄像头数据的TCP监听器
|
||||
udpListener *net.UDPConn // UDP监听器
|
||||
// 是否为TCP传输
|
||||
TCP bool
|
||||
// 是否为TCP主动模式
|
||||
TCPActive bool
|
||||
TargetIP string // 目标IP地址
|
||||
TargetPort int // 目标端口
|
||||
TargetSSRC string // 目标SSRC,用于替换RTP包中的SSRC
|
||||
udpConn *net.UDPConn // UDP发送连接
|
||||
tcpConn net.Conn // TCP发送连接
|
||||
bufferPool sync.Pool // 缓冲池
|
||||
ForwardCount int64 // 已转发的包数量
|
||||
SendInterval time.Duration // 发送间隔,可用于限流
|
||||
lastSendTime time.Time // 上次发送时间
|
||||
stopChan chan struct{} // 停止信号通道
|
||||
*slog.Logger
|
||||
StreamMode string // 数据流传输模式(UDP:udp传输/TCP-ACTIVE:tcp主动模式/TCP-PASSIVE:tcp被动模式)
|
||||
}
|
||||
|
||||
// NewRTPForwarder 创建一个新的RTP转发器
|
||||
func NewRTPForwarder() *RTPForwarder {
|
||||
ret := &RTPForwarder{
|
||||
FeedChan: make(chan []byte, 2000), // 增加缓冲区大小,减少丢包风险
|
||||
SendInterval: time.Millisecond * 0, // 默认不限制发送间隔,最大速度转发
|
||||
stopChan: make(chan struct{}),
|
||||
Logger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
|
||||
}
|
||||
|
||||
ret.bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, 1500) // 常见MTU大小
|
||||
},
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ReadRTP 读取RTP包
|
||||
func (p *RTPForwarder) ReadRTP(rtpBuf util.Buffer) (err error) {
|
||||
if err = p.Unmarshal(rtpBuf); err != nil {
|
||||
p.Error("unmarshal error", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if p.Enabled(p, task.TraceLevel) {
|
||||
p.Trace("rtp", "len", rtpBuf.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
|
||||
}
|
||||
|
||||
// 直接使用原始RTP包数据
|
||||
rtpData := make([]byte, rtpBuf.Len())
|
||||
copy(rtpData, rtpBuf)
|
||||
|
||||
// 检查是否已经停止
|
||||
select {
|
||||
case <-p.stopChan:
|
||||
// 已经收到停止信号,不再发送数据
|
||||
return nil
|
||||
default:
|
||||
// 将完整的RTP包数据发送到通道
|
||||
select {
|
||||
case p.FeedChan <- rtpData:
|
||||
// 成功发送
|
||||
case <-p.stopChan:
|
||||
// 发送过程中收到停止信号
|
||||
return nil
|
||||
default:
|
||||
// 通道已满,记录警告
|
||||
p.Warn("feed channel full, dropping packet")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTarget 设置转发目标地址
|
||||
func (p *RTPForwarder) SetTarget(ip string, port int) error {
|
||||
p.TargetIP = ip
|
||||
p.TargetPort = port
|
||||
|
||||
// 根据转发协议创建相应的连接
|
||||
if !p.TCP {
|
||||
// 关闭已存在的UDP连接
|
||||
if p.udpConn != nil {
|
||||
p.udpConn.Close()
|
||||
}
|
||||
|
||||
p.Info("start udp to up platform", "ip", ip, "port", port)
|
||||
// 创建新的UDP连接
|
||||
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(ip, fmt.Sprintf("%d", port)))
|
||||
if err != nil {
|
||||
p.Error("resolve udp addr error", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.udpConn, err = net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
p.Error("dial udp error", "err", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
// 如果是TCP主动模式且还没有建立连接,等待连接
|
||||
p.Info("start to accept uplistener", "p.UpListenAddr,", p.UpListenAddr, "tcpConn is", p.tcpConn == nil, "p.Tcp is", p.TCP, "p.TCPActive", p.TCPActive)
|
||||
if p.TCP && p.TCPActive && p.tcpConn == nil {
|
||||
var err error
|
||||
if p.upListener == nil {
|
||||
p.upListener, err = net.Listen("tcp4", p.UpListenAddr)
|
||||
if err != nil {
|
||||
p.Error("start udp listen error", "err", err)
|
||||
}
|
||||
}
|
||||
p.Info("waiting for upstream connection...")
|
||||
p.tcpConn, err = p.upListener.Accept()
|
||||
if err != nil {
|
||||
p.Error("accept upstream connection failed", "err", err)
|
||||
}
|
||||
p.Info("upstream connected", "addr", p.tcpConn.RemoteAddr())
|
||||
}
|
||||
}()
|
||||
}
|
||||
p.Info("set target success", "ip", ip, "port", port, "TCP", p.TCP, "TCPActive", p.TCPActive)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动监听
|
||||
func (p *RTPForwarder) Start() (err error) {
|
||||
p.Info("RTPForwarder start", "target", p.TargetIP, "port", p.TargetPort)
|
||||
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
|
||||
// TCP主动模式不需要监听,直接返回
|
||||
p.Info("TCP-ACTIVE mode, no need to listen")
|
||||
} else if strings.ToUpper(p.StreamMode) == "TCP-PASSIVE" {
|
||||
p.downListener, err = net.Listen("tcp4", p.DownListenAddr)
|
||||
if err != nil {
|
||||
p.Error("start tcp listen error", "err", err)
|
||||
return err
|
||||
}
|
||||
p.Info("start tcp down listen,streammode is ", p.StreamMode, "addr", p.DownListenAddr)
|
||||
} else {
|
||||
addr, err := net.ResolveUDPAddr("udp", p.DownListenAddr)
|
||||
if err != nil {
|
||||
p.Error("resolve udp addr error", "err", err)
|
||||
return err
|
||||
}
|
||||
p.udpListener, err = net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
p.Error("start udp listen error", "err", err)
|
||||
return err
|
||||
}
|
||||
p.Info("start udp listen", "addr", p.DownListenAddr)
|
||||
}
|
||||
|
||||
if !p.TCPActive && p.TCP { //TCP被动模式,需要服务器主动连接上级设备
|
||||
// 创建新的TCP连接
|
||||
addr := p.UpListenAddr
|
||||
var err error
|
||||
p.tcpConn, err = net.Dial("tcp", addr)
|
||||
p.Info("start tcp listen,now is tcp-passive", "addr", addr)
|
||||
if err != nil {
|
||||
p.Error("dial tcp error", "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.Info("RTPForwarder end")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Go 启动处理任务
|
||||
func (p *RTPForwarder) Go() error {
|
||||
p.Info("start go", "addr", p.DownListenAddr)
|
||||
//if p.TCP {
|
||||
return p.goTCP()
|
||||
//} else {
|
||||
// return p.goUDP()
|
||||
//}
|
||||
}
|
||||
|
||||
// goTCP 处理TCP连接的RTP包
|
||||
func (p *RTPForwarder) goTCP() error {
|
||||
p.Info("start tcp accept")
|
||||
|
||||
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
|
||||
// TCP主动模式:直接连接到设备
|
||||
addr := p.DownListenAddr
|
||||
if !strings.Contains(addr, ":") {
|
||||
return fmt.Errorf("invalid address %s, missing port", addr)
|
||||
}
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
p.Error("connect to device failed", "err", err)
|
||||
return err
|
||||
}
|
||||
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
|
||||
p.Info("connected to device", "addr", conn.RemoteAddr())
|
||||
return p.RTPReader.Read(p.ReadRTP)
|
||||
}
|
||||
|
||||
// TCP被动模式:等待连接
|
||||
conn, err := p.downListener.Accept()
|
||||
if err != nil {
|
||||
p.Error("accept error", "err", err)
|
||||
return err
|
||||
}
|
||||
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
|
||||
p.Info("accept connection", "addr", conn.RemoteAddr())
|
||||
return p.RTPReader.Read(p.ReadRTP)
|
||||
}
|
||||
|
||||
// Demux 阻塞读取RTP并转发至目标IP和端口
|
||||
func (p *RTPForwarder) Demux() {
|
||||
defer p.Info("demux exit")
|
||||
|
||||
// 检查是否设置了目标地址
|
||||
if !p.TCP && p.udpConn == nil {
|
||||
p.Error("no udp target set for forwarding")
|
||||
return
|
||||
}
|
||||
|
||||
//if p.TCP && p.tcpConn == nil {
|
||||
// p.Error("no tcp target set for forwarding")
|
||||
// return
|
||||
//}
|
||||
|
||||
p.Info("start demux and forward",
|
||||
"target", net.JoinHostPort(p.TargetIP, fmt.Sprintf("%d", p.TargetPort)),
|
||||
"TCP", p.TCP, "TCPActive", p.TCPActive)
|
||||
|
||||
// 持续从FeedChan读取RTP数据并转发
|
||||
for rtpData := range p.FeedChan {
|
||||
var err error
|
||||
|
||||
// 根据转发协议选择不同的发送方式
|
||||
if !p.TCP {
|
||||
// 确保发送的是标准RTP包
|
||||
// 检查是否是有效的RTP包
|
||||
packet := &rtp.Packet{}
|
||||
if parseErr := packet.Unmarshal(rtpData); parseErr != nil {
|
||||
p.Error("invalid RTP packet for UDP forwarding", "err", parseErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果设置了目标SSRC,则修改RTP包中的SSRC
|
||||
if p.TargetSSRC != "" {
|
||||
targetSSRCUint, err := strconv.ParseUint(p.TargetSSRC, 10, 32)
|
||||
if err == nil {
|
||||
// 修改SSRC
|
||||
packet.SSRC = uint32(targetSSRCUint)
|
||||
|
||||
// 重新编码RTP包
|
||||
modifiedData, err := packet.Marshal()
|
||||
if err == nil {
|
||||
// 发送修改后的RTP包
|
||||
_, err = p.udpConn.Write(modifiedData)
|
||||
} else {
|
||||
p.Error("marshal modified rtp packet error", "err", err)
|
||||
// 发送原始RTP包
|
||||
_, err = p.udpConn.Write(rtpData)
|
||||
}
|
||||
} else {
|
||||
p.Error("parse target ssrc error", "err", err)
|
||||
// 发送原始RTP包
|
||||
_, err = p.udpConn.Write(rtpData)
|
||||
}
|
||||
} else {
|
||||
// 直接发送原始RTP包
|
||||
_, err = p.udpConn.Write(rtpData)
|
||||
}
|
||||
} else {
|
||||
|
||||
// 对于TCP,需要添加2字节的长度前缀
|
||||
if p.tcpConn != nil {
|
||||
// 创建带长度前缀的数据包
|
||||
tcpData := make([]byte, len(rtpData)+2)
|
||||
// 设置长度前缀(大端序)
|
||||
tcpData[0] = byte((len(rtpData) >> 8) & 0xFF)
|
||||
tcpData[1] = byte(len(rtpData) & 0xFF)
|
||||
// 复制RTP数据
|
||||
copy(tcpData[2:], rtpData)
|
||||
|
||||
// 发送到TCP连接
|
||||
_, err = p.tcpConn.Write(tcpData)
|
||||
} else {
|
||||
err = fmt.Errorf("tcp connection not established")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
p.Error("forward rtp packet error", "err", err, "TCP", p.TCP, "TCPActive", p.TCPActive)
|
||||
continue
|
||||
}
|
||||
|
||||
p.ForwardCount++
|
||||
|
||||
// 控制发送速率
|
||||
if p.SendInterval > 0 && !p.lastSendTime.IsZero() {
|
||||
elapsed := time.Since(p.lastSendTime)
|
||||
if elapsed < p.SendInterval {
|
||||
time.Sleep(p.SendInterval - elapsed)
|
||||
}
|
||||
}
|
||||
p.lastSendTime = time.Now()
|
||||
|
||||
if p.Enabled(p, task.TraceLevel) && p.ForwardCount%1000 == 0 {
|
||||
p.Trace("forward rtp packet", "count", p.ForwardCount, "TCP", p.TCP, "TCPActive", p.TCPActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose 释放资源
|
||||
func (p *RTPForwarder) Dispose() {
|
||||
p.Info("disposing forwarder")
|
||||
|
||||
// 发送停止信号
|
||||
close(p.stopChan)
|
||||
|
||||
// 给一些时间让所有goroutine响应停止信号
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if p.downListener != nil {
|
||||
p.downListener.Close()
|
||||
}
|
||||
|
||||
if p.upListener != nil {
|
||||
p.upListener.Close()
|
||||
}
|
||||
|
||||
if p.udpListener != nil {
|
||||
p.udpListener.Close()
|
||||
}
|
||||
|
||||
if p.RTPReader != nil {
|
||||
p.RTPReader.Close()
|
||||
}
|
||||
|
||||
if p.udpConn != nil {
|
||||
p.udpConn.Close()
|
||||
}
|
||||
|
||||
if p.tcpConn != nil {
|
||||
p.tcpConn.Close()
|
||||
}
|
||||
|
||||
// 确保所有goroutine都有机会处理停止信号后再关闭FeedChan
|
||||
close(p.FeedChan)
|
||||
p.Info("forwarder disposed", "forwarded_packets", p.ForwardCount)
|
||||
}
|
||||
30
plugin/gb28181/pkg/gbcode.go
Normal file
30
plugin/gb28181/pkg/gbcode.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package gb28181
|
||||
|
||||
// GbCode 国标编码对象
|
||||
type GbCode struct {
|
||||
CenterCode string `json:"centerCode"` // 中心编码,由监控中心所在地的行政区划代码确定,符合GB/T2260—2007的要求
|
||||
IndustryCode string `json:"industryCode"` // 行业编码
|
||||
TypeCode string `json:"typeCode"` // 类型编码
|
||||
NetCode string `json:"netCode"` // 网络标识
|
||||
SN string `json:"sn"` // 序号
|
||||
}
|
||||
|
||||
// DecodeGBCode 解析国标编号
|
||||
func DecodeGBCode(code string) *GbCode {
|
||||
if code == "" || len(code) != 20 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &GbCode{
|
||||
CenterCode: code[0:8],
|
||||
IndustryCode: code[8:10],
|
||||
TypeCode: code[10:13],
|
||||
NetCode: code[13:14],
|
||||
SN: code[14:],
|
||||
}
|
||||
}
|
||||
|
||||
// Encode 编码为完整的国标编号
|
||||
func (g *GbCode) Encode() string {
|
||||
return g.CenterCode + g.IndustryCode + g.TypeCode + g.NetCode + g.SN
|
||||
}
|
||||
61
plugin/gb28181/pkg/group.go
Normal file
61
plugin/gb28181/pkg/group.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Group 表示业务分组
|
||||
type Group struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // 数据库自增ID
|
||||
DeviceID string `gorm:"column:device_id" json:"deviceId"` // 区域国标编号
|
||||
Name string `gorm:"column:name" json:"name"` // 区域名称
|
||||
ParentID int `gorm:"column:parent_id" json:"parentId"` // 父分组ID
|
||||
ParentDeviceID string `gorm:"column:parent_device_id" json:"parentDeviceId"` // 父区域国标ID
|
||||
BusinessGroup string `gorm:"column:business_group" json:"businessGroup"` // 所属的业务分组国标编号
|
||||
CreateTime string `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||
UpdateTime string `gorm:"column:update_time" json:"updateTime"` // 更新时间
|
||||
CivilCode string `gorm:"column:civil_code" json:"civilCode"` // 行政区划
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (g *Group) TableName() string {
|
||||
return "group_gb28181pro"
|
||||
}
|
||||
|
||||
// NewGroupFromChannel 从 DeviceChannel 创建 Group 实例
|
||||
func NewGroupFromChannel(channel *DeviceChannel) *Group {
|
||||
gbCode := DecodeGBCode(channel.DeviceID)
|
||||
if gbCode == nil || (gbCode.TypeCode != "215" && gbCode.TypeCode != "216") {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
group := &Group{
|
||||
Name: channel.Name,
|
||||
DeviceID: channel.DeviceID,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
switch gbCode.TypeCode {
|
||||
case "215":
|
||||
group.BusinessGroup = channel.DeviceID
|
||||
case "216":
|
||||
group.BusinessGroup = channel.BusinessGroupID // 注意:需要在 DeviceChannel 中添加 BusinessGroupID 字段
|
||||
group.ParentDeviceID = channel.ParentID
|
||||
}
|
||||
|
||||
if group.BusinessGroup == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
// CompareTo 实现比较功能
|
||||
func (g *Group) CompareTo(other *Group) int {
|
||||
thisID, _ := strconv.Atoi(g.DeviceID)
|
||||
otherID, _ := strconv.Atoi(other.DeviceID)
|
||||
return thisID - otherID
|
||||
}
|
||||
107
plugin/gb28181/pkg/groupschannelmodel.go
Normal file
107
plugin/gb28181/pkg/groupschannelmodel.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package gb28181
|
||||
|
||||
// GroupsChannelModel 表示分组与通道的关联关系
|
||||
type GroupsChannelModel struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // ID表示数据库中的唯一标识符
|
||||
GroupID int `gorm:"column:group_id;index" json:"groupId"` // GroupID表示关联的分组ID
|
||||
ChannelID string `gorm:"column:channel_id;index" json:"channelId"` // ChannelID表示关联的通道ID
|
||||
DeviceID string `gorm:"column:device_id;index" json:"deviceId"` // DeviceID表示关联的设备ID
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (g *GroupsChannelModel) TableName() string {
|
||||
return "groups_channel"
|
||||
}
|
||||
|
||||
// NewGroupsChannel 创建并返回一个新的GroupsChannelModel实例
|
||||
func NewGroupsChannel(groupID int, channelID string, deviceID string) *GroupsChannelModel {
|
||||
return &GroupsChannelModel{
|
||||
GroupID: groupID,
|
||||
ChannelID: channelID,
|
||||
DeviceID: deviceID,
|
||||
}
|
||||
}
|
||||
|
||||
// FindGroupChannels 通过分组ID查找关联的通道
|
||||
func FindGroupChannels(db interface{}, groupID int) ([]*GroupsChannelModel, error) {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
type DBAPI interface {
|
||||
Find(dest interface{}, conds ...interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
var channels []*GroupsChannelModel
|
||||
err := gdb.Find(&channels, "group_id = ?", groupID)
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindChannelGroups 通过通道ID查找关联的分组
|
||||
func FindChannelGroups(db interface{}, channelID string) ([]*GroupsChannelModel, error) {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
type DBAPI interface {
|
||||
Find(dest interface{}, conds ...interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
var groups []*GroupsChannelModel
|
||||
err := gdb.Find(&groups, "channel_id = ?", channelID)
|
||||
return groups, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDeviceGroups 通过设备ID查找关联的分组
|
||||
func FindDeviceGroups(db interface{}, deviceID string) ([]*GroupsChannelModel, error) {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
type DBAPI interface {
|
||||
Find(dest interface{}, conds ...interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
var groups []*GroupsChannelModel
|
||||
err := gdb.Find(&groups, "device_id = ?", deviceID)
|
||||
return groups, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindGroupChannelsByDevice 通过分组ID和设备ID查找关联的通道
|
||||
func FindGroupChannelsByDevice(db interface{}, groupID int, deviceID string) ([]*GroupsChannelModel, error) {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
type DBAPI interface {
|
||||
Find(dest interface{}, conds ...interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
var channels []*GroupsChannelModel
|
||||
err := gdb.Find(&channels, "group_id = ? AND device_id = ?", groupID, deviceID)
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AutoMigrate 执行自动迁移
|
||||
// 此函数应在插件初始化时调用
|
||||
func AutoMigrateGroupChannel(db interface{}) error {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
// 使用类型断言获取 DB 实例
|
||||
type DBAPI interface {
|
||||
AutoMigrate(dst ...interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
// 执行表结构自动迁移
|
||||
if err := gdb.AutoMigrate(&GroupsChannelModel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
plugin/gb28181/pkg/groupsmodel.go
Normal file
111
plugin/gb28181/pkg/groupsmodel.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GroupsModel 表示分组结构
|
||||
type GroupsModel struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // ID表示数据库中的唯一标识符
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // CreateTime表示记录创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // UpdateTime表示记录更新时间
|
||||
Name string `gorm:"column:name" json:"name"` // Name表示分组名称
|
||||
PID int `gorm:"column:pid;default:0" json:"pid"` // PID表示父分组ID
|
||||
Level int `gorm:"column:level;default:0" json:"level"` // Level表示分组层级
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (g *GroupsModel) TableName() string {
|
||||
return "groups"
|
||||
}
|
||||
|
||||
// NewGroup 创建并返回一个新的GroupsModel实例
|
||||
func NewGroup(name string, pid int, level int) *GroupsModel {
|
||||
now := time.Now()
|
||||
return &GroupsModel{
|
||||
Name: name,
|
||||
PID: pid,
|
||||
Level: level,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRootGroup 创建根分组实例
|
||||
func NewRootGroup() *GroupsModel {
|
||||
return NewGroup("根", 0, 0)
|
||||
}
|
||||
|
||||
// InitRootGroup 初始化根分组记录
|
||||
// 如果数据库中不存在根分组,则创建一个
|
||||
func InitRootGroup(db interface{}) error {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
// 使用类型断言获取 DB 实例
|
||||
// 这里使用 interface{} 是为了避免直接依赖 GORM
|
||||
type DBAPI interface {
|
||||
First(dest interface{}, conds ...interface{}) error
|
||||
Create(value interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
root := &GroupsModel{}
|
||||
|
||||
// 检查是否存在根分组
|
||||
err := gdb.First(root, "pid = ? AND level = ?", 0, 0)
|
||||
if err != nil {
|
||||
// 如果不存在,则创建一个根分组
|
||||
rootGroup := NewRootGroup()
|
||||
return gdb.Create(rootGroup)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoMigrateAll 执行分组及分组-通道关联的自动迁移,并初始化根组织
|
||||
// 此函数应在插件初始化时调用,一次完成所有相关表的迁移
|
||||
func AutoMigrateAll(db interface{}) error {
|
||||
// db 参数应为 *gorm.DB 类型
|
||||
// 使用类型断言获取 DB 实例
|
||||
type DBAPI interface {
|
||||
AutoMigrate(dst ...interface{}) error
|
||||
First(dest interface{}, conds ...interface{}) error
|
||||
Create(value interface{}) error
|
||||
}
|
||||
|
||||
if gdb, ok := db.(DBAPI); ok {
|
||||
// 执行表结构自动迁移 - 分组表和分组-通道关联表
|
||||
if err := gdb.AutoMigrate(&GroupsModel{}, &GroupsChannelModel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否存在根分组
|
||||
root := &GroupsModel{}
|
||||
err := gdb.First(root, "pid = ? AND level = ?", 0, 0)
|
||||
if err != nil {
|
||||
// 如果不存在,则创建一个根分组
|
||||
rootGroup := NewRootGroup()
|
||||
return gdb.Create(rootGroup)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子,在创建记录前设置创建时间和更新时间
|
||||
func (g *GroupsModel) BeforeCreate() error {
|
||||
now := time.Now()
|
||||
g.CreateTime = now
|
||||
g.UpdateTime = now
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate GORM钩子,在更新记录前设置更新时间
|
||||
func (g *GroupsModel) BeforeUpdate() error {
|
||||
g.UpdateTime = time.Now()
|
||||
return nil
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type InviteOptions struct {
|
||||
Start int
|
||||
End int
|
||||
Start string
|
||||
End string
|
||||
dump string
|
||||
ssrc string
|
||||
SSRC uint32
|
||||
@@ -19,7 +19,7 @@ type InviteOptions struct {
|
||||
}
|
||||
|
||||
func (o InviteOptions) IsLive() bool {
|
||||
return o.Start == 0 || o.End == 0
|
||||
return o.Start == "" && o.End == ""
|
||||
}
|
||||
|
||||
func (o InviteOptions) Record() bool {
|
||||
@@ -27,21 +27,25 @@ func (o InviteOptions) Record() bool {
|
||||
}
|
||||
|
||||
func (o *InviteOptions) Validate(start, end string) error {
|
||||
var sint int64
|
||||
var eint int64
|
||||
if start != "" {
|
||||
sint, err1 := strconv.ParseInt(start, 10, 0)
|
||||
sinttmp, err1 := strconv.ParseInt(start, 10, 0)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
o.Start = int(sint)
|
||||
sint = sinttmp
|
||||
o.Start = start
|
||||
}
|
||||
if end != "" {
|
||||
eint, err2 := strconv.ParseInt(end, 10, 0)
|
||||
einttmp, err2 := strconv.ParseInt(end, 10, 0)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
o.End = int(eint)
|
||||
eint = einttmp
|
||||
o.End = end
|
||||
}
|
||||
if o.Start >= o.End {
|
||||
if sint >= eint {
|
||||
return errors.New("start < end")
|
||||
}
|
||||
return nil
|
||||
|
||||
36
plugin/gb28181/pkg/invite_info.go
Normal file
36
plugin/gb28181/pkg/invite_info.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gb28181
|
||||
|
||||
// InviteInfo 从INVITE消息中解析需要的信息
|
||||
type InviteInfo struct {
|
||||
// 请求者ID
|
||||
RequesterId string `json:"requesterId"`
|
||||
// 目标通道ID
|
||||
TargetChannelId string `json:"targetChannelId"`
|
||||
// 源通道ID
|
||||
SourceChannelId string `json:"sourceChannelId"`
|
||||
// 会话名称
|
||||
SessionName string `json:"sessionName"`
|
||||
// SSRC
|
||||
SSRC string `json:"ssrc"`
|
||||
// 是否使用TCP
|
||||
TCP bool `json:"tcp"`
|
||||
// TCP是否为主动模式
|
||||
TCPActive bool `json:"tcpActive"`
|
||||
// 呼叫ID
|
||||
CallId string `json:"callId"`
|
||||
// 开始时间
|
||||
StartTime int64 `json:"startTime"`
|
||||
// 结束时间
|
||||
StopTime int64 `json:"stopTime"`
|
||||
// 下载速度
|
||||
DownloadSpeed string `json:"downloadSpeed"`
|
||||
// IP地址
|
||||
IP string `json:"ip"`
|
||||
// 端口
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// NewInviteInfo 创建一个新的 InviteInfo 实例
|
||||
func NewInviteInfo() *InviteInfo {
|
||||
return &InviteInfo{}
|
||||
}
|
||||
@@ -4,25 +4,28 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
const (
|
||||
// CatalogXML 获取设备列表xml样式
|
||||
CatalogXML = `<?xml version="1.0"?><Query>
|
||||
CatalogXML = `<?xml version="1.0" encoding="%s"?>
|
||||
<Query>
|
||||
<CmdType>Catalog</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
// RecordInfoXML 获取录像文件列表xml样式
|
||||
RecordInfoXML = `<?xml version="1.0"?><Query>
|
||||
RecordInfoXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>RecordInfo</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
@@ -33,19 +36,37 @@ const (
|
||||
</Query>
|
||||
`
|
||||
// DeviceInfoXML 查询设备详情xml样式
|
||||
DeviceInfoXML = `<?xml version="1.0"?><Query>
|
||||
DeviceInfoXML = `<?xml version="1.0" encoding="%s"?>
|
||||
<Query>
|
||||
<CmdType>DeviceInfo</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
// DeviceStatusXML 查询设备详情xml样式
|
||||
DeviceStatusXML = `<?xml version="1.0" encoding="%s"?>
|
||||
<Query>
|
||||
<CmdType>DeviceStatus</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
// DevicePositionXML 订阅设备位置
|
||||
DevicePositionXML = `<?xml version="1.0"?><Query>
|
||||
DevicePositionXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>MobilePosition</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<Interval>%d</Interval>
|
||||
</Query>
|
||||
`
|
||||
// PresetQueryXML 查询预置位指令
|
||||
PresetQueryXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>PresetQuery</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
AlarmResponseXML = `<?xml version="1.0"?><Response>
|
||||
<CmdType>Alarm</CmdType>
|
||||
@@ -61,8 +82,6 @@ const (
|
||||
<Status>OK</Status>
|
||||
</Notify>
|
||||
`
|
||||
ChannelOnStatus ChannelStatus = "ON"
|
||||
ChannelOffStatus ChannelStatus = "OFF"
|
||||
)
|
||||
|
||||
func intTotime(t int64) time.Time {
|
||||
@@ -83,13 +102,18 @@ func toGB2312(s string) []byte {
|
||||
}
|
||||
|
||||
// BuildDeviceInfoXML 获取设备详情指令
|
||||
func BuildDeviceInfoXML(sn int, id string) []byte {
|
||||
return toGB2312(fmt.Sprintf(DeviceInfoXML, sn, id))
|
||||
func BuildDeviceInfoXML(sn int, id string, charset string) []byte {
|
||||
return toGB2312(fmt.Sprintf(DeviceInfoXML, charset, sn, id))
|
||||
}
|
||||
|
||||
// BuildDeviceStatusXML 获取设备详情指令
|
||||
func BuildDeviceStatusXML(sn int, id string, charset string) []byte {
|
||||
return toGB2312(fmt.Sprintf(DeviceStatusXML, charset, sn, id))
|
||||
}
|
||||
|
||||
// BuildCatalogXML 获取NVR下设备列表指令
|
||||
func BuildCatalogXML(sn int, id string) []byte {
|
||||
return toGB2312(fmt.Sprintf(CatalogXML, sn, id))
|
||||
func BuildCatalogXML(charset string, sn int, id string) []byte {
|
||||
return toGB2312(fmt.Sprintf(CatalogXML, charset, sn, id))
|
||||
}
|
||||
|
||||
// BuildRecordInfoXML 获取录像文件列表指令
|
||||
@@ -102,6 +126,11 @@ func BuildDevicePositionXML(sn int, id string, interval int) []byte {
|
||||
return toGB2312(fmt.Sprintf(DevicePositionXML, sn, id, interval))
|
||||
}
|
||||
|
||||
// BuildPresetQueryXML 构建预置位查询XML
|
||||
func BuildPresetQueryXML(sn int, id string) []byte {
|
||||
return toGB2312(fmt.Sprintf(PresetQueryXML, sn, id))
|
||||
}
|
||||
|
||||
func BuildAlarmResponseXML(id string) []byte {
|
||||
return toGB2312(fmt.Sprintf(AlarmResponseXML, id))
|
||||
}
|
||||
@@ -111,45 +140,42 @@ func BuildKeepAliveXML(sn int, id string) []byte {
|
||||
}
|
||||
|
||||
type (
|
||||
ChannelStatus string
|
||||
Record struct {
|
||||
DeviceID string
|
||||
Name string
|
||||
FilePath string
|
||||
Address string
|
||||
StartTime string
|
||||
EndTime string
|
||||
Secrecy int
|
||||
Type string
|
||||
}
|
||||
ChannelInfo struct {
|
||||
DeviceID string // 通道ID
|
||||
ParentID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
CivilCode string
|
||||
Address string
|
||||
Port int
|
||||
Parental int
|
||||
SafetyWay int
|
||||
RegisterWay int
|
||||
Secrecy int
|
||||
Status ChannelStatus
|
||||
}
|
||||
Message struct {
|
||||
XMLName xml.Name
|
||||
CmdType string
|
||||
SN int // 请求序列号,一般用于对应 request 和 response
|
||||
DeviceID string
|
||||
DeviceName string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Channel string
|
||||
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
|
||||
RecordList []Record `xml:"RecordList>Item"`
|
||||
SumNum int // 录像结果的总数 SumNum,录像结果会按照多条消息返回,可用于判断是否全部返回
|
||||
XMLName xml.Name
|
||||
CmdType string
|
||||
SN int // 请求序列号,一般用于对应 request 和 response
|
||||
DeviceID string
|
||||
Longitude string // 经度
|
||||
Latitude string // 纬度
|
||||
DeviceName string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Channel string
|
||||
Firmware string
|
||||
DeviceChannelList []DeviceChannel `xml:"DeviceList>Item"`
|
||||
RecordList struct {
|
||||
Num int `xml:"Num,attr"`
|
||||
Item []RecordItem `xml:"Item"`
|
||||
} `xml:"RecordList"`
|
||||
PresetList struct {
|
||||
Num int `xml:"Num,attr"`
|
||||
Item []PresetItem `xml:"Item"`
|
||||
} `xml:"PresetList"`
|
||||
SumNum int // 录像结果的总数 SumNum,录像结果会按照多条消息返回,可用于判断是否全部返回
|
||||
Name string // 设备/通道名称
|
||||
LastTime time.Time `xml:"LastTime"` // 最后时间
|
||||
// 报警相关字段
|
||||
AlarmPriority string `xml:"AlarmPriority"` // 报警级别
|
||||
AlarmMethod string `xml:"AlarmMethod"` // 报警方式
|
||||
AlarmTime string `xml:"AlarmTime"` // 报警时间
|
||||
Info struct {
|
||||
AlarmType string `xml:"AlarmType"` // 报警类型
|
||||
} `xml:"Info"`
|
||||
}
|
||||
|
||||
PresetItem struct {
|
||||
PresetID string `xml:"PresetID"`
|
||||
PresetName string `xml:"PresetName"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
68
plugin/gb28181/pkg/platformchannel.go
Normal file
68
plugin/gb28181/pkg/platformchannel.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package gb28181
|
||||
|
||||
// PlatformChannel 表示平台通道信息
|
||||
type PlatformChannel struct {
|
||||
//CommonGBChannel `gorm:"-"` // 通过组合继承 CommonGBChannel 的字段
|
||||
|
||||
PlatformServerGBID string `gorm:"primaryKey;"` // 平台ID
|
||||
ChannelDBID string `gorm:"primaryKey;"` // 设备通道ID
|
||||
CustomDeviceId string `gorm:"default:null"` // 国标-编码
|
||||
CustomName string `gorm:"default:null"` // 国标-名称
|
||||
CustomManufacturer string `gorm:"default:null"` // 国标-设备厂商
|
||||
CustomModel string `gorm:"default:null"` // 国标-设备型号
|
||||
CustomOwner string `gorm:"default:null"` // 国标-设备归属
|
||||
CustomCivilCode string // 国标-行政区域
|
||||
CustomBlock string // 国标-警区
|
||||
CustomAddress string `gorm:"default:null"` // 国标-安装地址
|
||||
CustomParental int // 国标-是否有子设备
|
||||
CustomParentId string // 国标-父节点ID
|
||||
CustomSafetyWay int // 国标-信令安全模式
|
||||
CustomRegisterWay int // 国标-注册方式
|
||||
CustomCertNum int // 国标-证书序列号
|
||||
CustomCertifiable int // 国标-证书有效标识
|
||||
CustomErrCode int // 国标-无效原因码
|
||||
CustomEndTime int // 国标-证书终止有效期
|
||||
CustomSecurityLevelCode string // 国标-摄像机安全能力等级代码
|
||||
CustomSecrecy int // 国标-保密属性
|
||||
CustomIpAddress string // 国标-设备/系统IPv4/IPv6地址
|
||||
CustomPort int // 国标-设备/系统端口
|
||||
CustomPassword string // 国标-设备口令
|
||||
CustomStatus string // 国标-设备状态
|
||||
CustomLongitude float64 // 国标-经度
|
||||
CustomLatitude float64 // 国标-纬度
|
||||
CustomBusinessGroupId string // 国标-虚拟组织所属的业务分组ID
|
||||
CustomPtzType int // 国标-摄像机结构类型
|
||||
CustomPositionType int // 国标-摄像机位置类型扩展
|
||||
CustomPhotoelectricImagingType string // 国标-摄像机光电成像类型
|
||||
CustomCapturePositionType string // 国标-摄像机采集部位类型
|
||||
CustomRoomType int // 国标-摄像机安装位置室外、室内属性
|
||||
CustomUseType int // 国标-用途属性
|
||||
CustomSupplyLightType int // 国标-摄像机补光属性
|
||||
CustomDirectionType int // 国标-摄像机监视方位属性
|
||||
CustomResolution string // 国标-摄像机支持的分辨率
|
||||
CustomStreamNumberList string // 国标-摄像机支持的码流编号列表
|
||||
CustomDownloadSpeed string // 国标-下载倍速
|
||||
CustomSvcSpaceSupportMod int // 国标-空域编码能力
|
||||
CustomSvcTimeSupportMode int // 国标-时域编码能力
|
||||
CustomSsvcRatioSupportList string // 国标-SSVC增强层与基本层比例能力
|
||||
CustomMobileDeviceType int // 国标-移动采集设备类型
|
||||
CustomHorizontalFieldAngle float64 // 国标-摄像机水平视场角
|
||||
CustomVerticalFieldAngle float64 // 国标-摄像机竖直视场角
|
||||
CustomMaxViewDistance float64 // 国标-摄像机可视距离
|
||||
CustomGrassrootsCode string // 国标-基层组织编码
|
||||
CustomPoType int // 国标-监控点位类型
|
||||
CustomPoCommonName string // 国标-点位俗称
|
||||
CustomMac string // 国标-设备MAC地址
|
||||
CustomFunctionType string // 国标-摄像机卡口功能类型
|
||||
CustomEncodeType string // 国标-摄像机视频编码格式
|
||||
CustomInstallTime string // 国标-摄像机安装使用时间
|
||||
CustomManagementUnit string // 国标-摄像机所属管理单位名称
|
||||
CustomContactInfo string // 国标-摄像机所属管理单位联系方式
|
||||
CustomRecordSaveDays int // 国标-录像保存天数
|
||||
CustomIndustrialClassification string // 国标-国民经济行业分类代码
|
||||
}
|
||||
|
||||
// TableName 返回数据库表名
|
||||
func (*PlatformChannel) TableName() string {
|
||||
return "gb28181_platform_channel"
|
||||
}
|
||||
69
plugin/gb28181/pkg/platformmodel.go
Normal file
69
plugin/gb28181/pkg/platformmodel.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Package gb28181 实现了GB28181协议相关的功能
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlatformModel 表示GB28181平台的配置信息。
|
||||
// 包含了平台的基本信息、SIP服务配置、设备信息、认证信息等。
|
||||
// 用于存储和管理GB28181平台的所有相关参数。
|
||||
type PlatformModel struct {
|
||||
Enable bool `gorm:"column:enable" json:"enable"` // Enable表示该平台配置是否启用
|
||||
Name string `gorm:"column:name;omitempty" json:"name"` // Name表示平台的名称
|
||||
ServerGBID string `gorm:"primaryKey;column:server_gb_id;omitempty" json:"serverGBId"` // ServerGBID表示SIP服务器的国标编码
|
||||
ServerGBDomain string `gorm:"column:server_gb_domain;omitempty" json:"serverGBDomain"` // ServerGBDomain表示SIP服务器的国标域
|
||||
ServerIP string `gorm:"column:server_ip;omitempty" json:"serverIp"` // ServerIP表示SIP服务器的IP地址
|
||||
ServerPort int `gorm:"column:server_port;omitempty" json:"serverPort"` // ServerPort表示SIP服务器的端口号
|
||||
DeviceGBID string `gorm:"column:device_gb_id;omitempty" json:"deviceGBId"` // DeviceGBID表示设备的国标编号
|
||||
DeviceIP string `gorm:"column:device_ip;omitempty" json:"deviceIp"` // DeviceIP表示设备的IP地址
|
||||
DevicePort int `gorm:"column:device_port;omitempty" json:"devicePort"` // DevicePort表示设备的端口号
|
||||
Username string `gorm:"column:username;omitempty" json:"username"` // Username表示SIP认证的用户名,默认使用设备国标编号
|
||||
Password string `gorm:"column:password;omitempty" json:"password"` // Password表示SIP认证的密码
|
||||
Expires int `gorm:"column:expires;omitempty" json:"expires"` // Expires表示注册的过期时间,单位为秒
|
||||
KeepTimeout int `gorm:"column:keep_timeout;omitempty" json:"keepTimeout"` // KeepTimeout表示心跳超时时间,单位为秒
|
||||
Transport string `gorm:"column:transport;omitempty" json:"transport"` // Transport表示传输协议类型
|
||||
CharacterSet string `gorm:"column:character_set;omitempty" json:"characterSet"` // CharacterSet表示字符集编码
|
||||
PTZ bool `gorm:"column:ptz" json:"ptz"` // PTZ表示是否允许云台控制
|
||||
RTCP bool `gorm:"column:rtcp" json:"rtcp"` // RTCP表示是否启用RTCP流保活
|
||||
Status bool `gorm:"column:status" json:"status"` // Status表示平台当前的在线状态
|
||||
ChannelCount int `gorm:"column:channel_count;omitempty" json:"channelCount"` // ChannelCount表示通道数量
|
||||
CatalogSubscribe bool `gorm:"column:catalog_subscribe" json:"catalogSubscribe"` // CatalogSubscribe表示是否已订阅目录信息
|
||||
AlarmSubscribe bool `gorm:"column:alarm_subscribe" json:"alarmSubscribe"` // AlarmSubscribe表示是否已订阅报警信息
|
||||
MobilePositionSubscribe bool `gorm:"column:mobile_position_subscribe" json:"mobilePositionSubscribe"` // MobilePositionSubscribe表示是否已订阅移动位置信息
|
||||
CatalogGroup int `gorm:"column:catalog_group;omitempty" json:"catalogGroup"` // CatalogGroup表示目录分组大小,每次向上级发送通道数量
|
||||
UpdateTime string `gorm:"column:update_time;omitempty" json:"updateTime"` // UpdateTime表示最后更新时间
|
||||
CreateTime string `gorm:"column:create_time;omitempty" json:"createTime"` // CreateTime表示创建时间
|
||||
AsMessageChannel bool `gorm:"column:as_message_channel" json:"asMessageChannel"` // AsMessageChannel表示是否作为消息通道使用
|
||||
SendStreamIP string `gorm:"column:send_stream_ip;omitempty" json:"sendStreamIp"` // SendStreamIP表示点播回复200OK时使用的IP地址
|
||||
AutoPushChannel bool `gorm:"column:auto_push_channel" json:"autoPushChannel"` // AutoPushChannel表示是否自动推送通道变化
|
||||
CatalogWithPlatform int `gorm:"column:catalog_with_platform;omitempty" json:"catalogWithPlatform"` // CatalogWithPlatform表示目录信息是否包含平台信息(0:关闭,1:打开)
|
||||
CatalogWithGroup int `gorm:"column:catalog_with_group;omitempty" json:"catalogWithGroup"` // CatalogWithGroup表示目录信息是否包含分组信息(0:关闭,1:打开)
|
||||
CatalogWithRegion int `gorm:"column:catalog_with_region;omitempty" json:"catalogWithRegion"` // CatalogWithRegion表示目录信息是否包含行政区划(0:关闭,1:打开)
|
||||
CivilCode string `gorm:"column:civil_code;omitempty" json:"civilCode"` // CivilCode表示行政区划代码
|
||||
Manufacturer string `gorm:"column:manufacturer;omitempty" json:"manufacturer"` // Manufacturer表示平台厂商
|
||||
Model string `gorm:"column:model;omitempty" json:"model"` // Model表示平台型号
|
||||
Address string `gorm:"column:address;omitempty" json:"address"` // Address表示平台安装地址
|
||||
RegisterWay int `gorm:"column:register_way;omitempty" json:"registerWay"` // RegisterWay表示注册方式(1:标准认证注册,2:口令认证,3:数字证书双向认证,4:数字证书单向认证)
|
||||
Secrecy int `gorm:"column:secrecy;omitempty" json:"secrecy"` // Secrecy表示保密属性(0:不涉密,1:涉密)
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (p *PlatformModel) TableName() string {
|
||||
return "gb28181_platform"
|
||||
}
|
||||
|
||||
// NewPlatform 创建并返回一个新的Platform实例。
|
||||
// 该函数会初始化Platform结构体,并设置一些默认值:
|
||||
// - RegisterWay默认设置为1(标准认证注册模式)
|
||||
// - Secrecy默认设置为0(不涉密)
|
||||
// 返回值为指向新创建的Platform实例的指针。
|
||||
func NewPlatform() *PlatformModel {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
return &PlatformModel{
|
||||
RegisterWay: 1, // 默认使用标准认证注册模式
|
||||
Secrecy: 0, // 默认为不涉密
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
}
|
||||
117
plugin/gb28181/pkg/record_info.go
Normal file
117
plugin/gb28181/pkg/record_info.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package gb28181
|
||||
|
||||
import "time"
|
||||
|
||||
// RecordInfo 设备录像信息
|
||||
type RecordInfo struct {
|
||||
// 设备编号
|
||||
DeviceID string `json:"deviceId"`
|
||||
|
||||
// 通道编号
|
||||
ChannelID string `json:"channelId"`
|
||||
|
||||
// 命令序列号
|
||||
SN string `json:"sn"`
|
||||
|
||||
// 设备名称
|
||||
Name string `json:"name"`
|
||||
|
||||
// 列表总数
|
||||
SumNum int `json:"sumNum"`
|
||||
|
||||
// 计数
|
||||
Count int `json:"count"`
|
||||
|
||||
// 最后时间
|
||||
LastTime time.Time `json:"lastTime"`
|
||||
|
||||
// 录像列表
|
||||
RecordList []RecordItem `json:"recordList"`
|
||||
}
|
||||
|
||||
// NewRecordInfo 创建新的 RecordInfo 实例
|
||||
func NewRecordInfo() *RecordInfo {
|
||||
return &RecordInfo{
|
||||
RecordList: make([]RecordItem, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeviceID 获取设备编号
|
||||
func (r *RecordInfo) GetDeviceID() string {
|
||||
return r.DeviceID
|
||||
}
|
||||
|
||||
// SetDeviceID 设置设备编号
|
||||
func (r *RecordInfo) SetDeviceID(deviceID string) {
|
||||
r.DeviceID = deviceID
|
||||
}
|
||||
|
||||
// GetName 获取设备名称
|
||||
func (r *RecordInfo) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
// SetName 设置设备名称
|
||||
func (r *RecordInfo) SetName(name string) {
|
||||
r.Name = name
|
||||
}
|
||||
|
||||
// GetSumNum 获取列表总数
|
||||
func (r *RecordInfo) GetSumNum() int {
|
||||
return r.SumNum
|
||||
}
|
||||
|
||||
// SetSumNum 设置列表总数
|
||||
func (r *RecordInfo) SetSumNum(sumNum int) {
|
||||
r.SumNum = sumNum
|
||||
}
|
||||
|
||||
// GetRecordList 获取录像列表
|
||||
func (r *RecordInfo) GetRecordList() []RecordItem {
|
||||
return r.RecordList
|
||||
}
|
||||
|
||||
// SetRecordList 设置录像列表
|
||||
func (r *RecordInfo) SetRecordList(recordList []RecordItem) {
|
||||
r.RecordList = recordList
|
||||
}
|
||||
|
||||
// GetChannelID 获取通道编号
|
||||
func (r *RecordInfo) GetChannelID() string {
|
||||
return r.ChannelID
|
||||
}
|
||||
|
||||
// SetChannelID 设置通道编号
|
||||
func (r *RecordInfo) SetChannelID(channelID string) {
|
||||
r.ChannelID = channelID
|
||||
}
|
||||
|
||||
// GetSN 获取命令序列号
|
||||
func (r *RecordInfo) GetSN() string {
|
||||
return r.SN
|
||||
}
|
||||
|
||||
// SetSN 设置命令序列号
|
||||
func (r *RecordInfo) SetSN(sn string) {
|
||||
r.SN = sn
|
||||
}
|
||||
|
||||
// GetLastTime 获取最后时间
|
||||
func (r *RecordInfo) GetLastTime() time.Time {
|
||||
return r.LastTime
|
||||
}
|
||||
|
||||
// SetLastTime 设置最后时间
|
||||
func (r *RecordInfo) SetLastTime(lastTime time.Time) {
|
||||
r.LastTime = lastTime
|
||||
}
|
||||
|
||||
// GetCount 获取计数
|
||||
func (r *RecordInfo) GetCount() int {
|
||||
return r.Count
|
||||
}
|
||||
|
||||
// SetCount 设置计数
|
||||
func (r *RecordInfo) SetCount(count int) {
|
||||
r.Count = count
|
||||
}
|
||||
77
plugin/gb28181/pkg/record_item.go
Normal file
77
plugin/gb28181/pkg/record_item.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RecordItem 设备录像信息
|
||||
type RecordItem struct {
|
||||
// 设备编号
|
||||
DeviceID string `xml:"DeviceID" json:"deviceId"`
|
||||
|
||||
// 名称
|
||||
Name string `xml:"Name" json:"name"`
|
||||
|
||||
// 文件路径名 (可选)
|
||||
FilePath string `xml:"FilePath" json:"filePath"`
|
||||
|
||||
// 录像文件大小,单位:Byte(可选)
|
||||
FileSize string `xml:"FileSize" json:"fileSize"`
|
||||
|
||||
// 录像地址(可选)
|
||||
Address string `xml:"Address" json:"address"`
|
||||
|
||||
// 录像开始时间(可选)
|
||||
StartTime string `xml:"StartTime" json:"startTime"`
|
||||
|
||||
// 录像结束时间(可选)
|
||||
EndTime string `xml:"EndTime" json:"endTime"`
|
||||
|
||||
// 保密属性(必选)缺省为0;0:不涉密,1:涉密
|
||||
Secrecy int `xml:"Secrecy" json:"secrecy"`
|
||||
|
||||
// 录像产生类型(可选)time或alarm或manual
|
||||
Type string `xml:"Type" json:"type"`
|
||||
|
||||
// 录像触发者ID(可选)
|
||||
RecorderID string `xml:"RecorderId" json:"recorderId"`
|
||||
}
|
||||
|
||||
// CompareTo 比较两个录像记录的开始时间
|
||||
// 返回值:
|
||||
// -1: r < other
|
||||
//
|
||||
// 0: r = other
|
||||
// 1: r > other
|
||||
func (r *RecordItem) CompareTo(other *RecordItem) int {
|
||||
startTimeNow, err := time.Parse("2006-01-02T15:04:05", r.StartTime)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
startTimeParam, err := time.Parse("2006-01-02T15:04:05", other.StartTime)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if startTimeNow.Equal(startTimeParam) {
|
||||
return 0
|
||||
} else if startTimeParam.After(startTimeNow) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Less 用于排序
|
||||
func (r *RecordItem) Less(other *RecordItem) bool {
|
||||
return r.CompareTo(other) < 0
|
||||
}
|
||||
|
||||
// Equal 判断两个录像记录是否相等
|
||||
func (r *RecordItem) Equal(other *RecordItem) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(r.StartTime, other.StartTime)
|
||||
}
|
||||
110
plugin/gb28181/pkg/region.go
Normal file
110
plugin/gb28181/pkg/region.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Region 区域信息
|
||||
type Region struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // 数据库自增ID
|
||||
DeviceID string `gorm:"column:device_id" json:"deviceId"` // 区域国标编号
|
||||
Name string `gorm:"column:name" json:"name"` // 区域名称
|
||||
ParentID int `gorm:"column:parent_id" json:"parentId"` // 父区域ID
|
||||
ParentDeviceID string `gorm:"column:parent_device_id" json:"parentDeviceId"` // 父区域国标ID
|
||||
BusinessGroup string `gorm:"column:business_group" json:"businessGroup"` // 所属的业务分组国标编号
|
||||
CreateTime string `gorm:"column:create_time" json:"createTime"` // 创建时间
|
||||
UpdateTime string `gorm:"column:update_time" json:"updateTime"` // 更新时间
|
||||
CivilCode string `gorm:"column:civil_code" json:"civilCode"` // 行政区划
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (r *Region) TableName() string {
|
||||
return "region_gb28181pro"
|
||||
}
|
||||
|
||||
// NewRegion 创建新的区域实例
|
||||
func NewRegion(deviceID, name, parentDeviceID string) *Region {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
return &Region{
|
||||
DeviceID: deviceID,
|
||||
Name: name,
|
||||
ParentDeviceID: parentDeviceID,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRegionFromCivilCode 从行政区划编码创建区域实例
|
||||
func NewRegionFromCivilCode(civilCode *CivilCode) *Region {
|
||||
if civilCode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
region := &Region{
|
||||
Name: civilCode.Name,
|
||||
DeviceID: civilCode.Code,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
// 如果编码长度大于2,设置父级编码
|
||||
if len(civilCode.Code) > 2 {
|
||||
region.ParentDeviceID = civilCode.ParentCode
|
||||
}
|
||||
|
||||
return region
|
||||
}
|
||||
|
||||
// NewRegionFromChannel 从设备通道创建区域实例
|
||||
func NewRegionFromChannel(channel *DeviceChannel) *Region {
|
||||
if channel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
region := &Region{
|
||||
Name: channel.Name,
|
||||
DeviceID: channel.DeviceID,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
// 获取父级编码
|
||||
parentCode := GetInstance().GetParentCode(channel.DeviceID)
|
||||
if parentCode != nil {
|
||||
region.ParentDeviceID = parentCode.Code
|
||||
}
|
||||
|
||||
return region
|
||||
}
|
||||
|
||||
// CompareTo 实现比较功能
|
||||
func (r *Region) CompareTo(other *Region) int {
|
||||
thisID, _ := strconv.Atoi(r.DeviceID)
|
||||
otherID, _ := strconv.Atoi(other.DeviceID)
|
||||
return thisID - otherID
|
||||
}
|
||||
|
||||
// Equals 判断两个区域是否相等
|
||||
func (r *Region) Equals(other interface{}) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
if r == other {
|
||||
return true
|
||||
}
|
||||
otherRegion, ok := other.(*Region)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return r.ID == otherRegion.ID
|
||||
}
|
||||
|
||||
// GetParentCode 获取父级编码(这个函数需要在 civilcodeutil.go 中实现)
|
||||
func GetParentCode(deviceID string) *CivilCode {
|
||||
// TODO: 实现获取父级编码的逻辑
|
||||
// 这部分需要参考 CivilCodeUtil.java 的实现
|
||||
return nil
|
||||
}
|
||||
193
plugin/gb28181/pkg/sdputil.go
Normal file
193
plugin/gb28181/pkg/sdputil.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
)
|
||||
|
||||
// DecodeSDP 从 SIP 请求中解析 SDP 信息
|
||||
func DecodeSDP(req *sip.Request) (*InviteInfo, error) {
|
||||
inviteInfo := NewInviteInfo()
|
||||
|
||||
// 获取请求者ID
|
||||
from := req.From()
|
||||
if from == nil || from.Address.User == "" {
|
||||
return nil, fmt.Errorf("无法从请求中获取来源id")
|
||||
}
|
||||
inviteInfo.RequesterId = from.Address.User
|
||||
|
||||
// 获取目标通道ID
|
||||
channelIDArray := getChannelIDFromRequest(req)
|
||||
|
||||
// 获取CallID
|
||||
callID := req.CallID()
|
||||
if callID != nil {
|
||||
inviteInfo.CallId = callID.Value()
|
||||
}
|
||||
|
||||
// 解析SDP消息
|
||||
sdpStr := string(req.Body())
|
||||
if sdpStr == "" {
|
||||
return nil, fmt.Errorf("SDP内容为空")
|
||||
}
|
||||
|
||||
// 解析SDP各个字段
|
||||
lines := strings.Split(sdpStr, "\r\n")
|
||||
var channelIdFromSdp string
|
||||
var port int = -1
|
||||
var mediaTransmissionTCP bool
|
||||
var tcpActive *bool
|
||||
var supportedMediaFormat bool
|
||||
var sessionName string
|
||||
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "s="):
|
||||
sessionName = strings.TrimPrefix(line, "s=")
|
||||
inviteInfo.SessionName = sessionName
|
||||
|
||||
// 如果是回放,从URI中获取通道ID
|
||||
if strings.EqualFold(sessionName, "Playback") {
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "u=") {
|
||||
uriField := strings.TrimPrefix(l, "u=")
|
||||
parts := strings.Split(uriField, ":")
|
||||
if len(parts) > 0 {
|
||||
channelIdFromSdp = parts[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "c="):
|
||||
// c=IN IP4 192.168.1.100
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) >= 3 {
|
||||
inviteInfo.IP = parts[2]
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "t="):
|
||||
// t=开始时间 结束时间
|
||||
parts := strings.Split(strings.TrimPrefix(line, "t="), " ")
|
||||
if len(parts) >= 2 {
|
||||
startTime, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err == nil {
|
||||
inviteInfo.StartTime = startTime
|
||||
}
|
||||
stopTime, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err == nil {
|
||||
inviteInfo.StopTime = stopTime
|
||||
}
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "m="):
|
||||
mediaDesc := strings.Split(strings.TrimPrefix(line, "m="), " ")
|
||||
if len(mediaDesc) >= 4 { // 必须有足够的元素:类型、端口、传输协议和格式
|
||||
portVal, err := strconv.Atoi(mediaDesc[1])
|
||||
if err == nil {
|
||||
port = portVal
|
||||
}
|
||||
|
||||
// 检查传输协议
|
||||
if strings.EqualFold(mediaDesc[2], "TCP/RTP/AVP") {
|
||||
mediaTransmissionTCP = true
|
||||
}
|
||||
|
||||
// 检查是否包含支持的媒体格式:96或8
|
||||
for i := 3; i < len(mediaDesc); i++ {
|
||||
if mediaDesc[i] == "96" || mediaDesc[i] == "8" {
|
||||
supportedMediaFormat = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "a=setup:"):
|
||||
val := strings.TrimPrefix(line, "a=setup:")
|
||||
if strings.EqualFold(val, "active") {
|
||||
activeVal := true
|
||||
tcpActive = &activeVal
|
||||
} else if strings.EqualFold(val, "passive") {
|
||||
passiveVal := false
|
||||
tcpActive = &passiveVal
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "y="):
|
||||
inviteInfo.SSRC = strings.TrimPrefix(line, "y=")
|
||||
|
||||
case strings.HasPrefix(line, "a=downloadspeed:"):
|
||||
inviteInfo.DownloadSpeed = strings.TrimPrefix(line, "a=downloadspeed:")
|
||||
}
|
||||
}
|
||||
|
||||
// 确定最终的通道ID,优先使用SDP中的通道ID
|
||||
var finalChannelId string
|
||||
if channelIdFromSdp != "" {
|
||||
finalChannelId = channelIdFromSdp
|
||||
} else if len(channelIDArray) > 0 {
|
||||
finalChannelId = channelIDArray[0]
|
||||
}
|
||||
|
||||
// 验证通道ID和请求者ID
|
||||
if inviteInfo.RequesterId == "" || finalChannelId == "" {
|
||||
return nil, fmt.Errorf("无法从请求中获取通道id或来源id")
|
||||
}
|
||||
|
||||
// 设置目标通道ID
|
||||
inviteInfo.TargetChannelId = finalChannelId
|
||||
|
||||
// 设置源通道ID(如果有)
|
||||
if len(channelIDArray) >= 2 {
|
||||
inviteInfo.SourceChannelId = channelIDArray[1]
|
||||
}
|
||||
|
||||
// 验证媒体格式支持
|
||||
if port == -1 || !supportedMediaFormat {
|
||||
return nil, fmt.Errorf("不支持的媒体格式")
|
||||
}
|
||||
|
||||
// 设置传输相关信息
|
||||
inviteInfo.TCP = mediaTransmissionTCP
|
||||
if tcpActive != nil {
|
||||
inviteInfo.TCPActive = *tcpActive
|
||||
} else {
|
||||
inviteInfo.TCPActive = false // 默认值
|
||||
}
|
||||
inviteInfo.Port = port
|
||||
|
||||
return inviteInfo, nil
|
||||
}
|
||||
|
||||
// getChannelIDFromRequest 从请求中获取通道ID
|
||||
func getChannelIDFromRequest(req *sip.Request) []string {
|
||||
subjectHeaders := req.GetHeaders("Subject")
|
||||
if len(subjectHeaders) == 0 {
|
||||
// 如果缺失subject
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取第一个Subject头部的值
|
||||
subjectStr := subjectHeaders[0].Value()
|
||||
|
||||
result := make([]string, 2)
|
||||
|
||||
if strings.Contains(subjectStr, ",") {
|
||||
subjectSplit := strings.Split(subjectStr, ",")
|
||||
result[0] = strings.Split(subjectSplit[0], ":")[0]
|
||||
if len(subjectSplit) > 1 {
|
||||
result[1] = strings.Split(subjectSplit[1], ":")[0]
|
||||
}
|
||||
} else {
|
||||
result[0] = strings.Split(subjectStr, ":")[0]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package gb28181
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"m7s.live/v5"
|
||||
@@ -43,6 +45,7 @@ type Receiver struct {
|
||||
RTPReader *rtp2.TCP
|
||||
ListenAddr string
|
||||
listener net.Listener
|
||||
StreamMode string // 数据流传输模式(UDP:udp传输/TCP-ACTIVE:tcp主动模式/TCP-PASSIVE:tcp被动模式)
|
||||
}
|
||||
|
||||
func NewPSPublisher(puber *m7s.Publisher) *PSPublisher {
|
||||
@@ -141,39 +144,71 @@ func (dec *PSPublisher) decProgramStreamMap() (err error) {
|
||||
func (p *Receiver) ReadRTP(rtp util.Buffer) (err error) {
|
||||
lastSeq := p.SequenceNumber
|
||||
if err = p.Unmarshal(rtp); err != nil {
|
||||
p.Error("unmarshal error", "err", err)
|
||||
return
|
||||
}
|
||||
if p.SequenceNumber != lastSeq+1 {
|
||||
return ErrRTPReceiveLost
|
||||
if lastSeq == 0 || p.SequenceNumber == lastSeq+1 {
|
||||
if p.Enabled(p, task.TraceLevel) {
|
||||
p.Trace("rtp", "len", rtp.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
|
||||
}
|
||||
copyData := make([]byte, len(p.Payload))
|
||||
copy(copyData, p.Payload)
|
||||
p.FeedChan <- copyData
|
||||
return
|
||||
}
|
||||
if p.Enabled(p, task.TraceLevel) {
|
||||
p.Trace("rtp", "len", rtp.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
|
||||
}
|
||||
copyData := make([]byte, len(p.Payload))
|
||||
copy(copyData, p.Payload)
|
||||
p.FeedChan <- copyData
|
||||
return
|
||||
return ErrRTPReceiveLost
|
||||
}
|
||||
|
||||
func (p *Receiver) Start() (err error) {
|
||||
p.listener, err = net.Listen("tcp", p.ListenAddr)
|
||||
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
|
||||
// TCP主动模式不需要监听,直接返回
|
||||
p.Info("TCP-ACTIVE mode, no need to listen")
|
||||
return nil
|
||||
}
|
||||
// TCP被动模式
|
||||
p.listener, err = net.Listen("tcp4", p.ListenAddr)
|
||||
if err != nil {
|
||||
p.Error("start listen", "err", err)
|
||||
return
|
||||
return errors.New("start listen,err" + err.Error())
|
||||
}
|
||||
p.Info("start listen", "addr", p.ListenAddr)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Receiver) Dispose() {
|
||||
p.listener.Close()
|
||||
if p.listener != nil {
|
||||
p.listener.Close()
|
||||
}
|
||||
if p.RTPReader != nil {
|
||||
p.RTPReader.Close()
|
||||
}
|
||||
//close(p.FeedChan)
|
||||
if p.FeedChan != nil {
|
||||
close(p.FeedChan)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Receiver) Go() error {
|
||||
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
|
||||
// TCP主动模式,主动连接设备
|
||||
addr := p.ListenAddr
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = ":" + addr
|
||||
}
|
||||
if strings.HasPrefix(addr, ":") {
|
||||
p.Error("invalid address, missing IP", "addr", addr)
|
||||
return fmt.Errorf("invalid address %s, missing IP", addr)
|
||||
}
|
||||
p.Info("TCP-ACTIVE mode, connecting to device", "addr", addr)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
p.Error("connect to device failed", "err", err)
|
||||
return err
|
||||
}
|
||||
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
|
||||
p.Info("connected to device", "addr", conn.RemoteAddr())
|
||||
return p.RTPReader.Read(p.ReadRTP)
|
||||
}
|
||||
// TCP被动模式
|
||||
p.Info("start accept")
|
||||
conn, err := p.listener.Accept()
|
||||
if err != nil {
|
||||
|
||||
1377
plugin/gb28181/platform.go
Normal file
1377
plugin/gb28181/platform.go
Normal file
File diff suppressed because it is too large
Load Diff
45
plugin/gb28181/positionsub.go
Normal file
45
plugin/gb28181/positionsub.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
// PositionSubscribeTask 位置订阅任务
|
||||
type PositionSubscribeTask struct {
|
||||
task.TickTask
|
||||
device *Device
|
||||
}
|
||||
|
||||
// NewPositionSubscribeTask 创建新的位置订阅任务
|
||||
func NewPositionSubscribeTask(device *Device) *PositionSubscribeTask {
|
||||
return &PositionSubscribeTask{
|
||||
device: device,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTickInterval 获取定时间隔
|
||||
func (p *PositionSubscribeTask) GetTickInterval() time.Duration {
|
||||
// 如果设备配置了位置订阅周期,则使用设备配置的周期,否则使用默认值3600秒
|
||||
if p.device.SubscribePosition > 0 {
|
||||
return time.Second * time.Duration(p.device.SubscribePosition)
|
||||
}
|
||||
return time.Second * 3600
|
||||
}
|
||||
|
||||
// Tick 定时执行的方法
|
||||
func (p *PositionSubscribeTask) Tick(any) {
|
||||
// 执行位置订阅,使用设备配置的位置间隔,如果未配置则使用默认值6
|
||||
interval := 6
|
||||
if p.device.PositionInterval > 0 {
|
||||
interval = p.device.PositionInterval
|
||||
}
|
||||
|
||||
response, err := p.device.subscribePosition(interval)
|
||||
if err != nil {
|
||||
p.Error("subPosition", "err", err)
|
||||
} else {
|
||||
p.Debug("subPosition", "response", response.String())
|
||||
}
|
||||
}
|
||||
69
plugin/gb28181/recordinfo.go
Normal file
69
plugin/gb28181/recordinfo.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
// RecordInfoQuery 发送录像查询请求
|
||||
// startTime 和 endTime 的格式为 "2006-01-02 15:04:05"
|
||||
func (gb *GB28181Plugin) RecordInfoQuery(deviceID string, channelID string, startTime time.Time, endTime time.Time, sn int) (*util.Promise, error) {
|
||||
device, ok := gb.devices.Get(deviceID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("device not found: %s", deviceID)
|
||||
}
|
||||
|
||||
channel, ok := device.channels.Get(channelID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("channel not found: %s", channelID)
|
||||
}
|
||||
|
||||
// 构建XML消息
|
||||
charset := "GB2312"
|
||||
if device.Charset != "" {
|
||||
charset = device.Charset
|
||||
}
|
||||
|
||||
msgBody := fmt.Sprintf(`<?xml version="1.0" encoding="%s"?>
|
||||
<Query>
|
||||
<CmdType>RecordInfo</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<StartTime>%s</StartTime>
|
||||
<EndTime>%s</EndTime>
|
||||
<Secrecy>0</Secrecy>
|
||||
<Type>all</Type>
|
||||
</Query>`, charset, sn, channelID, startTime.Format("2006-01-02T15:04:05"), endTime.Format("2006-01-02T15:04:05"))
|
||||
|
||||
// 创建 MESSAGE 请求
|
||||
request := device.CreateRequest(sip.MESSAGE, nil)
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("create request failed")
|
||||
}
|
||||
|
||||
// 设置消息体
|
||||
request.SetBody([]byte(msgBody))
|
||||
|
||||
// 创建Promise并保存到channel的RecordReqs中
|
||||
promise := util.NewPromise(context.Background())
|
||||
recordReq := &RecordRequest{
|
||||
SN: sn,
|
||||
Promise: promise,
|
||||
}
|
||||
|
||||
// 先保存请求到RecordReqs,确保能接收到响应
|
||||
channel.RecordReqs.Set(recordReq)
|
||||
|
||||
// 发送请求
|
||||
_, err := device.send(request)
|
||||
if err != nil {
|
||||
channel.RecordReqs.Remove(recordReq)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return promise, nil
|
||||
}
|
||||
70
plugin/gb28181/register.go
Normal file
70
plugin/gb28181/register.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package plugin_gb28181pro
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
type Register struct {
|
||||
task.TickTask
|
||||
platform *Platform
|
||||
registerType string
|
||||
seconds time.Duration
|
||||
platformKeepAliveTask *PlatformKeepAliveTask
|
||||
}
|
||||
|
||||
func NewRegister(platform *Platform, registerType string) *Register {
|
||||
register := &Register{
|
||||
registerType: registerType,
|
||||
}
|
||||
register.platform = platform
|
||||
if registerType == "firstRegister" {
|
||||
register.seconds = time.Second * 60
|
||||
} else {
|
||||
register.seconds = time.Second * time.Duration(platform.PlatformModel.Expires)
|
||||
}
|
||||
return register
|
||||
}
|
||||
|
||||
func (r *Register) GetTickInterval() time.Duration {
|
||||
return r.seconds
|
||||
}
|
||||
|
||||
func (r *Register) Tick(any) {
|
||||
r.Register()
|
||||
}
|
||||
|
||||
func (r *Register) Register() {
|
||||
if err := r.platform.DoRegister(); err != nil {
|
||||
if r.registerType == "keepaliveRegister" { //保活注册失败,需要回到首次注册类型
|
||||
r.Error("keepaliveRegister err", err, "register type is ", r.registerType, "DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
|
||||
//r.platform.eventChan <- r
|
||||
r.platformKeepAliveTask.Stop(errors.New("keepaliveRegister failed,start to firstRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
|
||||
r.Ticker.Reset(time.Second * 60)
|
||||
//register := NewRegister(r.platform, "firstRegister")
|
||||
//r.platform.AddTask(register)
|
||||
//r.Stop(errors.New("keepaliveRegister failed,start to firstRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
|
||||
}
|
||||
} else {
|
||||
if r.registerType == "firstRegister" {
|
||||
r.platform.Info("firstRegister success", "register type is ", r.registerType, "DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
|
||||
//r.platform.eventChan <- r
|
||||
//register := NewRegister(r.platform, "keepaliveRegister")
|
||||
//r.platform.AddTask(register)
|
||||
|
||||
pat := PlatformKeepAliveTask{
|
||||
platform: r.platform,
|
||||
}
|
||||
r.platformKeepAliveTask = &pat
|
||||
r.platform.AddTask(&pat)
|
||||
r.Ticker.Reset(time.Second * time.Duration(r.platform.PlatformModel.Expires))
|
||||
//r.Stop(errors.New("firstRegister success,start to keepaliveRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//func (r *Register) Dispose() {
|
||||
// r.platform.Info("into dispose,DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
|
||||
//}
|
||||
@@ -21,6 +21,8 @@
|
||||
width: 70%;
|
||||
padding: 8px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -29,6 +31,7 @@
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@@ -48,32 +51,623 @@
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 2px dashed #4CAF50;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
background-color: #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drop-zone.drag-over {
|
||||
background-color: #e8f5e9;
|
||||
border-color: #2e7d32;
|
||||
}
|
||||
|
||||
.drop-zone p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="input-container">
|
||||
<input type="text" id="m3u8Url" placeholder="输入 M3U8 地址">
|
||||
<button onclick="loadM3U8()">加载</button>
|
||||
<input type="text" id="m3u8Url" placeholder="输入 M3U8 地址"
|
||||
value="http://localhost:8080/hls/vod/fmp4.m3u8?start=1740116409&streamPath=live/test">
|
||||
<button onclick="loadM3U8()">加载 M3U8</button>
|
||||
|
||||
<div id="m3u8Content"
|
||||
style="margin: 10px 0; padding: 10px; background-color: #f0f0f0; border: 1px solid #ddd; font-family: monospace; white-space: pre; max-height: 200px; overflow-y: auto;">
|
||||
</div>
|
||||
|
||||
<input type="text" id="fmp4Url" placeholder="输入 FMP4 地址">
|
||||
<button onclick="testFMP4()">测试 FMP4</button>
|
||||
|
||||
<input type="text" id="wsUrl" placeholder="输入 WebSocket 地址" value="ws://localhost:8080/mp4/live/test.mp4">
|
||||
<button onclick="connectWebSocket()">连接 WebSocket</button>
|
||||
|
||||
<div style="margin: 10px 0;">
|
||||
<label for="bufferCount">缓存包数量: <span id="bufferCountValue">1</span></label>
|
||||
<input type="range" id="bufferCount" min="1" max="50" value="1" style="width: 200px; margin-left: 10px;">
|
||||
</div>
|
||||
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<p>拖放 FMP4 文件到这里<br>或点击选择文件</p>
|
||||
<input type="file" id="fileInput" style="display: none" accept=".mp4,.fmp4">
|
||||
</div>
|
||||
</div>
|
||||
<video id="videoPlayer" controls></video>
|
||||
<video id="videoPlayer" controls autoplay></video>
|
||||
<div id="debug"></div>
|
||||
|
||||
<script>
|
||||
// MSE Player Class
|
||||
class MSEPlayer {
|
||||
constructor(videoElement, onLog = console.log) {
|
||||
this.video = videoElement;
|
||||
this.mediaSource = null;
|
||||
this.sourceBuffer = null;
|
||||
this.pendingBuffers = [];
|
||||
this.isBuffering = false;
|
||||
this.onLog = onLog;
|
||||
this.codecConfigs = [
|
||||
// 'video/mp4; codecs="avc1.4d001f, mp4a.40.2"',
|
||||
'video/mp4; codecs="avc1.4d001f"',
|
||||
'video/mp4'
|
||||
];
|
||||
this.MAX_BUFFER_LENGTH = 30;
|
||||
this.hasError = false;
|
||||
this.isDestroyed = false;
|
||||
this.retryCount = 0;
|
||||
this.MAX_RETRIES = 3;
|
||||
this.isSourceBufferReady = false;
|
||||
this.hasMetadata = false;
|
||||
}
|
||||
|
||||
log(message) {
|
||||
this.onLog(message);
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.mediaSource) {
|
||||
if (this.mediaSource.readyState === 'open') {
|
||||
try {
|
||||
// 等待 SourceBuffer 更新完成
|
||||
if (this.sourceBuffer && this.sourceBuffer.updating) {
|
||||
await new Promise((resolve) => {
|
||||
const onUpdate = () => {
|
||||
this.sourceBuffer.removeEventListener('updateend', onUpdate);
|
||||
resolve();
|
||||
};
|
||||
this.sourceBuffer.addEventListener('updateend', onUpdate);
|
||||
});
|
||||
}
|
||||
// 不在这里调用 endOfStream,而是等待视频元数据加载完成
|
||||
} catch (e) {
|
||||
this.log(`清理旧的 MediaSource 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
URL.revokeObjectURL(this.video.src);
|
||||
this.log('清理旧的 MediaSource');
|
||||
}
|
||||
|
||||
this.mediaSource = new MediaSource();
|
||||
this.video.src = URL.createObjectURL(this.mediaSource);
|
||||
this.pendingBuffers = [];
|
||||
this.isBuffering = false;
|
||||
this.hasError = false;
|
||||
this.isDestroyed = false;
|
||||
this.retryCount = 0;
|
||||
this.isSourceBufferReady = false;
|
||||
this.hasMetadata = false;
|
||||
|
||||
// 监听视频元数据加载事件
|
||||
this.video.addEventListener('loadedmetadata', () => {
|
||||
this.hasMetadata = true;
|
||||
this.log('视频元数据已加载');
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId;
|
||||
let sourceOpenHandler, errorHandler;
|
||||
|
||||
sourceOpenHandler = async () => {
|
||||
this.log('MediaSource 已打开');
|
||||
clearTimeout(timeoutId);
|
||||
try {
|
||||
await this.initSourceBuffer();
|
||||
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
|
||||
this.mediaSource.removeEventListener('error', errorHandler);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
this.log(`初始化失败: ${error.message}`);
|
||||
this.handleError(error.message);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
errorHandler = (e) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.log(`MediaSource 错误: ${e}`);
|
||||
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
|
||||
this.mediaSource.removeEventListener('error', errorHandler);
|
||||
this.handleError(e);
|
||||
reject(e);
|
||||
};
|
||||
|
||||
this.mediaSource.addEventListener('sourceopen', sourceOpenHandler);
|
||||
this.mediaSource.addEventListener('error', errorHandler);
|
||||
|
||||
// 添加超时处理
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!this.isDestroyed && this.mediaSource && this.mediaSource.readyState !== 'open') {
|
||||
const error = new Error('MediaSource 打开超时');
|
||||
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
|
||||
this.mediaSource.removeEventListener('error', errorHandler);
|
||||
this.handleError(error.message);
|
||||
reject(error);
|
||||
}
|
||||
}, 5000); // 5秒超时
|
||||
});
|
||||
}
|
||||
|
||||
async initSourceBuffer() {
|
||||
if (!this.mediaSource || this.mediaSource.readyState !== 'open') {
|
||||
debugger;
|
||||
throw new Error('MediaSource 未准备好');
|
||||
}
|
||||
|
||||
let sourceBufferCreated = false;
|
||||
for (const codec of this.codecConfigs) {
|
||||
try {
|
||||
if (MediaSource.isTypeSupported(codec)) {
|
||||
this.sourceBuffer = this.mediaSource.addSourceBuffer(codec);
|
||||
sourceBufferCreated = true;
|
||||
this.log(`成功创建 SourceBuffer,使用编解码器: ${codec}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(`尝试编解码器 ${codec} 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceBufferCreated) {
|
||||
throw new Error('无法创建支持的 SourceBuffer');
|
||||
}
|
||||
|
||||
this.sourceBuffer.mode = 'sequence';
|
||||
const handleUpdateEnd = () => this.handleUpdateEnd();
|
||||
const handleError = (e) => {
|
||||
const errorMessage = e.message || e.toString();
|
||||
this.log(`SourceBuffer 错误: ${errorMessage}`);
|
||||
this.handleError(new Error(`SourceBuffer 错误: ${errorMessage}`));
|
||||
};
|
||||
|
||||
this.sourceBuffer.addEventListener('updateend', handleUpdateEnd);
|
||||
this.sourceBuffer.addEventListener('error', handleError);
|
||||
|
||||
// 保存事件处理函数的引用,以便在销毁时正确移除
|
||||
this._updateEndHandler = handleUpdateEnd;
|
||||
this._errorHandler = handleError;
|
||||
|
||||
// 等待一小段时间确保 SourceBuffer 完全准备好
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
this.isSourceBufferReady = true;
|
||||
}
|
||||
|
||||
async appendBuffer(buffer) {
|
||||
if (this.hasError || this.isDestroyed) {
|
||||
this.log('播放器处于错误状态或已销毁,忽略新数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果 SourceBuffer 还未准备好,将数据加入队列
|
||||
if (!this.isSourceBufferReady) {
|
||||
if (this.pendingBuffers.length < 10) {
|
||||
this.pendingBuffers.push(buffer);
|
||||
this.log('SourceBuffer 未准备好,将数据加入队列');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.sourceBuffer || this.sourceBuffer.updating || this.pendingBuffers.length > 0) {
|
||||
if (this.pendingBuffers.length < 10) {
|
||||
this.pendingBuffers.push(buffer);
|
||||
this.log('缓冲区正忙,将数据加入队列');
|
||||
} else {
|
||||
this.log('等待队列已满,丢弃数据');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!buffer || buffer.byteLength === 0) {
|
||||
throw new Error('收到空数据');
|
||||
}
|
||||
|
||||
// 检查 MediaSource 状态
|
||||
if (!this.mediaSource || this.mediaSource.readyState !== 'open') {
|
||||
if (this.retryCount < this.MAX_RETRIES) {
|
||||
this.retryCount++;
|
||||
this.log(`MediaSource 未准备好,重试 ${this.retryCount}/${this.MAX_RETRIES}`);
|
||||
this.pendingBuffers.unshift(buffer);
|
||||
setTimeout(() => this.processNextBuffer(), 500);
|
||||
return;
|
||||
}
|
||||
throw new Error('MediaSource 未准备好或已关闭');
|
||||
}
|
||||
|
||||
await this.removeOldBuffers();
|
||||
this.sourceBuffer.appendBuffer(buffer);
|
||||
this.isBuffering = true;
|
||||
this.retryCount = 0; // 重置重试计数
|
||||
this.log(`添加数据到缓冲区,大小: ${buffer.byteLength} 字节`);
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || '未知错误';
|
||||
this.log(`添加缓冲区失败: ${errorMessage}`);
|
||||
console.error('添加缓冲区失败:', error);
|
||||
|
||||
// 只有在重试次数用完后才触发致命错误
|
||||
if (this.retryCount >= this.MAX_RETRIES) {
|
||||
this.handleError(new Error(`添加缓冲区失败: ${errorMessage}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async processNextBuffer() {
|
||||
if (this.pendingBuffers.length > 0 && !this.sourceBuffer.updating) {
|
||||
const nextBuffer = this.pendingBuffers.shift();
|
||||
await this.appendBuffer(nextBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
async removeOldBuffers() {
|
||||
if (!this.sourceBuffer || !this.video.buffered.length) return;
|
||||
|
||||
const currentTime = this.video.currentTime;
|
||||
const buffered = this.video.buffered;
|
||||
|
||||
for (let i = 0; i < buffered.length; i++) {
|
||||
const start = buffered.start(i);
|
||||
const end = buffered.end(i);
|
||||
|
||||
if (end - currentTime > this.MAX_BUFFER_LENGTH) {
|
||||
const removeEnd = currentTime - 1;
|
||||
if (removeEnd > start) {
|
||||
try {
|
||||
this.log(`清理缓冲区: ${start.toFixed(2)} - ${removeEnd.toFixed(2)}`);
|
||||
await new Promise((resolve, reject) => {
|
||||
this.sourceBuffer.remove(start, removeEnd);
|
||||
const onUpdate = () => {
|
||||
this.sourceBuffer.removeEventListener('updateend', onUpdate);
|
||||
resolve();
|
||||
};
|
||||
this.sourceBuffer.addEventListener('updateend', onUpdate);
|
||||
});
|
||||
} catch (e) {
|
||||
this.log(`清理缓冲区失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateEnd() {
|
||||
this.isBuffering = false;
|
||||
this.log('缓冲区更新完成');
|
||||
|
||||
// 处理队列中的下一个缓冲区
|
||||
this.processNextBuffer();
|
||||
|
||||
if (!this.hasError && !this.video.playing) {
|
||||
this.video.play().catch(e => {
|
||||
this.log(`播放失败: ${e.message}`);
|
||||
this.handleError(e.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
if (this.hasError || this.isDestroyed) {
|
||||
return; // 防止重复处理错误
|
||||
}
|
||||
this.hasError = true;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.log(`播放器错误: ${errorMessage}`);
|
||||
this.destroy().catch(e => {
|
||||
this.log(`销毁播放器时发生错误: ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
if (this.isDestroyed) {
|
||||
return; // 防止重复销毁
|
||||
}
|
||||
this.isDestroyed = true;
|
||||
this.hasError = true;
|
||||
|
||||
try {
|
||||
this.video.pause();
|
||||
|
||||
// 等待 SourceBuffer 更新完成
|
||||
if (this.sourceBuffer && this.sourceBuffer.updating) {
|
||||
await new Promise((resolve) => {
|
||||
const onUpdate = () => {
|
||||
if (this.sourceBuffer) {
|
||||
this.sourceBuffer.removeEventListener('updateend', onUpdate);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
this.sourceBuffer.addEventListener('updateend', onUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
// 清理 SourceBuffer 事件监听器
|
||||
if (this.sourceBuffer) {
|
||||
if (this._updateEndHandler) {
|
||||
this.sourceBuffer.removeEventListener('updateend', this._updateEndHandler);
|
||||
}
|
||||
if (this._errorHandler) {
|
||||
this.sourceBuffer.removeEventListener('error', this._errorHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理 MediaSource
|
||||
if (this.mediaSource && this.mediaSource.readyState === 'open') {
|
||||
// 移除所有 SourceBuffers
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating) {
|
||||
try {
|
||||
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
|
||||
} catch (e) {
|
||||
this.log(`移除 SourceBuffer 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在视频元数据加载完成后才调用 endOfStream
|
||||
if (this.hasMetadata) {
|
||||
try {
|
||||
await new Promise(resolve => {
|
||||
// 确保在下一个事件循环中执行 endOfStream
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.mediaSource.endOfStream();
|
||||
} catch (e) {
|
||||
this.log(`关闭 MediaSource 失败: ${e.message}`);
|
||||
}
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
} catch (e) {
|
||||
this.log(`关闭 MediaSource 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.video.src) {
|
||||
URL.revokeObjectURL(this.video.src);
|
||||
this.video.removeAttribute('src');
|
||||
this.video.load();
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`销毁播放器时发生错误: ${error.message}`);
|
||||
} finally {
|
||||
// 确保清理所有资源
|
||||
this.sourceBuffer = null;
|
||||
this.mediaSource = null;
|
||||
this.pendingBuffers = [];
|
||||
this.isBuffering = false;
|
||||
this._updateEndHandler = null;
|
||||
this._errorHandler = null;
|
||||
this.hasMetadata = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global variables
|
||||
let currentPlaylist = [];
|
||||
let currentIndex = 0;
|
||||
let mediaSource;
|
||||
let sourceBuffer;
|
||||
let pendingBuffers = [];
|
||||
let isBuffering = false;
|
||||
const MAX_BUFFER_LENGTH = 30; // 保持30秒的缓冲区
|
||||
let msePlayer = null;
|
||||
let wsConnection = null;
|
||||
let bufferMergeCount = 10; // 默认缓存包数量
|
||||
|
||||
// 添加滑动条事件监听
|
||||
const bufferCountSlider = document.getElementById('bufferCount');
|
||||
const bufferCountValue = document.getElementById('bufferCountValue');
|
||||
|
||||
bufferCountSlider.addEventListener('input', (e) => {
|
||||
bufferMergeCount = parseInt(e.target.value);
|
||||
bufferCountValue.textContent = bufferMergeCount;
|
||||
if (wsConnection) {
|
||||
log(`已更新缓存包数量为: ${bufferMergeCount}`);
|
||||
}
|
||||
});
|
||||
|
||||
function log(message) {
|
||||
const debug = document.getElementById('debug');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
debug.textContent = `[${time}] ${message}\n` + debug.textContent;
|
||||
const newLine = document.createElement('div');
|
||||
newLine.innerHTML = `[${time}] ${message}`;
|
||||
if (debug.firstChild) {
|
||||
debug.insertBefore(newLine, debug.firstChild);
|
||||
} else {
|
||||
debug.appendChild(newLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize MSE player
|
||||
function initPlayer() {
|
||||
const video = document.getElementById('videoPlayer');
|
||||
if (msePlayer) {
|
||||
msePlayer.destroy();
|
||||
}
|
||||
msePlayer = new MSEPlayer(video, log);
|
||||
return msePlayer.init();
|
||||
}
|
||||
|
||||
// WebSocket handling
|
||||
async function connectWebSocket() {
|
||||
const wsUrl = document.getElementById('wsUrl').value;
|
||||
if (!wsUrl) {
|
||||
alert('请输入 WebSocket 地址');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await initPlayer();
|
||||
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
|
||||
wsConnection = new WebSocket(wsUrl);
|
||||
wsConnection.binaryType = 'arraybuffer';
|
||||
log(`正在连接 WebSocket: ${wsUrl}`);
|
||||
|
||||
wsConnection.onopen = () => {
|
||||
log('WebSocket 连接已建立');
|
||||
};
|
||||
|
||||
wsConnection.onmessage = async (event) => {
|
||||
if (!msePlayer || msePlayer.hasError || msePlayer.isDestroyed) {
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!event.data || event.data.byteLength === 0) {
|
||||
throw new Error('收到空数据');
|
||||
}
|
||||
|
||||
// 为前两个 buffer 创建 Blob URL
|
||||
if (!wsConnection.bufferCount) {
|
||||
wsConnection.bufferCount = 0;
|
||||
}
|
||||
if (wsConnection.bufferCount < 2) {
|
||||
const blob = new Blob([event.data], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.href = url;
|
||||
linkElement.download = `buffer-${wsConnection.bufferCount + 1}.mp4`;
|
||||
linkElement.textContent = `下载第 ${wsConnection.bufferCount + 1} 个 buffer`;
|
||||
linkElement.target = '_blank';
|
||||
const debug = document.getElementById('debug');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const newLine = document.createElement('div');
|
||||
newLine.textContent = `[${time}] 第 ${wsConnection.bufferCount + 1} 个 buffer 大小: ${event.data.byteLength} 字节 `;
|
||||
newLine.appendChild(linkElement);
|
||||
debug.insertBefore(newLine, debug.firstChild);
|
||||
wsConnection.bufferCount++;
|
||||
}
|
||||
|
||||
// 初始化缓存数组
|
||||
if (!wsConnection.cachedBuffers) {
|
||||
wsConnection.cachedBuffers = [];
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
wsConnection.cachedBuffers.push(new Uint8Array(event.data));
|
||||
log(`已缓存 ${wsConnection.cachedBuffers.length} 个数据包`);
|
||||
|
||||
// 当累积到指定数量的数据包时,合并并添加到 buffer
|
||||
if (wsConnection.cachedBuffers.length >= bufferMergeCount) {
|
||||
// 计算总长度
|
||||
const totalLength = wsConnection.cachedBuffers.reduce((acc, curr) => acc + curr.byteLength, 0);
|
||||
|
||||
// 创建合并后的 buffer
|
||||
const mergedBuffer = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
|
||||
// 合并所有缓存的数据
|
||||
for (const buffer of wsConnection.cachedBuffers) {
|
||||
mergedBuffer.set(buffer, offset);
|
||||
offset += buffer.byteLength;
|
||||
}
|
||||
|
||||
log(`合并 ${wsConnection.cachedBuffers.length} 个数据包,总大小: ${totalLength} 字节`);
|
||||
|
||||
// 清空缓存
|
||||
wsConnection.cachedBuffers = [];
|
||||
|
||||
// 添加到 MSE
|
||||
await msePlayer.appendBuffer(mergedBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || '未知错误';
|
||||
log(`处理数据失败: ${errorMessage}`);
|
||||
// 只有在发生致命错误时才关闭连接
|
||||
if (msePlayer.hasError) {
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
wsConnection.onclose = (event) => {
|
||||
const reason = event.reason || '未知原因';
|
||||
log(`WebSocket 连接已关闭: ${reason} (code: ${event.code})`);
|
||||
if (msePlayer && !msePlayer.isDestroyed) {
|
||||
msePlayer.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
wsConnection.onerror = (error) => {
|
||||
const errorMessage = error.message || '未知错误';
|
||||
log(`WebSocket 错误: ${errorMessage}`);
|
||||
if (msePlayer && !msePlayer.isDestroyed) {
|
||||
msePlayer.destroy();
|
||||
}
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || '未知错误';
|
||||
log(`WebSocket 初始化失败: ${errorMessage}`);
|
||||
if (msePlayer && !msePlayer.isDestroyed) {
|
||||
msePlayer.destroy();
|
||||
}
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle local file
|
||||
async function handleLocalFile(file) {
|
||||
if (!file.name.toLowerCase().endsWith('.mp4') && !file.name.toLowerCase().endsWith('.fmp4')) {
|
||||
alert('请选择 FMP4/MP4 文件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(`开始处理本地文件: ${file.name}`);
|
||||
await initPlayer();
|
||||
const buffer = await file.arrayBuffer();
|
||||
log(`本地文件加载完成,大小: ${buffer.byteLength} 字节`);
|
||||
await msePlayer.appendBuffer(buffer);
|
||||
} catch (error) {
|
||||
log(`处理本地文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// M3U8 handling
|
||||
async function loadM3U8() {
|
||||
const m3u8Url = document.getElementById('m3u8Url').value;
|
||||
if (!m3u8Url) {
|
||||
@@ -84,7 +678,15 @@
|
||||
try {
|
||||
log(`开始加载 M3U8: ${m3u8Url}`);
|
||||
const response = await fetch(m3u8Url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const content = await response.text();
|
||||
|
||||
// 显示 M3U8 内容
|
||||
const m3u8ContentDiv = document.getElementById('m3u8Content');
|
||||
m3u8ContentDiv.textContent = content;
|
||||
|
||||
const mp4Urls = parseM3U8(content, m3u8Url);
|
||||
log(`解析到 ${mp4Urls.length} 个 MP4 文件`);
|
||||
|
||||
@@ -95,10 +697,10 @@
|
||||
|
||||
currentPlaylist = mp4Urls;
|
||||
currentIndex = 0;
|
||||
initMSE();
|
||||
await initPlayer();
|
||||
await loadNextSegment();
|
||||
} catch (error) {
|
||||
console.error('加载 M3U8 文件失败:', error);
|
||||
log(`加载失败: ${error.message}`);
|
||||
log(`加载 M3U8 文件失败: ${error.message}`);
|
||||
alert('加载 M3U8 文件失败');
|
||||
}
|
||||
}
|
||||
@@ -106,224 +708,135 @@
|
||||
function parseM3U8(content, baseUrl) {
|
||||
const lines = content.split('\n');
|
||||
const mp4Urls = [];
|
||||
let duration = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() && !line.startsWith('#')) {
|
||||
const url = line.startsWith('http') ? line : new URL(line, baseUrl).href;
|
||||
if (url.endsWith('.mp4')) {
|
||||
mp4Urls.push(url);
|
||||
log(`找到 MP4: ${url}`);
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// 解析 EXTINF 获取时长
|
||||
if (line.startsWith('#EXTINF:')) {
|
||||
duration = parseFloat(line.split(':')[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过注释和空行
|
||||
if (line === '' || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理 MP4 文件 URL
|
||||
const url = line.startsWith('http') ? line : new URL(line, baseUrl).href;
|
||||
mp4Urls.push({
|
||||
url,
|
||||
duration
|
||||
});
|
||||
log(`找到 MP4: ${url} (时长: ${duration}秒)`);
|
||||
}
|
||||
|
||||
return mp4Urls;
|
||||
}
|
||||
|
||||
function initMSE() {
|
||||
const video = document.getElementById('videoPlayer');
|
||||
log('初始化 MSE');
|
||||
|
||||
if (mediaSource) {
|
||||
if (mediaSource.readyState === 'open') {
|
||||
mediaSource.endOfStream();
|
||||
}
|
||||
URL.revokeObjectURL(video.src);
|
||||
log('清理旧的 MediaSource');
|
||||
}
|
||||
|
||||
mediaSource = new MediaSource();
|
||||
video.src = URL.createObjectURL(mediaSource);
|
||||
pendingBuffers = [];
|
||||
isBuffering = false;
|
||||
|
||||
mediaSource.addEventListener('sourceopen', async () => {
|
||||
log('MediaSource 已打开');
|
||||
try {
|
||||
// Try different codec combinations
|
||||
const codecConfigs = [
|
||||
'video/mp4; codecs="avc1.64001f"', // Video only
|
||||
'video/mp4; codecs="avc1.64001f,mp4a.40.2"', // Video + AAC
|
||||
'video/mp4' // Let the browser figure it out
|
||||
];
|
||||
|
||||
let sourceBufferCreated = false;
|
||||
for (const codec of codecConfigs) {
|
||||
try {
|
||||
if (MediaSource.isTypeSupported(codec)) {
|
||||
sourceBuffer = mediaSource.addSourceBuffer(codec);
|
||||
sourceBufferCreated = true;
|
||||
log(`成功创建 SourceBuffer,使用编解码器: ${codec}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
log(`尝试编解码器 ${codec} 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceBufferCreated) {
|
||||
throw new Error('无法创建支持的 SourceBuffer');
|
||||
}
|
||||
|
||||
sourceBuffer.mode = 'sequence';
|
||||
sourceBuffer.addEventListener('updateend', handleUpdateEnd);
|
||||
sourceBuffer.addEventListener('error', (e) => {
|
||||
log(`SourceBuffer 错误: ${e}`);
|
||||
});
|
||||
|
||||
// 先加载第一个片段,等待缓冲完成后再播放
|
||||
log('等待第一个片段加载完成...');
|
||||
await loadNextSegment();
|
||||
await new Promise(resolve => {
|
||||
const checkBuffer = () => {
|
||||
if (!sourceBuffer.updating && video.buffered.length > 0) {
|
||||
log('首个片段缓冲完成,开始播放');
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkBuffer, 100);
|
||||
}
|
||||
};
|
||||
checkBuffer();
|
||||
});
|
||||
|
||||
video.play().catch(e => {
|
||||
log(`播放失败: ${e.message}`);
|
||||
console.error('播放失败:', e);
|
||||
});
|
||||
} catch (error) {
|
||||
log(`创建 SourceBuffer 失败: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
mediaSource.addEventListener('sourceended', () => {
|
||||
log('MediaSource 已结束');
|
||||
});
|
||||
|
||||
mediaSource.addEventListener('error', (e) => {
|
||||
log(`MediaSource 错误: ${e}`);
|
||||
});
|
||||
|
||||
video.addEventListener('error', (e) => {
|
||||
log(`视频错误: ${video.error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNextSegment() {
|
||||
if (currentIndex >= currentPlaylist.length) {
|
||||
if (mediaSource.readyState === 'open') {
|
||||
mediaSource.endOfStream();
|
||||
if (msePlayer.mediaSource.readyState === 'open') {
|
||||
msePlayer.mediaSource.endOfStream();
|
||||
log('已到达播放列表末尾');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 在加载新片段前检查并清理缓冲区
|
||||
await removeOldBuffers();
|
||||
|
||||
log(`加载视频片段 ${currentIndex + 1}`);
|
||||
const response = await fetch(currentPlaylist[currentIndex]);
|
||||
const segment = currentPlaylist[currentIndex];
|
||||
log(`加载视频片段 ${currentIndex + 1}/${currentPlaylist.length}`);
|
||||
const response = await fetch(segment.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
log(`视频片段 ${currentIndex + 1} 加载完成,大小: ${buffer.byteLength} 字节`);
|
||||
appendBuffer(buffer);
|
||||
await msePlayer.appendBuffer(buffer);
|
||||
|
||||
// 预加载下一个片段,但要控制预加载的数量
|
||||
if (currentIndex < currentPlaylist.length - 1 && pendingBuffers.length < 2) {
|
||||
// 预加载下一个片段
|
||||
if (currentIndex < currentPlaylist.length - 1 && msePlayer.pendingBuffers.length < 2) {
|
||||
currentIndex++;
|
||||
loadNextSegment();
|
||||
}
|
||||
} catch (error) {
|
||||
log(`加载视频片段失败: ${error.message}`);
|
||||
console.error('加载视频片段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOldBuffers() {
|
||||
if (!sourceBuffer || !video.buffered.length) return;
|
||||
|
||||
const currentTime = video.currentTime;
|
||||
const buffered = video.buffered;
|
||||
|
||||
// 计算当前缓冲区的范围
|
||||
for (let i = 0; i < buffered.length; i++) {
|
||||
const start = buffered.start(i);
|
||||
const end = buffered.end(i);
|
||||
|
||||
// 如果缓冲区超过了最大长度,移除旧的部分
|
||||
if (end - currentTime > MAX_BUFFER_LENGTH) {
|
||||
const removeEnd = currentTime - 1; // 保留当前播放位置前1秒
|
||||
if (removeEnd > start) {
|
||||
try {
|
||||
log(`清理缓冲区: ${start.toFixed(2)} - ${removeEnd.toFixed(2)}`);
|
||||
await new Promise((resolve, reject) => {
|
||||
sourceBuffer.remove(start, removeEnd);
|
||||
const onUpdate = () => {
|
||||
sourceBuffer.removeEventListener('updateend', onUpdate);
|
||||
resolve();
|
||||
};
|
||||
sourceBuffer.addEventListener('updateend', onUpdate);
|
||||
});
|
||||
} catch (e) {
|
||||
log(`清理缓冲区失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function appendBuffer(buffer) {
|
||||
if (!sourceBuffer || sourceBuffer.updating || pendingBuffers.length > 0) {
|
||||
// 限制等待队列的长度
|
||||
if (pendingBuffers.length < 3) {
|
||||
pendingBuffers.push(buffer);
|
||||
log('缓冲区正忙,将数据加入队列');
|
||||
} else {
|
||||
log('等待队列已满,丢弃数据');
|
||||
}
|
||||
// FMP4 testing
|
||||
async function testFMP4() {
|
||||
const fmp4Url = document.getElementById('fmp4Url').value;
|
||||
if (!fmp4Url) {
|
||||
alert('请输入 FMP4 地址');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sourceBuffer.appendBuffer(buffer);
|
||||
isBuffering = true;
|
||||
log('添加数据到缓冲区');
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
log('缓冲区已满,将进行清理');
|
||||
pendingBuffers.push(buffer);
|
||||
removeOldBuffers();
|
||||
} else {
|
||||
log(`添加缓冲区失败: ${error.message}`);
|
||||
console.error('添加缓冲区失败:', error);
|
||||
log(`开始测试 FMP4: ${fmp4Url}`);
|
||||
await initPlayer();
|
||||
|
||||
const response = await fetch(fmp4Url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
log(`FMP4 文件加载完成,大小: ${buffer.byteLength} 字节`);
|
||||
await msePlayer.appendBuffer(buffer);
|
||||
} catch (error) {
|
||||
log(`加载 FMP4 文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateEnd() {
|
||||
isBuffering = false;
|
||||
log('缓冲区更新完成');
|
||||
// Event listeners
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const video = document.getElementById('videoPlayer');
|
||||
|
||||
if (pendingBuffers.length > 0) {
|
||||
const nextBuffer = pendingBuffers.shift();
|
||||
appendBuffer(nextBuffer);
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleLocalFile(files[0]);
|
||||
}
|
||||
});
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleLocalFile(e.target.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('videoPlayer').addEventListener('ended', () => {
|
||||
log('视频播放结束,重新开始');
|
||||
currentIndex = 0;
|
||||
initMSE();
|
||||
});
|
||||
|
||||
const video = document.getElementById('videoPlayer');
|
||||
video.addEventListener('ended', () => {
|
||||
log('视频播放结束');
|
||||
});
|
||||
|
||||
video.addEventListener('playing', () => log('视频开始播放'));
|
||||
video.addEventListener('pause', () => log('视频暂停'));
|
||||
video.addEventListener('waiting', () => log('视频缓冲中'));
|
||||
video.addEventListener('canplay', () => log('视频可以播放'));
|
||||
video.addEventListener('loadedmetadata', () => log('视频元数据已加载'));
|
||||
video.addEventListener('error', (e) => {
|
||||
if (msePlayer && !msePlayer.isDestroyed) {
|
||||
log(`视频错误: ${video.error ? video.error.message : '未知错误'}`);
|
||||
msePlayer.handleError(video.error ? video.error.message : '未知错误');
|
||||
}
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@ import (
|
||||
hls "m7s.live/v5/plugin/hls/pkg"
|
||||
)
|
||||
|
||||
var _ = m7s.InstallPlugin[HLSPlugin](hls.NewTransform, hls.NewRecorder)
|
||||
var _ = m7s.InstallPlugin[HLSPlugin](m7s.PluginMeta{
|
||||
NewTransformer: hls.NewTransform,
|
||||
NewRecorder: hls.NewRecorder,
|
||||
NewPuller: hls.NewPuller,
|
||||
NewPullProxy: m7s.NewHTTPPullPorxy,
|
||||
})
|
||||
|
||||
//go:embed hls.js
|
||||
var hls_js embed.FS
|
||||
@@ -45,13 +50,6 @@ func (p *HLSPlugin) RegisterHandler() map[string]http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HLSPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any {
|
||||
d := &m7s.HTTPPullProxy{}
|
||||
d.PullProxy = pullProxy
|
||||
d.Plugin = &p.Plugin
|
||||
return d
|
||||
}
|
||||
|
||||
func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
|
||||
recordType := "ts"
|
||||
if r.PathValue("streamPath") == "mp4.m3u8" {
|
||||
@@ -76,13 +74,35 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
|
||||
if !startTime.IsZero() {
|
||||
if config.DB != nil {
|
||||
var records []m7s.RecordStream
|
||||
if endTime.IsZero() {
|
||||
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time > ?`
|
||||
config.DB.Where(query, streamPath, recordType, startTime).Find(&records)
|
||||
} else {
|
||||
if recordType == "fmp4" {
|
||||
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time`
|
||||
config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records)
|
||||
config.DB.Where(query, streamPath, "mp4", startTime, endTime).Find(&records)
|
||||
if len(records) == 0 {
|
||||
return
|
||||
}
|
||||
playlist := hls.Playlist{
|
||||
Version: 7,
|
||||
Sequence: 0,
|
||||
Targetduration: 90,
|
||||
}
|
||||
var plBuffer util.Buffer
|
||||
playlist.Writer = &plBuffer
|
||||
playlist.Init()
|
||||
|
||||
for _, record := range records {
|
||||
duration := record.EndTime.Sub(record.StartTime).Seconds()
|
||||
playlist.WriteInf(hls.PlaylistInf{
|
||||
Duration: duration,
|
||||
URL: fmt.Sprintf("/mp4/download/%s.fmp4?id=%d", streamPath, record.ID),
|
||||
Title: record.StartTime.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
plBuffer.WriteString("#EXT-X-ENDLIST\n")
|
||||
w.Write(plBuffer)
|
||||
return
|
||||
}
|
||||
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time`
|
||||
config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records)
|
||||
if len(records) > 0 {
|
||||
playlist := hls.Playlist{
|
||||
Version: 7,
|
||||
@@ -97,8 +117,7 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
|
||||
duration := record.EndTime.Sub(record.StartTime).Seconds()
|
||||
playlist.WriteInf(hls.PlaylistInf{
|
||||
Duration: duration,
|
||||
Title: record.FilePath,
|
||||
FilePath: record.FilePath,
|
||||
URL: record.FilePath,
|
||||
})
|
||||
}
|
||||
plBuffer.WriteString("#EXT-X-ENDLIST\n")
|
||||
@@ -178,6 +197,7 @@ func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Sequence: 0,
|
||||
Targetduration: 90,
|
||||
}
|
||||
|
||||
var plBuffer util.Buffer
|
||||
playlist.Writer = &plBuffer
|
||||
playlist.Init()
|
||||
@@ -186,7 +206,7 @@ func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
duration := record.EndTime.Sub(record.StartTime).Seconds()
|
||||
playlist.WriteInf(hls.PlaylistInf{
|
||||
Duration: duration,
|
||||
Title: path.Base(record.FilePath),
|
||||
URL: path.Base(record.FilePath),
|
||||
FilePath: record.FilePath,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ type PlaylistKey struct {
|
||||
|
||||
type PlaylistInf struct {
|
||||
Duration float64
|
||||
URL string
|
||||
Title string
|
||||
FilePath string
|
||||
}
|
||||
@@ -113,13 +114,14 @@ func (pl *Playlist) Init() (err error) {
|
||||
"#EXT-X-VERSION:%d\n"+
|
||||
"#EXT-X-MEDIA-SEQUENCE:%d\n"+
|
||||
"#EXT-X-TARGETDURATION:%d\n", pl.Version, pl.Sequence, pl.Targetduration)
|
||||
|
||||
pl.Sequence++
|
||||
return
|
||||
}
|
||||
|
||||
func (pl *Playlist) WriteInf(inf PlaylistInf) (err error) {
|
||||
_, err = fmt.Fprintf(pl, "#EXTINF:%.3f,\n"+
|
||||
"%s\n", inf.Duration, inf.Title)
|
||||
_, err = fmt.Fprintf(pl, "#EXTINF:%.3f,%s\n"+
|
||||
"%s\n", inf.Duration, inf.Title, inf.URL)
|
||||
pl.tsCount++
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import (
|
||||
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
|
||||
)
|
||||
|
||||
func NewPuller(conf config.Pull) m7s.IPuller {
|
||||
return &Puller{}
|
||||
}
|
||||
|
||||
type Puller struct {
|
||||
task.Job
|
||||
PullJob m7s.PullJob
|
||||
@@ -276,7 +280,7 @@ func (p *Puller) pull(info *M3u8Info) (err error) {
|
||||
tsFilePath := p.PullJob.StreamPath + "/" + tsFilename
|
||||
ss := strings.Split(p.PullJob.StreamPath, "/")
|
||||
var plInfo = PlaylistInf{
|
||||
Title: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
|
||||
URL: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
|
||||
Duration: v.dur,
|
||||
FilePath: tsFilePath,
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@ func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
|
||||
@@ -135,7 +135,7 @@ func (w *HLSWriter) checkFragment(ts time.Duration) (err error) {
|
||||
inf := PlaylistInf{
|
||||
//浮点计算精度
|
||||
Duration: dur.Seconds(),
|
||||
Title: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
|
||||
URL: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
|
||||
FilePath: tsFilePath,
|
||||
}
|
||||
|
||||
|
||||
@@ -52,14 +52,15 @@ func (h *LogRotatePlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (l *LogRotatePlugin) API_trail(w http.ResponseWriter, r *http.Request) {
|
||||
writer := util.NewSSE(w, r.Context())
|
||||
file, err := os.Open(filepath.Join(l.Path, "current.log"))
|
||||
if err == nil {
|
||||
io.Copy(writer, file)
|
||||
file.Close()
|
||||
}
|
||||
h := console.NewHandler(writer, &console.HandlerOptions{NoColor: true})
|
||||
l.Server.LogHandler.Add(h)
|
||||
<-r.Context().Done()
|
||||
l.Server.LogHandler.Remove(h)
|
||||
util.NewSSE(w, r.Context(), func(sse *util.SSE) {
|
||||
file, err := os.Open(filepath.Join(l.Path, "current.log"))
|
||||
if err == nil {
|
||||
io.Copy(sse, file)
|
||||
file.Close()
|
||||
}
|
||||
h := console.NewHandler(sse, &console.HandlerOptions{NoColor: true})
|
||||
l.Server.LogHandler.Add(h)
|
||||
<-r.Context().Done()
|
||||
l.Server.LogHandler.Remove(h)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package plugin_mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"m7s.live/v5/pb"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
mp4pb "m7s.live/v5/plugin/mp4/pb"
|
||||
mp4 "m7s.live/v5/plugin/mp4/pkg"
|
||||
@@ -24,8 +27,80 @@ import (
|
||||
|
||||
type ContentPart struct {
|
||||
*os.File
|
||||
Start int64
|
||||
Size int
|
||||
Start int64
|
||||
Size int
|
||||
boxies []box.IBox
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) downloadSingleFile(stream *m7s.RecordStream, flag mp4.Flag, w http.ResponseWriter, r *http.Request) {
|
||||
if flag == 0 {
|
||||
http.ServeFile(w, r, stream.FilePath)
|
||||
} else if flag == mp4.FLAG_FRAGMENT {
|
||||
file, err := os.Open(stream.FilePath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
p.Info("read", "file", file.Name())
|
||||
demuxer := mp4.NewDemuxer(file)
|
||||
err = demuxer.Demux()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var trackMap = make(map[box.MP4_CODEC_TYPE]*mp4.Track)
|
||||
muxer := mp4.NewMuxer(mp4.FLAG_FRAGMENT)
|
||||
for _, track := range demuxer.Tracks {
|
||||
t := muxer.AddTrack(track.Cid)
|
||||
t.ExtraData = track.ExtraData
|
||||
trackMap[track.Cid] = t
|
||||
if track.Cid.IsAudio() {
|
||||
t.SampleSize = track.SampleSize
|
||||
t.SampleRate = track.SampleRate
|
||||
t.ChannelCount = track.ChannelCount
|
||||
} else if track.Cid.IsVideo() {
|
||||
t.Width = track.Width
|
||||
t.Height = track.Height
|
||||
}
|
||||
}
|
||||
moov := muxer.MakeMoov()
|
||||
var parts []*ContentPart
|
||||
var part *ContentPart
|
||||
for track, sample := range demuxer.RangeSample {
|
||||
if part == nil {
|
||||
part = &ContentPart{
|
||||
File: file,
|
||||
Start: sample.Offset,
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
fixSample := *sample
|
||||
part.Seek(sample.Offset, io.SeekStart)
|
||||
fixSample.Data = make([]byte, sample.Size)
|
||||
part.Read(fixSample.Data)
|
||||
moof, mdat := muxer.CreateFlagment(trackMap[track.Cid], fixSample)
|
||||
if moof != nil {
|
||||
part.boxies = append(part.boxies, moof, mdat)
|
||||
part.Size += int(moof.Size() + mdat.Size())
|
||||
}
|
||||
}
|
||||
var children []box.IBox
|
||||
var totalSize uint64
|
||||
ftyp := muxer.CreateFTYPBox()
|
||||
children = append(children, ftyp, moov)
|
||||
totalSize += uint64(ftyp.Size() + moov.Size())
|
||||
for _, part := range parts {
|
||||
totalSize += uint64(part.Size)
|
||||
children = append(children, part.boxies...)
|
||||
part.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", totalSize))
|
||||
_, err = box.WriteTo(w, children...)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -35,7 +110,13 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
streamPath := r.PathValue("streamPath")
|
||||
|
||||
var flag mp4.Flag
|
||||
if strings.HasSuffix(streamPath, ".fmp4") {
|
||||
flag = mp4.FLAG_FRAGMENT
|
||||
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
|
||||
} else {
|
||||
streamPath = strings.TrimSuffix(streamPath, ".mp4")
|
||||
}
|
||||
query := r.URL.Query()
|
||||
var streams []m7s.RecordStream
|
||||
if id := query.Get("id"); id != "" {
|
||||
@@ -45,10 +126,10 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "record not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, streams[0].FilePath)
|
||||
p.downloadSingleFile(&streams[0], flag, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 合并多个 mp4
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(query)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -59,23 +140,90 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
queryRecord := m7s.RecordStream{
|
||||
Mode: m7s.RecordModeAuto,
|
||||
Type: "mp4",
|
||||
}
|
||||
p.DB.Where(&queryRecord).Find(&streams, "end_time>? AND start_time<? AND stream_path=?", startTime, endTime, streamPath)
|
||||
muxer := mp4.NewMuxer(0)
|
||||
ftyp := box.CreateFTYPBox(box.TypeISOM, 0x200, box.TypeISOM, box.TypeISO2, box.TypeAVC1, box.TypeMP41)
|
||||
var n int64
|
||||
n, err = box.WriteTo(w, ftyp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
muxer.CurrentOffset = n
|
||||
muxer := mp4.NewMuxer(flag)
|
||||
ftyp := muxer.CreateFTYPBox()
|
||||
n := ftyp.Size()
|
||||
muxer.CurrentOffset = int64(n)
|
||||
var lastTs, tsOffset int64
|
||||
var parts []*ContentPart
|
||||
sampleOffset := muxer.CurrentOffset + box.BasicBoxLen*2
|
||||
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
|
||||
mdatOffset := sampleOffset
|
||||
var audioTrack, videoTrack *mp4.Track
|
||||
var file *os.File
|
||||
var moov box.IBox
|
||||
streamCount := len(streams)
|
||||
|
||||
// Track ExtraData history for each track
|
||||
type TrackHistory struct {
|
||||
Track *mp4.Track
|
||||
ExtraData []byte
|
||||
}
|
||||
var audioHistory, videoHistory []TrackHistory
|
||||
addAudioTrack := func(track *mp4.Track) {
|
||||
t := muxer.AddTrack(track.Cid)
|
||||
t.ExtraData = track.ExtraData
|
||||
t.SampleSize = track.SampleSize
|
||||
t.SampleRate = track.SampleRate
|
||||
t.ChannelCount = track.ChannelCount
|
||||
if len(audioHistory) > 0 {
|
||||
t.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
|
||||
}
|
||||
audioTrack = t
|
||||
audioHistory = append(audioHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
|
||||
}
|
||||
|
||||
addVideoTrack := func(track *mp4.Track) {
|
||||
t := muxer.AddTrack(track.Cid)
|
||||
t.ExtraData = track.ExtraData
|
||||
t.Width = track.Width
|
||||
t.Height = track.Height
|
||||
if len(videoHistory) > 0 {
|
||||
t.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
|
||||
}
|
||||
videoTrack = t
|
||||
videoHistory = append(videoHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
|
||||
}
|
||||
|
||||
addTrack := func(track *mp4.Track) {
|
||||
var lastAudioTrack, lastVideoTrack *TrackHistory
|
||||
if len(audioHistory) > 0 {
|
||||
lastAudioTrack = &audioHistory[len(audioHistory)-1]
|
||||
}
|
||||
if len(videoHistory) > 0 {
|
||||
lastVideoTrack = &videoHistory[len(videoHistory)-1]
|
||||
}
|
||||
if track.Cid.IsAudio() {
|
||||
if lastAudioTrack == nil {
|
||||
addAudioTrack(track)
|
||||
} else if !bytes.Equal(lastAudioTrack.ExtraData, track.ExtraData) {
|
||||
for _, history := range audioHistory {
|
||||
if bytes.Equal(history.ExtraData, track.ExtraData) {
|
||||
audioTrack = history.Track
|
||||
audioTrack.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
|
||||
return
|
||||
}
|
||||
}
|
||||
addAudioTrack(track)
|
||||
}
|
||||
} else if track.Cid.IsVideo() {
|
||||
if lastVideoTrack == nil {
|
||||
addVideoTrack(track)
|
||||
} else if !bytes.Equal(lastVideoTrack.ExtraData, track.ExtraData) {
|
||||
for _, history := range videoHistory {
|
||||
if bytes.Equal(history.ExtraData, track.ExtraData) {
|
||||
videoTrack = history.Track
|
||||
videoTrack.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
|
||||
return
|
||||
}
|
||||
}
|
||||
addVideoTrack(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, stream := range streams {
|
||||
tsOffset = lastTs
|
||||
file, err = os.Open(stream.FilePath)
|
||||
@@ -88,32 +236,29 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if i == 0 {
|
||||
trackCount := len(demuxer.Tracks)
|
||||
if i == 0 || flag == mp4.FLAG_FRAGMENT {
|
||||
for _, track := range demuxer.Tracks {
|
||||
t := muxer.AddTrack(track.Cid)
|
||||
t.ExtraData = track.ExtraData
|
||||
if track.Cid.IsAudio() {
|
||||
audioTrack = t
|
||||
t.SampleSize = track.SampleSize
|
||||
t.SampleRate = track.SampleRate
|
||||
t.ChannelCount = track.ChannelCount
|
||||
} else if track.Cid.IsVideo() {
|
||||
videoTrack = t
|
||||
t.Width = track.Width
|
||||
t.Height = track.Height
|
||||
}
|
||||
addTrack(track)
|
||||
}
|
||||
}
|
||||
if trackCount != len(muxer.Tracks) {
|
||||
if flag == mp4.FLAG_FRAGMENT {
|
||||
moov = muxer.MakeMoov()
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
|
||||
var startSample *box.Sample
|
||||
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
|
||||
tsOffset = 0
|
||||
continue
|
||||
}
|
||||
tsOffset = -int64(startSample.DTS)
|
||||
tsOffset = -int64(startSample.Timestamp)
|
||||
}
|
||||
var part *ContentPart
|
||||
for track, sample := range demuxer.RangeSample {
|
||||
if i == streamCount-1 && int64(sample.DTS) > endTime.Sub(stream.StartTime).Milliseconds() {
|
||||
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
|
||||
break
|
||||
}
|
||||
if part == nil {
|
||||
@@ -122,16 +267,31 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
Start: sample.Offset,
|
||||
}
|
||||
}
|
||||
part.Size += sample.Size
|
||||
lastTs = int64(sample.DTS + uint64(tsOffset))
|
||||
lastTs = int64(sample.Timestamp + uint32(tsOffset))
|
||||
fixSample := *sample
|
||||
fixSample.DTS += uint64(tsOffset)
|
||||
fixSample.PTS += uint64(tsOffset)
|
||||
fixSample.Offset += sampleOffset - part.Start
|
||||
if track.Cid.IsAudio() {
|
||||
audioTrack.AddSampleEntry(fixSample)
|
||||
} else if track.Cid.IsVideo() {
|
||||
videoTrack.AddSampleEntry(fixSample)
|
||||
fixSample.Timestamp += uint32(tsOffset)
|
||||
if flag == 0 {
|
||||
fixSample.Offset = sampleOffset + (fixSample.Offset - part.Start)
|
||||
part.Size += sample.Size
|
||||
if track.Cid.IsAudio() {
|
||||
audioTrack.AddSampleEntry(fixSample)
|
||||
} else if track.Cid.IsVideo() {
|
||||
videoTrack.AddSampleEntry(fixSample)
|
||||
}
|
||||
} else {
|
||||
part.Seek(sample.Offset, io.SeekStart)
|
||||
fixSample.Data = make([]byte, sample.Size)
|
||||
part.Read(fixSample.Data)
|
||||
var moof, mdat box.IBox
|
||||
if track.Cid.IsAudio() {
|
||||
moof, mdat = muxer.CreateFlagment(audioTrack, fixSample)
|
||||
} else if track.Cid.IsVideo() {
|
||||
moof, mdat = muxer.CreateFlagment(videoTrack, fixSample)
|
||||
}
|
||||
if moof != nil {
|
||||
part.boxies = append(part.boxies, moof, mdat)
|
||||
part.Size += int(moof.Size() + mdat.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
if part != nil {
|
||||
@@ -139,39 +299,67 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
}
|
||||
moovSize := muxer.GetMoovSize()
|
||||
for _, track := range muxer.Tracks {
|
||||
for i := range track.Samplelist {
|
||||
track.Samplelist[i].Offset += int64(moovSize)
|
||||
|
||||
if flag == 0 {
|
||||
moovSize := muxer.MakeMoov().Size()
|
||||
dataSize := uint64(sampleOffset - mdatOffset)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", uint64(sampleOffset)+moovSize))
|
||||
for _, track := range muxer.Tracks {
|
||||
for i := range track.Samplelist {
|
||||
track.Samplelist[i].Offset += int64(moovSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = muxer.WriteMoov(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var mdatBox = box.CreateBaseBox(box.TypeMDAT, uint64(sampleOffset-mdatOffset)+box.BasicBoxLen)
|
||||
var freeBox *box.FreeBox
|
||||
if mdatBox.HeaderSize() == box.BasicBoxLen {
|
||||
freeBox = box.CreateFreeBox(nil)
|
||||
}
|
||||
_, err = box.WriteTo(w, freeBox, mdatBox)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var written, totalWritten int64
|
||||
for _, part := range parts {
|
||||
part.Seek(part.Start, io.SeekStart)
|
||||
written, err = io.CopyN(w, part.File, int64(part.Size))
|
||||
mdatBox := box.CreateBaseBox(box.TypeMDAT, dataSize+box.BasicBoxLen)
|
||||
|
||||
var freeBox *box.FreeBox
|
||||
if mdatBox.HeaderSize() == box.BasicBoxLen {
|
||||
freeBox = box.CreateFreeBox(nil)
|
||||
}
|
||||
|
||||
var written, totalWritten int64
|
||||
|
||||
totalWritten, err = box.WriteTo(w, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
part.Seek(part.Start, io.SeekStart)
|
||||
written, err = io.CopyN(w, part.File, int64(part.Size))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalWritten += written
|
||||
part.Close()
|
||||
}
|
||||
} else {
|
||||
var children []box.IBox
|
||||
var totalSize uint64
|
||||
children = append(children, ftyp, moov)
|
||||
totalSize += uint64(ftyp.Size() + moov.Size())
|
||||
for _, part := range parts {
|
||||
totalSize += uint64(part.Size)
|
||||
children = append(children, part.boxies...)
|
||||
part.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", totalSize))
|
||||
_, err = box.WriteTo(w, children...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalWritten += written
|
||||
part.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord) (res *mp4pb.ResponseStartRecord, err error) {
|
||||
var recordExists bool
|
||||
var filePath = "."
|
||||
var fragment = time.Minute
|
||||
if req.Fragment != nil {
|
||||
fragment = req.Fragment.AsDuration()
|
||||
}
|
||||
if req.FilePath != "" {
|
||||
filePath = req.FilePath
|
||||
}
|
||||
res = &mp4pb.ResponseStartRecord{}
|
||||
p.Server.Records.Call(func() error {
|
||||
_, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
@@ -187,11 +375,32 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
|
||||
if stream, ok := p.Server.Streams.Get(req.StreamPath); ok {
|
||||
recordConf := config.Record{
|
||||
Append: false,
|
||||
Fragment: req.Fragment.AsDuration(),
|
||||
FilePath: req.FilePath,
|
||||
Fragment: fragment,
|
||||
FilePath: filePath,
|
||||
}
|
||||
job := p.Record(stream, recordConf, nil)
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(job.GetTask())))
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) StopRecord(ctx context.Context, req *mp4pb.ReqStopRecord) (res *mp4pb.ResponseStopRecord, err error) {
|
||||
res = &mp4pb.ResponseStopRecord{}
|
||||
var recordJob *m7s.RecordJob
|
||||
p.Server.Records.Call(func() error {
|
||||
recordJob, _ = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath
|
||||
})
|
||||
if recordJob != nil {
|
||||
t := recordJob.GetTask()
|
||||
if t != nil {
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(t)))
|
||||
t.Stop(task.ErrStopByUser)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -214,7 +423,7 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
p.Error("EventStart", "error", err)
|
||||
}
|
||||
}
|
||||
recorder := p.Meta.Recorder(config.Record{})
|
||||
//recorder := p.Meta.Recorder(config.Record{})
|
||||
var tmpJob *m7s.RecordJob
|
||||
p.Server.Records.Call(func() error {
|
||||
tmpJob, _ = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
@@ -230,7 +439,11 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
Fragment: 0,
|
||||
FilePath: filepath.Join(p.EventRecordFilePath, stream.StreamPath, time.Now().Local().Format("2006-01-02-15-04-05")),
|
||||
}
|
||||
recordJob := recorder.GetRecordJob()
|
||||
//recordJob := recorder.GetRecordJob()
|
||||
var subconfig config.Subscribe
|
||||
defaults.SetDefaults(&subconfig)
|
||||
subconfig.BufferTime = beforeDuration
|
||||
recordJob := p.Record(stream, recordConf, &subconfig)
|
||||
recordJob.EventId = req.EventId
|
||||
recordJob.EventLevel = req.EventLevel
|
||||
recordJob.EventName = req.EventName
|
||||
@@ -238,10 +451,6 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
recordJob.AfterDuration = afterDuration
|
||||
recordJob.BeforeDuration = beforeDuration
|
||||
recordJob.Mode = m7s.RecordModeEvent
|
||||
var subconfig config.Subscribe
|
||||
defaults.SetDefaults(&subconfig)
|
||||
subconfig.BufferTime = beforeDuration
|
||||
p.Record(stream, recordConf, &subconfig)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -258,6 +467,7 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
Mode: m7s.RecordModeEvent,
|
||||
BeforeDuration: beforeDuration,
|
||||
AfterDuration: afterDuration,
|
||||
Type: "mp4",
|
||||
}
|
||||
now := time.Now()
|
||||
startTime := now.Add(-beforeDuration)
|
||||
@@ -282,6 +492,7 @@ func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *p
|
||||
PageSize: req.PageSize,
|
||||
Mode: req.Mode,
|
||||
Type: "mp4",
|
||||
EventLevel: req.EventLevel,
|
||||
}
|
||||
return p.Server.GetRecordList(ctx, globalReq)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package plugin_mp4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
@@ -57,60 +59,75 @@ type Exception struct {
|
||||
// }
|
||||
|
||||
// 判断磁盘使用量是否中超限
|
||||
func (p *DeleteRecordTask) getDiskOutOfSpace(max float64) bool {
|
||||
exePath, err := os.Getwd()
|
||||
//pwd, _ := os.Getwd()
|
||||
//fmt.Printf("当前pwd是: %v\n", pwd)
|
||||
//if err != nil {
|
||||
// fmt.Printf("Error getting executable path: %v\n", err)
|
||||
// return false
|
||||
//}
|
||||
//// 获取路径的根目录部分
|
||||
//root := filepath.VolumeName(exePath)
|
||||
//if root == "" {
|
||||
// // 在Unix-like系统中,根目录是 "/"
|
||||
// root = "/"
|
||||
//}
|
||||
func (p *DeleteRecordTask) getDiskOutOfSpace(filePath string) bool {
|
||||
exePath := filepath.Dir(filePath)
|
||||
d, err := disk.Usage(exePath)
|
||||
if err != nil {
|
||||
if err != nil || d == nil {
|
||||
p.Error("getDiskOutOfSpace", "error", err)
|
||||
}
|
||||
p.Debug("getDiskOutOfSpace", "current path", exePath, "disk UsedPercent", d.UsedPercent, "total disk space", d.Total,
|
||||
"disk free", d.Free, "disk usage", d.Used, "AutoOverWriteDiskPercent", p.AutoOverWriteDiskPercent, "DiskMaxPercent", p.DiskMaxPercent)
|
||||
if d.UsedPercent >= max {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
p.plugin.Debug("getDiskOutOfSpace", "current path", exePath, "disk UsedPercent", d.UsedPercent, "total disk space", d.Total,
|
||||
"disk free", d.Free, "disk usage", d.Used, "AutoOverWriteDiskPercent", p.AutoOverWriteDiskPercent, "DiskMaxPercent", p.DiskMaxPercent)
|
||||
return d.UsedPercent >= p.AutoOverWriteDiskPercent
|
||||
}
|
||||
|
||||
func (p *DeleteRecordTask) deleteOldestFile() {
|
||||
//当当前磁盘使用量大于AutoOverWriteDiskPercent自动覆盖磁盘使用量配置时,自动删除最旧的文件
|
||||
//连续录像删除最旧的文件
|
||||
for p.getDiskOutOfSpace(p.AutoOverWriteDiskPercent) {
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelLow, // 查询条件:event_level = 1,非重要事件
|
||||
// 创建一个数组来存储所有的conf.FilePath
|
||||
var filePaths []string
|
||||
if len(p.plugin.GetCommonConf().OnPub.Record) > 0 {
|
||||
for _, conf := range p.plugin.GetCommonConf().OnPub.Record {
|
||||
// 处理路径,去掉最后的/$0部分,只保留目录部分
|
||||
dirPath := filepath.Dir(conf.FilePath)
|
||||
p.Info("deleteOldestFile", "original filepath", conf.FilePath, "processed filepath", dirPath)
|
||||
filePaths = append(filePaths, dirPath)
|
||||
}
|
||||
var eventRecords []m7s.RecordStream
|
||||
err := p.DB.Where(&queryRecord).Where("end_time != '1970-01-01 00:00:00'").Order("end_time ASC").Limit(1).Find(&eventRecords).Error
|
||||
if err == nil {
|
||||
if len(eventRecords) > 0 {
|
||||
for _, record := range eventRecords {
|
||||
p.Info("deleteOldestFile", "ready to delete oldestfile,ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
|
||||
err = os.Remove(record.FilePath)
|
||||
if err != nil {
|
||||
p.Error("deleteOldestFile", "delete file from disk error", err)
|
||||
}
|
||||
err = p.DB.Delete(&record).Error
|
||||
if err != nil {
|
||||
p.Error("deleteOldestFile", "delete record from disk error", err)
|
||||
}
|
||||
if p.plugin.EventRecordFilePath != "" {
|
||||
// 同样处理EventRecordFilePath
|
||||
dirPath := filepath.Dir(p.plugin.EventRecordFilePath)
|
||||
filePaths = append(filePaths, dirPath)
|
||||
}
|
||||
for _, filePath := range filePaths {
|
||||
for p.getDiskOutOfSpace(filePath) {
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelLow, // 查询条件:event_level = 1,非重要事件
|
||||
}
|
||||
var eventRecords []m7s.RecordStream
|
||||
// 使用不同的方法进行路径匹配,避免ESCAPE语法问题
|
||||
// 解决方案:用MySQL能理解的简单方式匹配路径前缀
|
||||
basePath := filePath
|
||||
// 直接替换所有反斜杠,不需要判断是否包含
|
||||
basePath = strings.Replace(basePath, "\\", "\\\\", -1)
|
||||
searchPattern := basePath + "%"
|
||||
p.Info("deleteOldestFile", "searching with path pattern", searchPattern)
|
||||
|
||||
err := p.DB.Where(&queryRecord).Where("end_time IS NOT NULL").
|
||||
Where("file_path LIKE ?", searchPattern).
|
||||
Order("end_time ASC").Find(&eventRecords).Error
|
||||
if err == nil {
|
||||
if len(eventRecords) > 0 {
|
||||
p.Info("deleteOldestFile", "found %d records", len(eventRecords))
|
||||
for _, record := range eventRecords {
|
||||
p.Info("deleteOldestFile", "ready to delete oldestfile,ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
|
||||
err = os.Remove(record.FilePath)
|
||||
if err != nil {
|
||||
p.Error("deleteOldestFile", "delete file from disk error", err)
|
||||
continue
|
||||
} else {
|
||||
err = p.DB.Delete(&record).Error
|
||||
if err != nil {
|
||||
p.Error("deleteOldestFile", "delete record from disk error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.Error("deleteOldestFile", "search record from db error", err)
|
||||
}
|
||||
} else {
|
||||
p.Error("deleteOldestFile", "search record from db error", err)
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +137,7 @@ type DeleteRecordTask struct {
|
||||
AutoOverWriteDiskPercent float64
|
||||
RecordFileExpireDays int
|
||||
DB *gorm.DB
|
||||
plugin *MP4Plugin
|
||||
}
|
||||
|
||||
func (t *DeleteRecordTask) GetTickInterval() time.Duration {
|
||||
@@ -138,7 +156,7 @@ func (t *DeleteRecordTask) Tick(any) {
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelLow, // 查询条件:event_level = low,非重要事件
|
||||
}
|
||||
err := t.DB.Where(&queryRecord).Find(&eventRecords, "end_time < ? AND end_time != '1970-01-01 00:00:00'", expireTime).Error
|
||||
err := t.DB.Where(&queryRecord).Find(&eventRecords, "end_time < ? AND end_time IS NOT NULL", expireTime).Error
|
||||
if err == nil {
|
||||
for _, record := range eventRecords {
|
||||
t.Info("RecordFileExpireDays is set to auto delete oldestfile", "ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Eyevinn/mp4ff/mp4"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"m7s.live/v5"
|
||||
v5 "m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/codec"
|
||||
@@ -20,83 +20,34 @@ import (
|
||||
|
||||
type MediaContext struct {
|
||||
io.Writer
|
||||
conn net.Conn
|
||||
wto time.Duration
|
||||
seqNumber uint32
|
||||
muxer *pkg.Muxer
|
||||
audio, video *pkg.Track
|
||||
buffer []byte
|
||||
offset int64
|
||||
conn net.Conn
|
||||
wto time.Duration
|
||||
ws bool
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
func (m *MediaContext) Write(p []byte) (n int, err error) {
|
||||
if m.conn != nil {
|
||||
if m.ws {
|
||||
m.buffer = append(m.buffer, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
if m.conn != nil && m.wto > 0 {
|
||||
m.conn.SetWriteDeadline(time.Now().Add(m.wto))
|
||||
}
|
||||
return m.Writer.Write(p)
|
||||
}
|
||||
|
||||
func (m *MediaContext) Read(p []byte) (n int, err error) {
|
||||
if m.offset >= int64(len(m.buffer)) {
|
||||
return 0, io.EOF
|
||||
func (m *MediaContext) Flush() (err error) {
|
||||
if m.ws {
|
||||
if m.wto > 0 {
|
||||
m.conn.SetWriteDeadline(time.Now().Add(m.wto))
|
||||
}
|
||||
err = wsutil.WriteServerBinary(m.conn, m.buffer)
|
||||
m.buffer = m.buffer[:0]
|
||||
}
|
||||
n = copy(p, m.buffer[m.offset:])
|
||||
m.offset += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *MediaContext) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
m.offset = offset
|
||||
case io.SeekCurrent:
|
||||
m.offset += offset
|
||||
case io.SeekEnd:
|
||||
m.offset = int64(len(m.buffer)) + offset
|
||||
}
|
||||
if m.offset < 0 {
|
||||
m.offset = 0
|
||||
}
|
||||
if m.offset > int64(len(m.buffer)) {
|
||||
m.offset = int64(len(m.buffer))
|
||||
}
|
||||
return m.offset, nil
|
||||
}
|
||||
|
||||
type TrackContext struct {
|
||||
TrackId uint32
|
||||
fragment *mp4.Fragment
|
||||
ts uint32 // 每个小片段起始时间戳
|
||||
abs uint32 // 绝对起始时间戳
|
||||
absSet bool // 是否设置过abs
|
||||
}
|
||||
|
||||
func (m *TrackContext) Push(ctx *MediaContext, dt uint32, dur uint32, data []byte, flags uint32) {
|
||||
if !m.absSet {
|
||||
m.abs = dt
|
||||
m.absSet = true
|
||||
}
|
||||
dt -= m.abs
|
||||
if m.fragment != nil && dt-m.ts > 1000 {
|
||||
m.fragment.Encode(ctx)
|
||||
m.fragment = nil
|
||||
}
|
||||
if m.fragment == nil {
|
||||
ctx.seqNumber++
|
||||
m.fragment, _ = mp4.CreateFragment(ctx.seqNumber, m.TrackId)
|
||||
m.ts = dt
|
||||
}
|
||||
m.fragment.AddFullSample(mp4.FullSample{
|
||||
Data: data,
|
||||
DecodeTime: uint64(dt),
|
||||
Sample: mp4.Sample{
|
||||
Flags: flags,
|
||||
Dur: dur,
|
||||
Size: uint32(len(data)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type MP4Plugin struct {
|
||||
pb.UnimplementedApiServer
|
||||
m7s.Plugin
|
||||
@@ -104,7 +55,8 @@ type MP4Plugin struct {
|
||||
AfterDuration time.Duration `default:"30s" desc:"事件录像结束时长,不配置则默认30s"`
|
||||
RecordFileExpireDays int `desc:"录像自动删除的天数,0或未设置表示不自动删除"`
|
||||
DiskMaxPercent float64 `default:"90" desc:"硬盘使用百分之上限值,超上限后触发报警,并停止当前所有磁盘写入动作。"`
|
||||
AutoOverWriteDiskPercent float64 `default:"80" desc:"自动覆盖功能磁盘占用上限值,超过上限时连续录像自动删除日有录像,事件录像自动删除非重要事件录像,删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
|
||||
AutoOverWriteDiskPercent float64 `default:"0" desc:"自动覆盖功能磁盘占用上限值,超过上限时连续录像自动删除日有录像,事件录像自动删除非重要事件录像,删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
|
||||
AutoRecovery bool `default:"true" desc:"是否自动恢复"`
|
||||
ExceptionPostUrl string `desc:"第三方异常上报地址"`
|
||||
EventRecordFilePath string `desc:"事件录像存放地址"`
|
||||
}
|
||||
@@ -113,22 +65,42 @@ const defaultConfig m7s.DefaultYaml = `publish:
|
||||
speed: 1`
|
||||
|
||||
// var exceptionChannel = make(chan *Exception)
|
||||
var _ = m7s.InstallPlugin[MP4Plugin](defaultConfig, &pb.Api_ServiceDesc, pb.RegisterApiHandler, pkg.NewPuller, pkg.NewRecorder)
|
||||
var _ = m7s.InstallPlugin[MP4Plugin](m7s.PluginMeta{
|
||||
DefaultYaml: defaultConfig,
|
||||
ServiceDesc: &pb.Api_ServiceDesc,
|
||||
RegisterGRPCHandler: pb.RegisterApiHandler,
|
||||
NewPuller: pkg.NewPuller,
|
||||
NewRecorder: pkg.NewRecorder,
|
||||
NewPullProxy: m7s.NewHTTPPullPorxy,
|
||||
})
|
||||
|
||||
func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
|
||||
return map[string]http.HandlerFunc{
|
||||
"/download/{streamPath...}": p.download,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) OnInit() (err error) {
|
||||
if p.DB != nil {
|
||||
err = p.DB.AutoMigrate(&Exception{})
|
||||
var deleteRecordTask DeleteRecordTask
|
||||
deleteRecordTask.DB = p.DB
|
||||
deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent
|
||||
deleteRecordTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
|
||||
deleteRecordTask.RecordFileExpireDays = p.RecordFileExpireDays
|
||||
p.AddTask(&deleteRecordTask)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if p.AutoOverWriteDiskPercent > 0 {
|
||||
var deleteRecordTask DeleteRecordTask
|
||||
deleteRecordTask.DB = p.DB
|
||||
deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent
|
||||
deleteRecordTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
|
||||
deleteRecordTask.RecordFileExpireDays = p.RecordFileExpireDays
|
||||
deleteRecordTask.plugin = p
|
||||
p.AddTask(&deleteRecordTask)
|
||||
}
|
||||
if p.AutoRecovery {
|
||||
var recoveryTask RecordRecoveryTask
|
||||
recoveryTask.DB = p.DB
|
||||
recoveryTask.plugin = p
|
||||
p.AddTask(&recoveryTask)
|
||||
}
|
||||
}
|
||||
// go func() { //处理所有异常,录像中断异常、录像读取异常、录像导出文件中断、磁盘容量低于阈值异常、磁盘异常
|
||||
// for exception := range exceptionChannel {
|
||||
@@ -149,6 +121,7 @@ func (p *MP4Plugin) OnInit() (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".mp4")
|
||||
if r.URL.RawQuery != "" {
|
||||
@@ -165,30 +138,34 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wto := p.GetCommonConf().WriteTimeout
|
||||
ctx.wto = p.GetCommonConf().WriteTimeout
|
||||
if ctx.conn == nil {
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if hijacker, ok := w.(http.Hijacker); ok && wto > 0 {
|
||||
if hijacker, ok := w.(http.Hijacker); ok && ctx.wto > 0 {
|
||||
ctx.conn, _, _ = hijacker.Hijack()
|
||||
ctx.conn.SetWriteDeadline(time.Now().Add(wto))
|
||||
ctx.conn.SetWriteDeadline(time.Now().Add(ctx.wto))
|
||||
ctx.Writer = ctx.conn
|
||||
} else {
|
||||
ctx.Writer = w
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.conn != nil {
|
||||
ctx.Writer = ctx.conn
|
||||
} else {
|
||||
ctx.Writer = w
|
||||
w.(http.Flusher).Flush()
|
||||
ctx.ws = true
|
||||
ctx.Writer = ctx.conn
|
||||
}
|
||||
|
||||
ctx.wto = p.GetCommonConf().WriteTimeout
|
||||
ctx.muxer = pkg.NewMuxer(pkg.FLAG_FRAGMENT)
|
||||
ctx.muxer.WriteInitSegment(ctx.Writer)
|
||||
muxer := pkg.NewMuxer(pkg.FLAG_FRAGMENT)
|
||||
err = muxer.WriteInitSegment(&ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var offsetAudio, offsetVideo = 1, 5
|
||||
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
var audio, video *pkg.Track
|
||||
var nextFragmentId uint32
|
||||
if sub.Publisher.HasVideoTrack() && sub.SubVideo {
|
||||
v := sub.Publisher.VideoTrack.AVTrack
|
||||
if err = v.WaitReady(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -201,24 +178,33 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case codec.FourCC_H265:
|
||||
codecID = box.MP4_CODEC_H265
|
||||
}
|
||||
ctx.video = ctx.muxer.AddTrack(codecID)
|
||||
ctx.video.Timescale = 1000
|
||||
|
||||
video = muxer.AddTrack(codecID)
|
||||
video.Timescale = 1000
|
||||
video.Samplelist = []box.Sample{
|
||||
{
|
||||
Offset: 0,
|
||||
Data: nil,
|
||||
Size: 0,
|
||||
Timestamp: 0,
|
||||
Duration: 0,
|
||||
KeyFrame: true,
|
||||
},
|
||||
}
|
||||
switch v.ICodecCtx.FourCC() {
|
||||
case codec.FourCC_H264:
|
||||
h264Ctx := v.ICodecCtx.GetBase().(*codec.H264Ctx)
|
||||
ctx.video.ExtraData = h264Ctx.Record
|
||||
ctx.video.Width = uint32(h264Ctx.Width())
|
||||
ctx.video.Height = uint32(h264Ctx.Height())
|
||||
video.ExtraData = h264Ctx.Record
|
||||
video.Width = uint32(h264Ctx.Width())
|
||||
video.Height = uint32(h264Ctx.Height())
|
||||
case codec.FourCC_H265:
|
||||
h265Ctx := v.ICodecCtx.GetBase().(*codec.H265Ctx)
|
||||
ctx.video.ExtraData = h265Ctx.Record
|
||||
ctx.video.Width = uint32(h265Ctx.Width())
|
||||
ctx.video.Height = uint32(h265Ctx.Height())
|
||||
video.ExtraData = h265Ctx.Record
|
||||
video.Width = uint32(h265Ctx.Width())
|
||||
video.Height = uint32(h265Ctx.Height())
|
||||
}
|
||||
}
|
||||
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
if sub.Publisher.HasAudioTrack() && sub.SubAudio {
|
||||
a := sub.Publisher.AudioTrack.AVTrack
|
||||
if err = a.WaitReady(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -228,60 +214,98 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch a.ICodecCtx.FourCC() {
|
||||
case codec.FourCC_MP4A:
|
||||
codecID = box.MP4_CODEC_AAC
|
||||
case codec.FourCC_ALAW:
|
||||
codecID = box.MP4_CODEC_G711A
|
||||
case codec.FourCC_ULAW:
|
||||
codecID = box.MP4_CODEC_G711U
|
||||
case codec.FourCC_OPUS:
|
||||
codecID = box.MP4_CODEC_OPUS
|
||||
}
|
||||
ctx.audio = ctx.muxer.AddTrack(codecID)
|
||||
ctx.audio.Timescale = 1000
|
||||
audio = muxer.AddTrack(codecID)
|
||||
audio.Timescale = 1000
|
||||
audioCtx := a.ICodecCtx.(v5.IAudioCodecCtx)
|
||||
ctx.audio.SampleRate = uint32(audioCtx.GetSampleRate())
|
||||
ctx.audio.ChannelCount = uint8(audioCtx.GetChannels())
|
||||
ctx.audio.SampleSize = uint16(audioCtx.GetSampleSize())
|
||||
|
||||
audio.SampleRate = uint32(audioCtx.GetSampleRate())
|
||||
audio.ChannelCount = uint8(audioCtx.GetChannels())
|
||||
audio.SampleSize = uint16(audioCtx.GetSampleSize())
|
||||
audio.Samplelist = []box.Sample{
|
||||
{
|
||||
Offset: 0,
|
||||
Data: nil,
|
||||
Size: 0,
|
||||
Timestamp: 0,
|
||||
Duration: 0,
|
||||
KeyFrame: true,
|
||||
},
|
||||
}
|
||||
switch a.ICodecCtx.FourCC() {
|
||||
case codec.FourCC_MP4A:
|
||||
offsetAudio = 2
|
||||
ctx.audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes
|
||||
audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes
|
||||
default:
|
||||
offsetAudio = 1
|
||||
}
|
||||
}
|
||||
|
||||
err = ctx.muxer.WriteInitSegment(&ctx)
|
||||
err = muxer.WriteMoov(&ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
m7s.PlayBlock(sub, func(audio *rtmp.RTMPAudio) error {
|
||||
bs := audio.Memory.ToBytes()
|
||||
if ctx.ws {
|
||||
ctx.Flush()
|
||||
}
|
||||
m7s.PlayBlock(sub, func(frame *rtmp.RTMPAudio) (err error) {
|
||||
bs := frame.Memory.ToBytes()
|
||||
if offsetAudio == 2 && bs[1] == 0 {
|
||||
return nil
|
||||
}
|
||||
sample := box.Sample{
|
||||
Offset: 0,
|
||||
Data: bs[offsetAudio:],
|
||||
Size: len(bs) - offsetAudio,
|
||||
DTS: uint64(audio.Timestamp),
|
||||
PTS: uint64(audio.Timestamp),
|
||||
KeyFrame: true,
|
||||
if audio.Samplelist[0].Data != nil {
|
||||
audio.Samplelist[0].Duration = sub.AudioReader.AbsTime - audio.Samplelist[0].Timestamp
|
||||
nextFragmentId++
|
||||
// Create moof box for this track
|
||||
moof := audio.MakeMoof(nextFragmentId)
|
||||
// Create mdat box for this track
|
||||
mdat := box.CreateDataBox(box.TypeMDAT, audio.Samplelist[0].Data)
|
||||
box.WriteTo(&ctx, moof, mdat)
|
||||
if ctx.ws {
|
||||
err = ctx.Flush()
|
||||
}
|
||||
}
|
||||
ctx.audio.AddSampleEntry(sample)
|
||||
return nil
|
||||
}, func(video *rtmp.RTMPVideo) error {
|
||||
bs := video.Memory.ToBytes()
|
||||
if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced && bs[0]&0b1111 == rtmp.PacketTypeCodedFrames {
|
||||
offsetVideo = 8
|
||||
audio.Samplelist[0].Timestamp = sub.AudioReader.AbsTime
|
||||
audio.Samplelist[0].Data = bs[offsetAudio:]
|
||||
audio.Samplelist[0].Size = len(audio.Samplelist[0].Data)
|
||||
return
|
||||
}, func(frame *rtmp.RTMPVideo) (err error) {
|
||||
bs := frame.Memory.ToBytes()
|
||||
if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced {
|
||||
switch bs[0] & 0b1111 {
|
||||
case rtmp.PacketTypeCodedFrames:
|
||||
offsetVideo = 8
|
||||
case rtmp.PacketTypeSequenceStart:
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if bs[1] == 0 {
|
||||
return nil
|
||||
}
|
||||
offsetVideo = 5
|
||||
}
|
||||
sample := box.Sample{
|
||||
Offset: 0,
|
||||
Data: bs[offsetVideo:],
|
||||
Size: len(bs) - offsetVideo,
|
||||
DTS: uint64(video.Timestamp),
|
||||
PTS: uint64(video.Timestamp),
|
||||
KeyFrame: sub.VideoReader.Value.IDR,
|
||||
if video.Samplelist[0].Data != nil {
|
||||
video.Samplelist[0].Duration = sub.VideoReader.AbsTime - video.Samplelist[0].Timestamp
|
||||
nextFragmentId++
|
||||
// Create moof box for this track
|
||||
moof := video.MakeMoof(nextFragmentId)
|
||||
// Create mdat box for this track
|
||||
mdat := box.CreateDataBox(box.TypeMDAT, video.Samplelist[0].Data)
|
||||
box.WriteTo(&ctx, moof, mdat)
|
||||
if ctx.ws {
|
||||
err = ctx.Flush()
|
||||
}
|
||||
}
|
||||
ctx.video.AddSampleEntry(sample)
|
||||
return nil
|
||||
video.Samplelist[0].Data = bs[offsetVideo:]
|
||||
video.Samplelist[0].Size = len(bs) - offsetVideo
|
||||
video.Samplelist[0].Timestamp = sub.VideoReader.AbsTime
|
||||
video.Samplelist[0].CTS = frame.CTS
|
||||
video.Samplelist[0].KeyFrame = sub.VideoReader.Value.IDR
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.19.1
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.28.3
|
||||
// source: mp4.proto
|
||||
|
||||
package pb
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
durationpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
_ "google.golang.org/protobuf/types/known/timestamppb"
|
||||
pb "m7s.live/v5/pb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,26 +26,24 @@ const (
|
||||
)
|
||||
|
||||
type ReqRecordList struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
|
||||
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
|
||||
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
|
||||
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||
EventLevel string `protobuf:"bytes,8,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
|
||||
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
|
||||
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
|
||||
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqRecordList) Reset() {
|
||||
*x = ReqRecordList{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqRecordList) String() string {
|
||||
@@ -56,7 +54,7 @@ func (*ReqRecordList) ProtoMessage() {}
|
||||
|
||||
func (x *ReqRecordList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -120,25 +118,29 @@ func (x *ReqRecordList) GetMode() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type ReqRecordDelete struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
func (x *ReqRecordList) GetEventLevel() string {
|
||||
if x != nil {
|
||||
return x.EventLevel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
|
||||
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
|
||||
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
|
||||
type ReqRecordDelete struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
|
||||
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
|
||||
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) Reset() {
|
||||
*x = ReqRecordDelete{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) String() string {
|
||||
@@ -149,7 +151,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
|
||||
|
||||
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -200,28 +202,24 @@ func (x *ReqRecordDelete) GetRange() string {
|
||||
}
|
||||
|
||||
type ReqEventRecord struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
EventId string `protobuf:"bytes,2,opt,name=eventId,proto3" json:"eventId,omitempty"`
|
||||
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` //auto=连续录像模式,event=事件录像模式
|
||||
EventName string `protobuf:"bytes,4,opt,name=eventName,proto3" json:"eventName,omitempty"`
|
||||
BeforeDuration string `protobuf:"bytes,5,opt,name=beforeDuration,proto3" json:"beforeDuration,omitempty"`
|
||||
AfterDuration string `protobuf:"bytes,6,opt,name=afterDuration,proto3" json:"afterDuration,omitempty"`
|
||||
EventDesc string `protobuf:"bytes,7,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
|
||||
EventLevel string `protobuf:"bytes,8,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"` //事件级别,0表示重要事件,无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
|
||||
Fragment string `protobuf:"bytes,9,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
EventId string `protobuf:"bytes,2,opt,name=eventId,proto3" json:"eventId,omitempty"`
|
||||
EventName string `protobuf:"bytes,3,opt,name=eventName,proto3" json:"eventName,omitempty"`
|
||||
BeforeDuration string `protobuf:"bytes,4,opt,name=beforeDuration,proto3" json:"beforeDuration,omitempty"`
|
||||
AfterDuration string `protobuf:"bytes,5,opt,name=afterDuration,proto3" json:"afterDuration,omitempty"`
|
||||
EventDesc string `protobuf:"bytes,6,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
|
||||
EventLevel string `protobuf:"bytes,7,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"` //事件级别,0表示重要事件,无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
|
||||
Fragment string `protobuf:"bytes,8,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqEventRecord) Reset() {
|
||||
*x = ReqEventRecord{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqEventRecord) String() string {
|
||||
@@ -232,7 +230,7 @@ func (*ReqEventRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ReqEventRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -261,13 +259,6 @@ func (x *ReqEventRecord) GetEventId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ReqEventRecord) GetMode() string {
|
||||
if x != nil {
|
||||
return x.Mode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ReqEventRecord) GetEventName() string {
|
||||
if x != nil {
|
||||
return x.EventName
|
||||
@@ -311,22 +302,19 @@ func (x *ReqEventRecord) GetFragment() string {
|
||||
}
|
||||
|
||||
type ResponseEventRecord struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data uint32 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data uint32 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResponseEventRecord) Reset() {
|
||||
*x = ResponseEventRecord{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResponseEventRecord) String() string {
|
||||
@@ -337,7 +325,7 @@ func (*ResponseEventRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ResponseEventRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -374,22 +362,19 @@ func (x *ResponseEventRecord) GetData() uint32 {
|
||||
}
|
||||
|
||||
type ReqStartRecord struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Fragment *durationpb.Duration `protobuf:"bytes,2,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
FilePath string `protobuf:"bytes,3,opt,name=filePath,proto3" json:"filePath,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Fragment *durationpb.Duration `protobuf:"bytes,2,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
FilePath string `protobuf:"bytes,3,opt,name=filePath,proto3" json:"filePath,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqStartRecord) Reset() {
|
||||
*x = ReqStartRecord{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqStartRecord) String() string {
|
||||
@@ -400,7 +385,7 @@ func (*ReqStartRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ReqStartRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -437,22 +422,19 @@ func (x *ReqStartRecord) GetFilePath() string {
|
||||
}
|
||||
|
||||
type ResponseStartRecord struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResponseStartRecord) Reset() {
|
||||
*x = ResponseStartRecord{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_mp4_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_mp4_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResponseStartRecord) String() string {
|
||||
@@ -463,7 +445,7 @@ func (*ResponseStartRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ResponseStartRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -499,152 +481,274 @@ func (x *ResponseStartRecord) GetData() uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type ReqStopRecord struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqStopRecord) Reset() {
|
||||
*x = ReqStopRecord{}
|
||||
mi := &file_mp4_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqStopRecord) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ReqStopRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ReqStopRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ReqStopRecord.ProtoReflect.Descriptor instead.
|
||||
func (*ReqStopRecord) Descriptor() ([]byte, []int) {
|
||||
return file_mp4_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ReqStopRecord) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ResponseStopRecord struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResponseStopRecord) Reset() {
|
||||
*x = ResponseStopRecord{}
|
||||
mi := &file_mp4_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResponseStopRecord) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResponseStopRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ResponseStopRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_mp4_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ResponseStopRecord.ProtoReflect.Descriptor instead.
|
||||
func (*ResponseStopRecord) Descriptor() ([]byte, []int) {
|
||||
return file_mp4_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *ResponseStopRecord) GetCode() int32 {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ResponseStopRecord) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ResponseStopRecord) GetData() uint64 {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_mp4_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_mp4_proto_rawDesc = []byte{
|
||||
var file_mp4_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x09, 0x6d, 0x70, 0x34, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x6d, 0x70, 0x34,
|
||||
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
|
||||
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
|
||||
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c,
|
||||
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x52,
|
||||
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
|
||||
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e,
|
||||
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61,
|
||||
0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67,
|
||||
0x65, 0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6d, 0x6f, 0x64, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f,
|
||||
0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18,
|
||||
0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
|
||||
0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0xa4, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71,
|
||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
|
||||
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c, 0x6f,
|
||||
0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd7, 0x01, 0x0a, 0x0d, 0x52, 0x65,
|
||||
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
|
||||
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76,
|
||||
0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76,
|
||||
0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72,
|
||||
0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12,
|
||||
0x24, 0x0a, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65,
|
||||
0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44,
|
||||
0x65, 0x73, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
|
||||
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x72,
|
||||
0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67,
|
||||
0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x04,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x67,
|
||||
0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67, 0x65,
|
||||
0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18,
|
||||
0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d,
|
||||
0x6f, 0x64, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
|
||||
0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65,
|
||||
0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18,
|
||||
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22,
|
||||
0x57, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65,
|
||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73,
|
||||
0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x0d, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71,
|
||||
0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
|
||||
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66,
|
||||
0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
|
||||
0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57,
|
||||
0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52,
|
||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
|
||||
0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
|
||||
0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xdf, 0x03, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12,
|
||||
0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65,
|
||||
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, 0x6c,
|
||||
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f,
|
||||
0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
|
||||
0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07, 0x43, 0x61, 0x74, 0x61,
|
||||
0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x67, 0x6c,
|
||||
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74,
|
||||
0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x6d,
|
||||
0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x62,
|
||||
0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
|
||||
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a, 0x16,
|
||||
0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22, 0x1f,
|
||||
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f,
|
||||
0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a,
|
||||
0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x72, 0x74,
|
||||
0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
|
||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22,
|
||||
0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70,
|
||||
0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x3a, 0x01, 0x2a,
|
||||
0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12,
|
||||
0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65,
|
||||
0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x29,
|
||||
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69,
|
||||
0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
|
||||
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73,
|
||||
0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f,
|
||||
0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
0x76, 0x65, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72,
|
||||
0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72,
|
||||
0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02,
|
||||
0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x45,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
|
||||
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x76,
|
||||
0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x65, 0x66, 0x6f,
|
||||
0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x66,
|
||||
0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x18, 0x06, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x12, 0x1e,
|
||||
0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72,
|
||||
0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
|
||||
0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x64,
|
||||
0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74,
|
||||
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
|
||||
0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
|
||||
0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61,
|
||||
0x74, 0x61, 0x22, 0x2f, 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74,
|
||||
0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50,
|
||||
0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53,
|
||||
0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xc4, 0x04, 0x0a, 0x03,
|
||||
0x61, 0x70, 0x69, 0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70,
|
||||
0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a,
|
||||
0x14, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f,
|
||||
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07,
|
||||
0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
|
||||
0x17, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12,
|
||||
0x12, 0x10, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c,
|
||||
0x6f, 0x67, 0x12, 0x62, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d,
|
||||
0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65,
|
||||
0x74, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93,
|
||||
0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
|
||||
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53,
|
||||
0x74, 0x61, 0x72, 0x74, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76,
|
||||
0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14,
|
||||
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73,
|
||||
0x74, 0x61, 0x72, 0x74, 0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61,
|
||||
0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f,
|
||||
0x72, 0x64, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x01, 0x2a, 0x22, 0x1e, 0x2f,
|
||||
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73,
|
||||
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x63, 0x0a,
|
||||
0x0a, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x2e, 0x6d, 0x70,
|
||||
0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a,
|
||||
0x17, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74,
|
||||
0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22,
|
||||
0x3a, 0x01, 0x2a, 0x22, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74,
|
||||
0x6f, 0x70, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a,
|
||||
0x2a, 0x7d, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76,
|
||||
0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_mp4_proto_rawDescOnce sync.Once
|
||||
file_mp4_proto_rawDescData = file_mp4_proto_rawDesc
|
||||
file_mp4_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_mp4_proto_rawDescGZIP() []byte {
|
||||
file_mp4_proto_rawDescOnce.Do(func() {
|
||||
file_mp4_proto_rawDescData = protoimpl.X.CompressGZIP(file_mp4_proto_rawDescData)
|
||||
file_mp4_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mp4_proto_rawDesc), len(file_mp4_proto_rawDesc)))
|
||||
})
|
||||
return file_mp4_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_mp4_proto_goTypes = []interface{}{
|
||||
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_mp4_proto_goTypes = []any{
|
||||
(*ReqRecordList)(nil), // 0: mp4.ReqRecordList
|
||||
(*ReqRecordDelete)(nil), // 1: mp4.ReqRecordDelete
|
||||
(*ReqEventRecord)(nil), // 2: mp4.ReqEventRecord
|
||||
(*ResponseEventRecord)(nil), // 3: mp4.ResponseEventRecord
|
||||
(*ReqStartRecord)(nil), // 4: mp4.ReqStartRecord
|
||||
(*ResponseStartRecord)(nil), // 5: mp4.ResponseStartRecord
|
||||
(*durationpb.Duration)(nil), // 6: google.protobuf.Duration
|
||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
||||
(*pb.ResponseList)(nil), // 8: global.ResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 9: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 10: global.ResponseDelete
|
||||
(*ReqStopRecord)(nil), // 6: mp4.ReqStopRecord
|
||||
(*ResponseStopRecord)(nil), // 7: mp4.ResponseStopRecord
|
||||
(*durationpb.Duration)(nil), // 8: google.protobuf.Duration
|
||||
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
|
||||
(*pb.ResponseList)(nil), // 10: global.ResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 11: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 12: global.ResponseDelete
|
||||
}
|
||||
var file_mp4_proto_depIdxs = []int32{
|
||||
6, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
|
||||
8, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
|
||||
0, // 1: mp4.api.List:input_type -> mp4.ReqRecordList
|
||||
7, // 2: mp4.api.Catalog:input_type -> google.protobuf.Empty
|
||||
9, // 2: mp4.api.Catalog:input_type -> google.protobuf.Empty
|
||||
1, // 3: mp4.api.Delete:input_type -> mp4.ReqRecordDelete
|
||||
2, // 4: mp4.api.EventStart:input_type -> mp4.ReqEventRecord
|
||||
4, // 5: mp4.api.StartRecord:input_type -> mp4.ReqStartRecord
|
||||
8, // 6: mp4.api.List:output_type -> global.ResponseList
|
||||
9, // 7: mp4.api.Catalog:output_type -> global.ResponseCatalog
|
||||
10, // 8: mp4.api.Delete:output_type -> global.ResponseDelete
|
||||
3, // 9: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
|
||||
5, // 10: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
|
||||
6, // [6:11] is the sub-list for method output_type
|
||||
1, // [1:6] is the sub-list for method input_type
|
||||
6, // 6: mp4.api.StopRecord:input_type -> mp4.ReqStopRecord
|
||||
10, // 7: mp4.api.List:output_type -> global.ResponseList
|
||||
11, // 8: mp4.api.Catalog:output_type -> global.ResponseCatalog
|
||||
12, // 9: mp4.api.Delete:output_type -> global.ResponseDelete
|
||||
3, // 10: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
|
||||
5, // 11: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
|
||||
7, // 12: mp4.api.StopRecord:output_type -> mp4.ResponseStopRecord
|
||||
7, // [7:13] is the sub-list for method output_type
|
||||
1, // [1:7] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
@@ -655,87 +759,13 @@ func file_mp4_proto_init() {
|
||||
if File_mp4_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_mp4_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqRecordList); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_mp4_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqRecordDelete); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_mp4_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqEventRecord); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_mp4_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ResponseEventRecord); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_mp4_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqStartRecord); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_mp4_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ResponseStartRecord); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_mp4_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mp4_proto_rawDesc), len(file_mp4_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
@@ -744,7 +774,6 @@ func file_mp4_proto_init() {
|
||||
MessageInfos: file_mp4_proto_msgTypes,
|
||||
}.Build()
|
||||
File_mp4_proto = out.File
|
||||
file_mp4_proto_rawDesc = nil
|
||||
file_mp4_proto_goTypes = nil
|
||||
file_mp4_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -266,10 +266,71 @@ func local_request_Api_StartRecord_0(ctx context.Context, marshaler runtime.Mars
|
||||
|
||||
}
|
||||
|
||||
func request_Api_StopRecord_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ReqStopRecord
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["streamPath"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
|
||||
}
|
||||
|
||||
protoReq.StreamPath, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
|
||||
}
|
||||
|
||||
msg, err := client.StopRecord(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_StopRecord_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ReqStopRecord
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["streamPath"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
|
||||
}
|
||||
|
||||
protoReq.StreamPath, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
|
||||
}
|
||||
|
||||
msg, err := server.StopRecord(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterApiHandlerServer registers the http handlers for service Api to "mux".
|
||||
// UnaryRPC :call ApiServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
|
||||
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
||||
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
|
||||
|
||||
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
@@ -397,27 +458,52 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Api_StopRecord_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/mp4.Api/StopRecord", runtime.WithHTTPPathPattern("/mp4/api/stop/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_StopRecord_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_StopRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.DialContext(ctx, endpoint, opts...)
|
||||
conn, err := grpc.NewClient(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
@@ -435,7 +521,7 @@ func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.C
|
||||
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "ApiClient" to call the correct interceptors.
|
||||
// "ApiClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
||||
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
@@ -548,6 +634,28 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Api_StopRecord_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/mp4.Api/StopRecord", runtime.WithHTTPPathPattern("/mp4/api/stop/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_StopRecord_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_StopRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -561,6 +669,8 @@ var (
|
||||
pattern_Api_EventStart_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"mp4", "api", "event", "start"}, ""))
|
||||
|
||||
pattern_Api_StartRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"mp4", "api", "start", "streamPath"}, ""))
|
||||
|
||||
pattern_Api_StopRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"mp4", "api", "stop", "streamPath"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -573,4 +683,6 @@ var (
|
||||
forward_Api_EventStart_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_StartRecord_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_StopRecord_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
syntax = "proto3";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/duration.proto";
|
||||
import "global.proto";
|
||||
package mp4;
|
||||
@@ -36,6 +35,12 @@ service api {
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc StopRecord (ReqStopRecord) returns (ResponseStopRecord) {
|
||||
option (google.api.http) = {
|
||||
post: "/mp4/api/stop/{streamPath=**}"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message ReqRecordList {
|
||||
@@ -46,6 +51,7 @@ message ReqRecordList {
|
||||
uint32 pageNum = 5;
|
||||
uint32 pageSize = 6;
|
||||
string mode = 7;
|
||||
string eventLevel = 8;
|
||||
}
|
||||
|
||||
message ReqRecordDelete {
|
||||
@@ -59,13 +65,12 @@ message ReqRecordDelete {
|
||||
message ReqEventRecord {
|
||||
string streamPath = 1;
|
||||
string eventId = 2;
|
||||
string mode = 3;//auto=连续录像模式,event=事件录像模式
|
||||
string eventName = 4;
|
||||
string beforeDuration = 5;
|
||||
string afterDuration = 6;
|
||||
string eventDesc = 7;
|
||||
string eventLevel = 8;//事件级别,0表示重要事件,无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
|
||||
string fragment = 9;
|
||||
string eventName = 3;
|
||||
string beforeDuration = 4;
|
||||
string afterDuration = 5;
|
||||
string eventDesc = 6;
|
||||
string eventLevel = 7;//事件级别,0表示重要事件,无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
|
||||
string fragment = 8;
|
||||
}
|
||||
|
||||
message ResponseEventRecord {
|
||||
@@ -84,4 +89,14 @@ message ResponseStartRecord {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint64 data = 3;
|
||||
}
|
||||
|
||||
message ReqStopRecord {
|
||||
string streamPath = 1;
|
||||
}
|
||||
|
||||
message ResponseStopRecord {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint64 data = 3;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.1
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: mp4.proto
|
||||
|
||||
package pb
|
||||
@@ -17,8 +17,17 @@ import (
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Api_List_FullMethodName = "/mp4.api/List"
|
||||
Api_Catalog_FullMethodName = "/mp4.api/Catalog"
|
||||
Api_Delete_FullMethodName = "/mp4.api/Delete"
|
||||
Api_EventStart_FullMethodName = "/mp4.api/EventStart"
|
||||
Api_StartRecord_FullMethodName = "/mp4.api/StartRecord"
|
||||
Api_StopRecord_FullMethodName = "/mp4.api/StopRecord"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
@@ -29,6 +38,7 @@ type ApiClient interface {
|
||||
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
|
||||
EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error)
|
||||
StartRecord(ctx context.Context, in *ReqStartRecord, opts ...grpc.CallOption) (*ResponseStartRecord, error)
|
||||
StopRecord(ctx context.Context, in *ReqStopRecord, opts ...grpc.CallOption) (*ResponseStopRecord, error)
|
||||
}
|
||||
|
||||
type apiClient struct {
|
||||
@@ -40,8 +50,9 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
|
||||
}
|
||||
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.ResponseList)
|
||||
err := c.cc.Invoke(ctx, "/mp4.api/List", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,8 +60,9 @@ func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.Ca
|
||||
}
|
||||
|
||||
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.ResponseCatalog)
|
||||
err := c.cc.Invoke(ctx, "/mp4.api/Catalog", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,8 +70,9 @@ func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.ResponseDelete)
|
||||
err := c.cc.Invoke(ctx, "/mp4.api/Delete", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,8 +80,9 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
|
||||
}
|
||||
|
||||
func (c *apiClient) EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseEventRecord)
|
||||
err := c.cc.Invoke(ctx, "/mp4.api/EventStart", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_EventStart_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,8 +90,19 @@ func (c *apiClient) EventStart(ctx context.Context, in *ReqEventRecord, opts ...
|
||||
}
|
||||
|
||||
func (c *apiClient) StartRecord(ctx context.Context, in *ReqStartRecord, opts ...grpc.CallOption) (*ResponseStartRecord, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseStartRecord)
|
||||
err := c.cc.Invoke(ctx, "/mp4.api/StartRecord", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_StartRecord_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) StopRecord(ctx context.Context, in *ReqStopRecord, opts ...grpc.CallOption) (*ResponseStopRecord, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResponseStopRecord)
|
||||
err := c.cc.Invoke(ctx, Api_StopRecord_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,19 +111,23 @@ func (c *apiClient) StartRecord(ctx context.Context, in *ReqStartRecord, opts ..
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
// All implementations must embed UnimplementedApiServer
|
||||
// for forward compatibility
|
||||
// for forward compatibility.
|
||||
type ApiServer interface {
|
||||
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
|
||||
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
|
||||
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
|
||||
EventStart(context.Context, *ReqEventRecord) (*ResponseEventRecord, error)
|
||||
StartRecord(context.Context, *ReqStartRecord) (*ResponseStartRecord, error)
|
||||
StopRecord(context.Context, *ReqStopRecord) (*ResponseStopRecord, error)
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
// UnimplementedApiServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedApiServer struct {
|
||||
}
|
||||
// UnimplementedApiServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedApiServer struct{}
|
||||
|
||||
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||
@@ -115,7 +144,11 @@ func (UnimplementedApiServer) EventStart(context.Context, *ReqEventRecord) (*Res
|
||||
func (UnimplementedApiServer) StartRecord(context.Context, *ReqStartRecord) (*ResponseStartRecord, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StartRecord not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) StopRecord(context.Context, *ReqStopRecord) (*ResponseStopRecord, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StopRecord not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
|
||||
func (UnimplementedApiServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ApiServer will
|
||||
@@ -125,6 +158,13 @@ type UnsafeApiServer interface {
|
||||
}
|
||||
|
||||
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
|
||||
// If the following call pancis, it indicates UnimplementedApiServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Api_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
@@ -138,7 +178,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mp4.api/List",
|
||||
FullMethod: Api_List_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
|
||||
@@ -156,7 +196,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mp4.api/Catalog",
|
||||
FullMethod: Api_Catalog_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
|
||||
@@ -174,7 +214,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mp4.api/Delete",
|
||||
FullMethod: Api_Delete_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))
|
||||
@@ -192,7 +232,7 @@ func _Api_EventStart_Handler(srv interface{}, ctx context.Context, dec func(inte
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mp4.api/EventStart",
|
||||
FullMethod: Api_EventStart_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).EventStart(ctx, req.(*ReqEventRecord))
|
||||
@@ -210,7 +250,7 @@ func _Api_StartRecord_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mp4.api/StartRecord",
|
||||
FullMethod: Api_StartRecord_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StartRecord(ctx, req.(*ReqStartRecord))
|
||||
@@ -218,6 +258,24 @@ func _Api_StartRecord_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_StopRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReqStopRecord)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).StopRecord(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_StopRecord_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).StopRecord(ctx, req.(*ReqStopRecord))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Api_ServiceDesc is the grpc.ServiceDesc for Api service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -245,6 +303,10 @@ var Api_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "StartRecord",
|
||||
Handler: _Api_StartRecord_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "StopRecord",
|
||||
Handler: _Api_StopRecord_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "mp4.proto",
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package bits
|
||||
|
||||
// Reader is a bit stream reader
|
||||
type Reader struct {
|
||||
Data []byte
|
||||
Offset int
|
||||
}
|
||||
|
||||
// Skip skips n bits
|
||||
func (r *Reader) Skip(n int) {
|
||||
r.Offset += n
|
||||
}
|
||||
|
||||
// ReadBit reads a single bit
|
||||
func (r *Reader) ReadBit() (uint, error) {
|
||||
if r.Offset/8 >= len(r.Data) {
|
||||
return 0, nil
|
||||
}
|
||||
b := r.Data[r.Offset/8]
|
||||
v := (b >> (7 - (r.Offset % 8))) & 0x01
|
||||
r.Offset++
|
||||
return uint(v), nil
|
||||
}
|
||||
|
||||
// ReadExpGolomb reads an Exp-Golomb code
|
||||
func (r *Reader) ReadExpGolomb() (uint, error) {
|
||||
leadingZeroBits := 0
|
||||
for {
|
||||
b, err := r.ReadBit()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if b == 1 {
|
||||
break
|
||||
}
|
||||
leadingZeroBits++
|
||||
}
|
||||
|
||||
result := uint(1<<leadingZeroBits) - 1
|
||||
for i := 0; i < leadingZeroBits; i++ {
|
||||
b, err := r.ReadBit()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
result = (result << 1) | uint(b)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReadSE reads a signed Exp-Golomb code
|
||||
func (r *Reader) ReadSE() (int, error) {
|
||||
val, err := r.ReadExpGolomb()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sign := ((val & 0x01) << 1) - 1
|
||||
val = ((val >> 1) + (val & 0x01)) * uint(sign)
|
||||
return int(val), nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package box
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
@@ -15,7 +14,7 @@ type (
|
||||
BoxHeader interface {
|
||||
Type() BoxType
|
||||
HeaderSize() uint32
|
||||
Size() uint32
|
||||
Size() uint64
|
||||
Header() BoxHeader
|
||||
HeaderWriteTo(w io.Writer) (n int64, err error)
|
||||
}
|
||||
@@ -85,7 +84,7 @@ func CreateContainerBox(typ BoxType, children ...IBox) *ContainerBox {
|
||||
if reflect.ValueOf(child).IsNil() {
|
||||
continue
|
||||
}
|
||||
size += child.Size()
|
||||
size += uint32(child.Size())
|
||||
realChildren = append(realChildren, child)
|
||||
}
|
||||
return &ContainerBox{
|
||||
@@ -101,9 +100,9 @@ func (b *BigBox) HeaderSize() uint32 { return BasicBoxLen + 8 }
|
||||
|
||||
func (b *BaseBox) Header() BoxHeader { return b }
|
||||
func (b *BaseBox) HeaderSize() uint32 { return BasicBoxLen }
|
||||
func (b *BaseBox) Size() uint32 { return b.size }
|
||||
|
||||
func (b *BaseBox) Type() BoxType { return b.typ }
|
||||
func (b *BaseBox) Size() uint64 { return uint64(b.size) }
|
||||
func (b *BigBox) Size() uint64 { return uint64(b.size) }
|
||||
func (b *BaseBox) Type() BoxType { return b.typ }
|
||||
|
||||
func (b *BaseBox) HeaderWriteTo(w io.Writer) (n int64, err error) {
|
||||
var tmp [4]byte
|
||||
@@ -161,7 +160,7 @@ func (b *FullBox) HeaderSize() uint32 { return FullBoxLen }
|
||||
func WriteTo(w io.Writer, box ...IBox) (n int64, err error) {
|
||||
var n1, n2 int64
|
||||
for _, b := range box {
|
||||
if b == nil {
|
||||
if reflect.ValueOf(b).IsNil() {
|
||||
continue
|
||||
}
|
||||
n1, err = b.HeaderWriteTo(w)
|
||||
@@ -172,8 +171,8 @@ func WriteTo(w io.Writer, box ...IBox) (n int64, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n1 + n2 != int64(b.Size()) {
|
||||
panic(fmt.Sprintf("write to %s size error, %d != %d", b.Type(), n1 + n2, b.Size()))
|
||||
if n1+n2 != int64(b.Size()) {
|
||||
// panic(fmt.Sprintf("write to %s size error, %d != %d", b.Type(), n1+n2, b.Size()))
|
||||
}
|
||||
n += n1 + n2
|
||||
}
|
||||
@@ -191,9 +190,10 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
|
||||
baseBox.typ = BoxType(tmp[4:])
|
||||
t, exists := registry[baseBox.typ.Uint32I()]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unknown box type: %s", baseBox.typ)
|
||||
io.CopyN(io.Discard, r, int64(baseBox.size-BasicBoxLen))
|
||||
return &baseBox, nil
|
||||
}
|
||||
b := reflect.New(t).Interface().(IBox)
|
||||
b := reflect.New(t.Elem()).Interface().(IBox)
|
||||
var payload []byte
|
||||
if baseBox.size == 1 {
|
||||
if _, err = io.ReadFull(r, tmp[:]); err != nil {
|
||||
@@ -203,8 +203,10 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
|
||||
} else {
|
||||
payload = make([]byte, baseBox.size-BasicBoxLen)
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(r, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boxHeader := b.Header()
|
||||
switch header := boxHeader.(type) {
|
||||
case *BaseBox:
|
||||
@@ -216,6 +218,9 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
|
||||
header.Flags = [3]byte(payload[1:4])
|
||||
box, err = b.Unmarshal(payload[4:])
|
||||
}
|
||||
if err == io.EOF {
|
||||
return box, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -301,12 +306,14 @@ var (
|
||||
TypeEDTS = f("edts")
|
||||
TypeELST = f("elst")
|
||||
TypeMVEX = f("mvex")
|
||||
TypeMEHD = f("mehd")
|
||||
TypeMOOF = f("moof")
|
||||
TypeMFHD = f("mfhd")
|
||||
TypeTRAF = f("traf")
|
||||
TypeTFHD = f("tfhd")
|
||||
TypeTFDT = f("tfdt")
|
||||
TypeTRUN = f("trun")
|
||||
TypeSDTP = f("sdtp")
|
||||
TypeSENC = f("senc")
|
||||
TypeSAIZ = f("saiz")
|
||||
TypeSAIO = f("saio")
|
||||
@@ -335,6 +342,8 @@ var (
|
||||
TypeMETA = f("meta")
|
||||
TypeAUXV = f("auxv")
|
||||
TypeHINT = f("hint")
|
||||
TypeUDTA = f("udta")
|
||||
TypeM7SP = f("m7sp") // Custom box type for M7S StreamPath
|
||||
)
|
||||
|
||||
// aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
|
||||
@@ -370,3 +379,7 @@ type SampleToChunkEntry struct {
|
||||
SamplesPerChunk uint32
|
||||
SampleDescriptionIndex uint32
|
||||
}
|
||||
|
||||
func ConvertUnixTimeToISO14496(unixTime uint64) uint64 {
|
||||
return unixTime + 0x7C25B080
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user