mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
Compare commits
257 Commits
tmp
...
v5.0.4-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad32f6f96e | ||
|
|
56c4ea5907 | ||
|
|
28c71545db | ||
|
|
17faf3f064 | ||
|
|
131af312f1 | ||
|
|
cf3b7dfabe | ||
|
|
584c2e9932 | ||
|
|
a7f04faa23 | ||
|
|
966153f873 | ||
|
|
4391ad2d8d | ||
|
|
747a5a1104 | ||
|
|
97d8de523d | ||
|
|
cad47aec5c | ||
|
|
baf3640b23 | ||
|
|
3d68712ff6 | ||
|
|
f06f43dbe9 | ||
|
|
75efcba311 | ||
|
|
6b58e2a9b5 | ||
|
|
7b6259ed67 | ||
|
|
0d3d86518d | ||
|
|
ac3ad009a7 | ||
|
|
5731c2e8da | ||
|
|
cf6153fa91 | ||
|
|
70e1ea51ac | ||
|
|
8f5a829900 | ||
|
|
10f4fe3fc6 | ||
|
|
3a2901fa5f | ||
|
|
55f5408f64 | ||
|
|
9e45c3eb71 | ||
|
|
01fa1f3ed8 | ||
|
|
830da3aaab | ||
|
|
5a04dc814d | ||
|
|
af5d2bc1f2 | ||
|
|
a3e0c1864e | ||
|
|
33d385d2bf | ||
|
|
29c47a8d08 | ||
|
|
5bf5e7bb20 | ||
|
|
4b74ea5841 | ||
|
|
43710fb017 | ||
|
|
962dda8d08 | ||
|
|
ec56bba75a | ||
|
|
b2b511d755 | ||
|
|
42acf47250 | ||
|
|
6206ee847d | ||
|
|
6cfdc03e4a | ||
|
|
b425b8da1f | ||
|
|
e105243cd5 | ||
|
|
20ec6c55cd | ||
|
|
e478a1972e | ||
|
|
94be02cd79 | ||
|
|
bacda6f5a0 | ||
|
|
61fae4cc97 | ||
|
|
e0752242b2 | ||
|
|
23f2ed39a1 | ||
|
|
0b731e468b | ||
|
|
4fe1472117 | ||
|
|
a8b3a644c3 | ||
|
|
4f0a097dac | ||
|
|
4df3de00af | ||
|
|
9c16905f28 | ||
|
|
0470f78ed7 | ||
|
|
7282f1f44d | ||
|
|
67186cd669 | ||
|
|
09e9761083 | ||
|
|
4acdc19beb | ||
|
|
80e19726d4 | ||
|
|
8ff14931fe | ||
|
|
9c7dc7e628 | ||
|
|
75791fe93f | ||
|
|
cf218215ff | ||
|
|
dbf820b845 | ||
|
|
86b9969954 | ||
|
|
b3143e8c14 | ||
|
|
7f859e6139 | ||
|
|
6eb2941087 | ||
|
|
e8b4cea007 | ||
|
|
3949773e63 | ||
|
|
d67279a404 | ||
|
|
043c62f38f | ||
|
|
acf9f0c677 | ||
|
|
49d1e7c784 | ||
|
|
40bc7d4675 | ||
|
|
5aa8503aeb | ||
|
|
09175f0255 | ||
|
|
dd1a398ca2 | ||
|
|
50cdfad931 | ||
|
|
6df793a8fb | ||
|
|
74c948d0c3 | ||
|
|
80ad1044e3 | ||
|
|
47884b6880 | ||
|
|
a38ddd68aa | ||
|
|
a2bc3d94c1 | ||
|
|
8d6bcc7b1b | ||
|
|
f475419b7b | ||
|
|
b8772f62c1 | ||
|
|
962f2450e5 | ||
|
|
aa683af001 | ||
|
|
e8a1e9e014 | ||
|
|
7a7e461f77 | ||
|
|
4db5d8fc9f | ||
|
|
718d752ea8 | ||
|
|
eb833ef2de | ||
|
|
91ddd03c19 | ||
|
|
fc8ec2ce70 | ||
|
|
ba1d16c91c | ||
|
|
83bb03be72 | ||
|
|
bcc7defa97 | ||
|
|
ecc3947016 | ||
|
|
73e101bc6c | ||
|
|
84558ce434 | ||
|
|
9a808a7b30 | ||
|
|
8586ffcb5a | ||
|
|
55af915a50 | ||
|
|
c2e49f1092 | ||
|
|
793936e88d | ||
|
|
06579ba60c | ||
|
|
917f757f97 | ||
|
|
78ce406609 | ||
|
|
bfed307fa2 | ||
|
|
721d4279d5 | ||
|
|
97a2906377 | ||
|
|
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 |
34
.github/workflows/go.yml
vendored
34
.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
|
||||
@@ -81,16 +80,29 @@ jobs:
|
||||
AWS_S3_BUCKET: monibuca
|
||||
SOURCE_DIR: 'bin'
|
||||
DEST_DIR: ${{ env.dest }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: docker build
|
||||
if: success() && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
tar -zxvf bin/m7s_linux_amd64.tar.gz
|
||||
mv m7s monibuca_linux
|
||||
curl -L https://download.m7s.live/bin/admin.zip -o admin.zip
|
||||
tar -zxvf bin/m7s_v5_linux_amd64.tar.gz
|
||||
mv m7s monibuca_amd64
|
||||
tar -zxvf bin/m7s_v5_linux_arm64.tar.gz
|
||||
mv m7s monibuca_arm64
|
||||
docker login -u langhuihui -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker build -t langhuihui/monibuca:v5 .
|
||||
docker push langhuihui/monibuca:v5
|
||||
- name: docker push
|
||||
if: success() && !contains(env.version, 'beta')
|
||||
if [[ "${{ env.version }}" == *"beta"* ]]; then
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t langhuihui/monibuca:v5 --push .
|
||||
else
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t langhuihui/monibuca:v5 -t langhuihui/monibuca:${{ env.version }} --push .
|
||||
fi
|
||||
- name: docker build lite version
|
||||
if: success() && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag langhuihui/monibuca:v5 langhuihui/monibuca:${{ env.version }}
|
||||
docker push langhuihui/monibuca:${{ env.version }}
|
||||
if [[ "${{ env.version }}" == *"beta"* ]]; then
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -f DockerfileLite -t monibuca/v5:latest --push .
|
||||
else
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -f DockerfileLite -t monibuca/v5:latest -t monibuca/v5:${{ env.version }} --push .
|
||||
fi
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,8 +13,10 @@ bin
|
||||
*.flv
|
||||
pullcf.yaml
|
||||
*.zip
|
||||
!plugin/hls/hls.js.zip
|
||||
__debug*
|
||||
.cursorrules
|
||||
example/default/*
|
||||
!example/default/main.go
|
||||
!example/default/config.yaml
|
||||
shutdown.sh
|
||||
60
Dockerfile
60
Dockerfile
@@ -1,34 +1,34 @@
|
||||
# Compile Stage
|
||||
FROM golang:1.23.2-bullseye AS builder
|
||||
|
||||
|
||||
LABEL stage=gobuilder
|
||||
|
||||
# Env
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GOOS linux
|
||||
ENV GOARCH amd64
|
||||
#ENV GOPROXY https://goproxy.cn,direct
|
||||
ENV HOME /monibuca
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN git clone -b v5 --depth 1 https://github.com/langhuihui/monibuca
|
||||
|
||||
# compile
|
||||
WORKDIR /monibuca
|
||||
RUN go build -tags sqlite -o ./build/monibuca ./example/default/main.go
|
||||
|
||||
RUN cp -r /monibuca/example/default/config.yaml /monibuca/build
|
||||
|
||||
# Running Stage
|
||||
FROM alpine:3.20
|
||||
FROM linuxserver/ffmpeg:latest
|
||||
|
||||
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
|
||||
|
||||
CMD [ "./monibuca", "-c", "/etc/monibuca/config.yaml" ]
|
||||
# Copy the pre-compiled binary from the build context
|
||||
# The GitHub Actions workflow prepares 'monibuca_linux' in the context root
|
||||
|
||||
COPY monibuca_amd64 ./monibuca_amd64
|
||||
COPY monibuca_arm64 ./monibuca_arm64
|
||||
|
||||
COPY admin.zip ./admin.zip
|
||||
|
||||
# Install tcpdump
|
||||
RUN apt-get update && apt-get install -y tcpdump && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the configuration file from the build context
|
||||
COPY example/default/config.yaml /etc/monibuca/config.yaml
|
||||
|
||||
# Export necessary ports
|
||||
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
|
||||
EXPOSE 5060/udp 44944/udp
|
||||
|
||||
RUN if [ "$(uname -m)" = "aarch64" ]; then \
|
||||
mv ./monibuca_arm64 ./monibuca_linux; \
|
||||
rm ./monibuca_amd64; \
|
||||
else \
|
||||
mv ./monibuca_amd64 ./monibuca_linux; \
|
||||
rm ./monibuca_arm64; \
|
||||
fi
|
||||
|
||||
|
||||
ENTRYPOINT [ "./monibuca_linux"]
|
||||
CMD ["-c", "/etc/monibuca/config.yaml"]
|
||||
|
||||
31
DockerfileLite
Normal file
31
DockerfileLite
Normal file
@@ -0,0 +1,31 @@
|
||||
# Running Stage
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /monibuca
|
||||
|
||||
# Copy the pre-compiled binary from the build context
|
||||
# The GitHub Actions workflow prepares 'monibuca_linux' in the context root
|
||||
|
||||
COPY monibuca_amd64 ./monibuca_amd64
|
||||
COPY monibuca_arm64 ./monibuca_arm64
|
||||
|
||||
COPY admin.zip ./admin.zip
|
||||
|
||||
# Copy the configuration file from the build context
|
||||
COPY example/default/config.yaml /etc/monibuca/config.yaml
|
||||
|
||||
# Export necessary ports
|
||||
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
|
||||
EXPOSE 5060/udp 44944/udp
|
||||
|
||||
RUN if [ "$(uname -m)" = "aarch64" ]; then \
|
||||
mv ./monibuca_arm64 ./monibuca_linux; \
|
||||
rm ./monibuca_amd64; \
|
||||
else \
|
||||
mv ./monibuca_amd64 ./monibuca_linux; \
|
||||
rm ./monibuca_arm64; \
|
||||
fi
|
||||
|
||||
|
||||
ENTRYPOINT [ "./monibuca_linux"]
|
||||
CMD ["-c", "/etc/monibuca/config.yaml"]
|
||||
11
README.md
11
README.md
@@ -10,6 +10,9 @@
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://m7s.live">
|
||||
<img src="https://m7s.live/logo+.svg" alt="Logo" width="200">
|
||||
</a>
|
||||
<h1 align="center">Monibuca v5</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -37,6 +40,7 @@
|
||||
<li><a href="#monitoring">Monitoring</a></li>
|
||||
<li><a href="#plugin-development">Plugin Development</a></li>
|
||||
<li><a href="#arch">Architecture</a></li>
|
||||
<li><a href="#third-party-plugins">Third-party Plugins</a></li>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
<li><a href="#license">License</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
@@ -112,6 +116,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>
|
||||
|
||||
@@ -141,6 +146,12 @@ For detailed architecture design documentation, please refer to the [Architectur
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
## Third-party Plugins
|
||||
|
||||
- https://github.com/cuteLittleDevil/m7s-jt1078
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![AGPL License][license-shield]][license-url]
|
||||
[](https://pkg.go.dev/m7s.live/v5)
|
||||
|
||||
<a href="https://hellogithub.com/repository/6d7916d851c2481f87568ffd9f1c21d9" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=6d7916d851c2481f87568ffd9f1c21d9&claim_uid=riBYPOGenUf7kbc&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://monibuca.com">
|
||||
<img src="https://monibuca.com/svg/logo.svg" alt="Logo" width="200">
|
||||
<img src="https://monibuca.com/logo+.svg" alt="Logo" width="200">
|
||||
</a>
|
||||
|
||||
<h1 align="center">Monibuca v5</h1>
|
||||
@@ -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>
|
||||
|
||||
@@ -144,6 +145,10 @@ Monibuca 支持通过插件扩展功能。查看[插件开发指南](./plugin/RE
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
## 第三方插件
|
||||
|
||||
- - https://github.com/cuteLittleDevil/m7s-jt1078
|
||||
|
||||
## 贡献指南
|
||||
|
||||
我们非常欢迎社区贡献,您的参与将使开源社区变得更加精彩!
|
||||
|
||||
139
RELEASE_NOTES_5.0.x_CN.md
Normal file
139
RELEASE_NOTES_5.0.x_CN.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Monibuca v5.0.x Release Notes
|
||||
|
||||
## v5.0.3 (2025-06-27)
|
||||
|
||||
### 🎉 新功能 (New Features)
|
||||
|
||||
#### 录像与流媒体协议增强
|
||||
- **MP4/FLV录像优化**:多项修复和优化录像拉取、分片、写入、格式转换等功能,提升兼容性和稳定性。
|
||||
- **GB28181协议增强**:支持pullproxy代理GB28181流,完善平台配置、子码流播放、单独media port等能力。
|
||||
- **插件与配置系统**:插件初始化、配置加载、数据库适配等增强,支持获取全部配置yaml示例。
|
||||
- **WebRTC/HLS/RTMP协议适配**:WebRTC支持更多编解码器,HLS/RTMP协议兼容性提升。
|
||||
- **crontab计划录像**:定时任务插件支持计划录像,拉流代理支持禁用。
|
||||
|
||||
### 🐛 问题修复 (Bug Fixes)
|
||||
- **录像/流媒体相关**:修复mp4、flv、rtmp、hls等协议的多项bug,包括clone buffer、SQL语法、表结构适配等。
|
||||
- **GB28181/数据库**:修复注册、流订阅、表结构、SQL语法等问题,适配PostgreSQL。
|
||||
- **插件系统**:修复插件初始化、数据库对象赋值、配置加载等问题。
|
||||
|
||||
### 🛠️ 优化改进 (Improvements)
|
||||
- **代码结构重构**:重构mp4、record、插件等系统,提升可维护性。
|
||||
- **文档与示例**:完善文档说明,增加配置和API示例。
|
||||
- **Docker镜像**:优化tcpdump、ffmpeg等工具集成。
|
||||
|
||||
### 👥 贡献者 (Contributors)
|
||||
- langhuihui
|
||||
- pggiroro
|
||||
- banshan
|
||||
|
||||
---
|
||||
|
||||
## v5.0.2 (2025-06-05)
|
||||
|
||||
### 🎉 新功能 (New Features)
|
||||
|
||||
#### 核心功能
|
||||
- **降低延迟** - 禁用了TCP WebRTC的重放保护功能,降低了延迟
|
||||
- **配置系统增强** - 支持更多配置格式(支持配置项中插入`-`、`_`和大写字母),提升配置灵活性
|
||||
- **原始数据检查** - 新增原始数据无帧检查功能,提升数据处理稳定性
|
||||
- **MP4循环读取** - 支持MP4文件循环读取功能(通过配置 pull 配置下的 `loop` 配置)
|
||||
- **S3插件** - 新增S3存储插件,支持云存储集成
|
||||
- **TCP读写缓冲配置** - 新增TCP连接读写缓冲区配置选项(针对高并发下的吞吐能力增强)
|
||||
- **拉流测试模式** - 新增拉流测试模式选项(可以选择拉流时不发布),便于调试和测试
|
||||
- **SEI API格式扩展** - 扩展SEI API支持更多数据格式
|
||||
- **Hook扩展** - 新增更多Hook回调点,增强扩展性
|
||||
- **定时任务插件** - 新增crontab定时任务插件
|
||||
- **服务器抓包** - 新增服务器抓包功能(调用`tcpdump`),支持TCP和UDP协议,API 说明见 [tcpdump](https://api.monibuca.com/api-301117332)
|
||||
|
||||
#### GB28181协议增强
|
||||
- **平台配置支持** - GB28181现在支持从config.yaml中添加平台和平台通道配置
|
||||
- **子码流播放** - 支持GB28181子码流播放功能
|
||||
- **SDP优化** - 优化invite SDP中的mediaip和sipip处理
|
||||
- **本地端口保存** - 修复GB28181本地端口保存到数据库的问题
|
||||
|
||||
#### MP4功能增强
|
||||
- **FLV格式下载** - 支持从MP4录制文件下载FLV格式
|
||||
- **下载功能修复** - 修复MP4下载功能的相关问题
|
||||
- **恢复功能修复** - 修复MP4恢复功能
|
||||
|
||||
### 🐛 问题修复 (Bug Fixes)
|
||||
|
||||
#### 网络通信
|
||||
- **TCP读取阻塞** - 修复TCP读取阻塞问题(增加了读取超时设置)
|
||||
- **RTSP内存泄漏** - 修复RTSP协议的内存泄漏问题
|
||||
- **RTSP音视频标识** - 修复RTSP无音频或视频标识的问题
|
||||
|
||||
#### GB28181协议
|
||||
- **任务管理** - 使用task.Manager解决注册处理器的问题
|
||||
- **计划长度** - 修复plan.length为168的问题
|
||||
- **注册频率** - 修复GB28181注册过快导致启动过多任务的问题
|
||||
- **联系信息** - 修复GB28181获取错误联系信息的问题
|
||||
|
||||
#### RTMP协议
|
||||
- **时间戳处理** - 修复RTMP时间戳开头跳跃问题
|
||||
|
||||
### 🛠️ 优化改进 (Improvements)
|
||||
|
||||
#### Docker支持
|
||||
- **tcpdump工具** - Docker镜像中新增tcpdump网络诊断工具
|
||||
|
||||
#### Linux平台优化
|
||||
- **SIP请求优化** - Linux平台移除SIP请求中的viaheader
|
||||
|
||||
### 👥 贡献者 (Contributors)
|
||||
- langhuihui
|
||||
- pggiroro
|
||||
- banshan
|
||||
|
||||
---
|
||||
|
||||
## v5.0.1 (2025-05-21)
|
||||
|
||||
### 🎉 新功能 (New Features)
|
||||
|
||||
#### WebRTC增强
|
||||
- **H265支持** - 新增WebRTC对H265编码的支持,提升视频质量和压缩效率
|
||||
|
||||
#### GB28181协议增强
|
||||
- **订阅功能扩展** - GB28181模块现在支持订阅报警、移动位置、目录信息
|
||||
- **通知请求** - 支持接收通知请求,增强与设备的交互能力
|
||||
|
||||
#### Docker优化
|
||||
- **FFmpeg集成** - Docker镜像中新增FFmpeg工具,支持更多音视频处理场景
|
||||
- **多架构支持** - 新增Docker多架构构建支持
|
||||
|
||||
### 🐛 问题修复 (Bug Fixes)
|
||||
|
||||
#### Docker相关
|
||||
- **构建问题** - 修复Docker构建过程中的多个问题
|
||||
- **构建优化** - 优化Docker构建流程,提升构建效率
|
||||
|
||||
#### RTMP协议
|
||||
- **时间戳处理** - 修复RTMP第一个chunk类型3需要添加时间戳的问题
|
||||
|
||||
#### GB28181协议
|
||||
- **路径匹配** - 修复GB28181模块中播放流路径的正则表达式匹配问题
|
||||
|
||||
#### MP4处理
|
||||
- **stsz box** - 修复stsz box采样大小的问题
|
||||
- **G711音频** - 修复拉取MP4文件时读取G711音频的问题
|
||||
- **H265解析** - 修复H265 MP4文件解析问题
|
||||
|
||||
### 🛠️ 优化改进 (Improvements)
|
||||
|
||||
#### 代码质量
|
||||
- **错误处理** - 新增maxcount错误处理机制
|
||||
- **文档更新** - 更新README文档和go.mod配置
|
||||
|
||||
#### 构建系统
|
||||
- **ARM架构** - 减少JavaScript代码,优化ARM架构Docker构建
|
||||
- **构建标签** - 移除Docker中不必要的构建标签
|
||||
|
||||
### 📦 其他更新 (Other Updates)
|
||||
- **MCP相关** - 更新Model Context Protocol相关功能
|
||||
- **依赖更新** - 更新项目依赖和模块配置
|
||||
|
||||
### 👥 贡献者 (Contributors)
|
||||
- langhuihui
|
||||
|
||||
---
|
||||
25
alarm.go
Normal file
25
alarm.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package m7s
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AlarmInfo 报警信息实体,用于存储到数据库
|
||||
type AlarmInfo struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"` // 主键,自增ID
|
||||
ServerInfo string `gorm:"type:varchar(255);not null" json:"serverInfo"` // 服务器信息
|
||||
StreamName string `gorm:"type:varchar(255);index" json:"streamName"` // 流名称
|
||||
StreamPath string `gorm:"type:varchar(500)" json:"streamPath"` // 流的streampath
|
||||
AlarmName string `gorm:"type:varchar(255);not null" json:"alarmName"` // 报警名称
|
||||
AlarmDesc string `gorm:"type:varchar(500);not null" json:"alarmDesc"` // 报警描述
|
||||
AlarmType int `gorm:"not null;index" json:"alarmType"` // 报警类型(对应之前定义的常量)
|
||||
IsSent bool `gorm:"default:false" json:"isSent"` // 是否已成功发送
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` // 创建时间,报警时间
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` // 更新时间
|
||||
FilePath string `gorm:"type:varchar(255)" json:"filePath"` // 文件路径
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (AlarmInfo) TableName() string {
|
||||
return "alarm_info"
|
||||
}
|
||||
640
api.go
640
api.go
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -79,7 +78,7 @@ func (s *Server) DisabledPlugins(ctx context.Context, _ *emptypb.Empty) (res *pb
|
||||
|
||||
// /api/stream/annexb/{streamPath}
|
||||
func (s *Server) api_Stream_AnnexB_(rw http.ResponseWriter, r *http.Request) {
|
||||
publisher, ok := s.Streams.Get(r.PathValue("streamPath"))
|
||||
publisher, ok := s.Streams.SafeGet(r.PathValue("streamPath"))
|
||||
if !ok || publisher.VideoTrack.AVTrack == nil {
|
||||
http.Error(rw, pkg.ErrNotFound.Error(), http.StatusNotFound)
|
||||
return
|
||||
@@ -97,22 +96,14 @@ func (s *Server) api_Stream_AnnexB_(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
defer reader.StopRead()
|
||||
if reader.Value.Raw == nil {
|
||||
if err = reader.Value.Demux(publisher.VideoTrack.ICodecCtx); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
var annexb pkg.AnnexB
|
||||
var t pkg.AVTrack
|
||||
|
||||
t.ICodecCtx, t.SequenceFrame, err = annexb.ConvertCtx(publisher.VideoTrack.ICodecCtx)
|
||||
if t.ICodecCtx == nil {
|
||||
http.Error(rw, "unsupported codec", http.StatusInternalServerError)
|
||||
var annexb *pkg.AnnexB
|
||||
var converter = pkg.NewAVFrameConvert[*pkg.AnnexB](publisher.VideoTrack.AVTrack, nil)
|
||||
annexb, err = converter.ConvertFromAVFrame(&reader.Value)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
annexb.Mux(t.ICodecCtx, &reader.Value)
|
||||
_, err = annexb.WriteTo(rw)
|
||||
annexb.WriteTo(rw)
|
||||
}
|
||||
|
||||
func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err error) {
|
||||
@@ -181,32 +172,27 @@ func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err
|
||||
|
||||
func (s *Server) StreamInfo(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.StreamInfoResponse, err error) {
|
||||
var recordings []*pb.RecordingDetail
|
||||
s.Records.Call(func() error {
|
||||
for record := range s.Records.Range {
|
||||
if record.StreamPath == req.StreamPath {
|
||||
recordings = append(recordings, &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
})
|
||||
}
|
||||
s.Records.SafeRange(func(record *RecordJob) bool {
|
||||
if record.StreamPath == req.StreamPath {
|
||||
recordings = append(recordings, &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.RecConf.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
s.Streams.Call(func() error {
|
||||
if pub, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
res, err = s.getStreamInfo(pub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Data.Recording = recordings
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
res, err = s.getStreamInfo(pub)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
})
|
||||
res.Data.Recording = recordings
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,17 +250,15 @@ func (s *Server) RestartTask(ctx context.Context, req *pb.RequestWithId64) (resp
|
||||
}
|
||||
|
||||
func (s *Server) GetRecording(ctx context.Context, req *emptypb.Empty) (resp *pb.RecordingListResponse, err error) {
|
||||
s.Records.Call(func() error {
|
||||
resp = &pb.RecordingListResponse{}
|
||||
for record := range s.Records.Range {
|
||||
resp.Data = append(resp.Data, &pb.Recording{
|
||||
StreamPath: record.StreamPath,
|
||||
StartTime: timestamppb.New(record.StartTime),
|
||||
Type: reflect.TypeOf(record.recorder).String(),
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
resp = &pb.RecordingListResponse{}
|
||||
s.Records.SafeRange(func(record *RecordJob) bool {
|
||||
resp.Data = append(resp.Data, &pb.Recording{
|
||||
StreamPath: record.StreamPath,
|
||||
StartTime: timestamppb.New(record.StartTime),
|
||||
Type: reflect.TypeOf(record.recorder).String(),
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -324,50 +308,47 @@ func (s *Server) GetSubscribers(context.Context, *pb.SubscribersRequest) (res *p
|
||||
return
|
||||
}
|
||||
func (s *Server) AudioTrackSnap(_ context.Context, req *pb.StreamSnapRequest) (res *pb.TrackSnapShotResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if pub, ok := s.Streams.Get(req.StreamPath); ok && pub.HasAudioTrack() {
|
||||
data := &pb.TrackSnapShotData{}
|
||||
if pub.AudioTrack.Allocator != nil {
|
||||
for _, memlist := range pub.AudioTrack.Allocator.GetChildren() {
|
||||
var list []*pb.MemoryBlock
|
||||
for _, block := range memlist.GetBlocks() {
|
||||
list = append(list, &pb.MemoryBlock{
|
||||
S: uint32(block.Start),
|
||||
E: uint32(block.End),
|
||||
})
|
||||
}
|
||||
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
|
||||
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok && pub.HasAudioTrack() {
|
||||
data := &pb.TrackSnapShotData{}
|
||||
if pub.AudioTrack.Allocator != nil {
|
||||
for _, memlist := range pub.AudioTrack.Allocator.GetChildren() {
|
||||
var list []*pb.MemoryBlock
|
||||
for _, block := range memlist.GetBlocks() {
|
||||
list = append(list, &pb.MemoryBlock{
|
||||
S: uint32(block.Start),
|
||||
E: uint32(block.End),
|
||||
})
|
||||
}
|
||||
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
|
||||
}
|
||||
pub.AudioTrack.Ring.Do(func(v *pkg.AVFrame) {
|
||||
if len(v.Wraps) > 0 {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = v.Sequence
|
||||
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(v.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
|
||||
snap.KeyFrame = v.IDR
|
||||
data.RingDataSize += uint32(v.Wraps[0].GetSize())
|
||||
for i, wrap := range v.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
data.Ring = append(data.Ring, &snap)
|
||||
}
|
||||
})
|
||||
res = &pb.TrackSnapShotResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
pub.AudioTrack.Ring.Do(func(v *pkg.AVFrame) {
|
||||
if len(v.Wraps) > 0 {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = v.Sequence
|
||||
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(v.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
|
||||
snap.KeyFrame = v.IDR
|
||||
data.RingDataSize += uint32(v.Wraps[0].GetSize())
|
||||
for i, wrap := range v.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
data.Ring = append(data.Ring, &snap)
|
||||
}
|
||||
})
|
||||
res = &pb.TrackSnapShotResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *Server) api_VideoTrack_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -383,27 +364,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,74 +397,68 @@ 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) {
|
||||
s.Streams.Call(func() error {
|
||||
if pub, ok := s.Streams.Get(req.StreamPath); ok && pub.HasVideoTrack() {
|
||||
data := &pb.TrackSnapShotData{}
|
||||
if pub.VideoTrack.Allocator != nil {
|
||||
for _, memlist := range pub.VideoTrack.Allocator.GetChildren() {
|
||||
var list []*pb.MemoryBlock
|
||||
for _, block := range memlist.GetBlocks() {
|
||||
list = append(list, &pb.MemoryBlock{
|
||||
S: uint32(block.Start),
|
||||
E: uint32(block.End),
|
||||
})
|
||||
}
|
||||
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
|
||||
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok && pub.HasVideoTrack() {
|
||||
data := &pb.TrackSnapShotData{}
|
||||
if pub.VideoTrack.Allocator != nil {
|
||||
for _, memlist := range pub.VideoTrack.Allocator.GetChildren() {
|
||||
var list []*pb.MemoryBlock
|
||||
for _, block := range memlist.GetBlocks() {
|
||||
list = append(list, &pb.MemoryBlock{
|
||||
S: uint32(block.Start),
|
||||
E: uint32(block.End),
|
||||
})
|
||||
}
|
||||
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
|
||||
}
|
||||
pub.VideoTrack.Ring.Do(func(v *pkg.AVFrame) {
|
||||
if len(v.Wraps) > 0 {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = v.Sequence
|
||||
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(v.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
|
||||
snap.KeyFrame = v.IDR
|
||||
data.RingDataSize += uint32(v.Wraps[0].GetSize())
|
||||
for i, wrap := range v.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
data.Ring = append(data.Ring, &snap)
|
||||
}
|
||||
})
|
||||
res = &pb.TrackSnapShotResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
pub.VideoTrack.Ring.Do(func(v *pkg.AVFrame) {
|
||||
if len(v.Wraps) > 0 {
|
||||
var snap pb.TrackSnapShot
|
||||
snap.Sequence = v.Sequence
|
||||
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
|
||||
snap.WriteTime = timestamppb.New(v.WriteTime)
|
||||
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
|
||||
snap.KeyFrame = v.IDR
|
||||
data.RingDataSize += uint32(v.Wraps[0].GetSize())
|
||||
for i, wrap := range v.Wraps {
|
||||
snap.Wrap[i] = &pb.Wrap{
|
||||
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
|
||||
Size: uint32(wrap.GetSize()),
|
||||
Data: wrap.String(),
|
||||
}
|
||||
}
|
||||
data.Ring = append(data.Ring, &snap)
|
||||
}
|
||||
})
|
||||
res = &pb.TrackSnapShotResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -532,86 +504,65 @@ func (s *Server) StopSubscribe(ctx context.Context, req *pb.RequestWithId) (res
|
||||
}
|
||||
|
||||
func (s *Server) PauseStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) ResumeStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) SetStreamSpeed(ctx context.Context, req *pb.SetStreamSpeedRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) SeekStream(ctx context.Context, req *pb.SeekStreamRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
s.Seek(time.Unix(int64(req.TimeStamp), 0))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Seek(time.Unix(int64(req.TimeStamp), 0))
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) StopPublish(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
s.Stop(task.ErrStopByUser)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Stop(task.ErrStopByUser)
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
// /api/stream/list
|
||||
func (s *Server) StreamList(_ context.Context, req *pb.StreamListRequest) (res *pb.StreamListResponse, err error) {
|
||||
recordingMap := make(map[string][]*pb.RecordingDetail)
|
||||
s.Records.Call(func() error {
|
||||
for record := range s.Records.Range {
|
||||
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
for record := range s.Records.SafeRange {
|
||||
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.RecConf.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
}
|
||||
var streams []*pb.StreamInfo
|
||||
for publisher := range s.Streams.SafeRange {
|
||||
info, err := s.getStreamInfo(publisher)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.Streams.Call(func() error {
|
||||
var streams []*pb.StreamInfo
|
||||
for publisher := range s.Streams.Range {
|
||||
info, err := s.getStreamInfo(publisher)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info.Data.Recording = recordingMap[info.Data.Path]
|
||||
streams = append(streams, info.Data)
|
||||
}
|
||||
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
|
||||
return nil
|
||||
})
|
||||
info.Data.Recording = recordingMap[info.Data.Path]
|
||||
streams = append(streams, info.Data)
|
||||
}
|
||||
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -638,24 +589,18 @@ func (s *Server) Api_Summary_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) Api_Stream_Position_SSE(rw http.ResponseWriter, r *http.Request) {
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
util.ReturnFetchValue(func() (t time.Time) {
|
||||
s.Streams.Call(func() error {
|
||||
if pub, ok := s.Streams.Get(streamPath); ok {
|
||||
t = pub.GetPosition()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if pub, ok := s.Streams.SafeGet(streamPath); ok {
|
||||
t = pub.GetPosition()
|
||||
}
|
||||
return
|
||||
}, rw, r)
|
||||
}
|
||||
|
||||
// func (s *Server) Api_Vod_Position(rw http.ResponseWriter, r *http.Request) {
|
||||
// streamPath := r.URL.Query().Get("streamPath")
|
||||
// s.Streams.Call(func() error {
|
||||
// if pub, ok := s.Streams.Get(streamPath); ok {
|
||||
// t = pub.GetPosition()
|
||||
// }
|
||||
// return nil
|
||||
// })
|
||||
// if pub, ok := s.Streams.SafeGet(streamPath); ok {
|
||||
// t = pub.GetPosition()
|
||||
// }
|
||||
// }
|
||||
|
||||
func (s *Server) Summary(context.Context, *emptypb.Empty) (res *pb.SummaryResponse, err error) {
|
||||
@@ -739,7 +684,7 @@ func (s *Server) GetConfigFile(_ context.Context, req *emptypb.Empty) (res *pb.G
|
||||
func (s *Server) UpdateConfigFile(_ context.Context, req *pb.UpdateConfigFileRequest) (res *pb.SuccessResponse, err error) {
|
||||
if s.configFileContent != nil {
|
||||
s.configFileContent = []byte(req.Content)
|
||||
os.WriteFile(filepath.Join(ExecDir, s.conf.(string)), s.configFileContent, 0644)
|
||||
os.WriteFile(s.configFilePath, s.configFileContent, 0644)
|
||||
res = &pb.SuccessResponse{}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
@@ -783,30 +728,7 @@ 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) {
|
||||
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
|
||||
if s.DB == nil {
|
||||
err = pkg.ErrNoDB
|
||||
return
|
||||
@@ -827,9 +749,6 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
} else if req.StreamPath != "" {
|
||||
query = query.Where("stream_path = ?", req.StreamPath)
|
||||
}
|
||||
if req.Mode != "" {
|
||||
query = query.Where("mode = ?", req.Mode)
|
||||
}
|
||||
if req.Type != "" {
|
||||
query = query.Where("type = ?", req.Type)
|
||||
}
|
||||
@@ -848,10 +767,10 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp = &pb.ResponseList{
|
||||
TotalCount: uint32(totalCount),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
resp = &pb.RecordResponseList{
|
||||
Total: uint32(totalCount),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
for _, recordFile := range result {
|
||||
resp.Data = append(resp.Data, &pb.RecordFile{
|
||||
@@ -865,6 +784,69 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) GetEventRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.EventRecordResponseList, err error) {
|
||||
if s.DB == nil {
|
||||
err = pkg.ErrNoDB
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
if req.PageNum == 0 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
|
||||
var totalCount int64 //总条数
|
||||
|
||||
var result []*EventRecordStream
|
||||
query := s.DB.Model(&EventRecordStream{})
|
||||
if strings.Contains(req.StreamPath, "*") {
|
||||
query = query.Where("stream_path like ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
|
||||
} else if req.StreamPath != "" {
|
||||
query = query.Where("stream_path = ?", req.StreamPath)
|
||||
}
|
||||
if req.Type != "" {
|
||||
query = query.Where("type = ?", req.Type)
|
||||
}
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"range": []string{req.Range}, "start": []string{req.Start}, "end": []string{req.End}})
|
||||
if err == nil {
|
||||
if !startTime.IsZero() {
|
||||
query = query.Where("start_time >= ?", startTime)
|
||||
}
|
||||
if !endTime.IsZero() {
|
||||
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
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp = &pb.EventRecordResponseList{
|
||||
Total: uint32(totalCount),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
for _, recordFile := range result {
|
||||
resp.Data = append(resp.Data, &pb.EventRecordFile{
|
||||
Id: uint32(recordFile.ID),
|
||||
StartTime: timestamppb.New(recordFile.StartTime),
|
||||
EndTime: timestamppb.New(recordFile.EndTime),
|
||||
FilePath: recordFile.FilePath,
|
||||
StreamPath: recordFile.StreamPath,
|
||||
EventLevel: recordFile.EventLevel,
|
||||
EventId: recordFile.EventId,
|
||||
EventName: recordFile.EventName,
|
||||
EventDesc: recordFile.EventDesc,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) GetRecordCatalog(ctx context.Context, req *pb.ReqRecordCatalog) (resp *pb.ResponseCatalog, err error) {
|
||||
if s.DB == nil {
|
||||
err = pkg.ErrNoDB
|
||||
@@ -960,3 +942,117 @@ func (s *Server) GetTransformList(ctx context.Context, req *emptypb.Empty) (res
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) GetAlarmList(ctx context.Context, req *pb.AlarmListRequest) (res *pb.AlarmListResponse, err error) {
|
||||
// 初始化响应对象
|
||||
res = &pb.AlarmListResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
|
||||
// 检查数据库连接是否可用
|
||||
if s.DB == nil {
|
||||
res.Code = 500
|
||||
res.Message = "数据库连接不可用"
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
query := s.DB.Model(&AlarmInfo{})
|
||||
|
||||
// 添加时间范围过滤
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{
|
||||
"range": []string{req.Range},
|
||||
"start": []string{req.Start},
|
||||
"end": []string{req.End},
|
||||
})
|
||||
if err == nil {
|
||||
if !startTime.IsZero() {
|
||||
query = query.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if !endTime.IsZero() {
|
||||
query = query.Where("created_at <= ?", endTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加告警类型过滤
|
||||
if req.AlarmType != 0 {
|
||||
query = query.Where("alarm_type = ?", req.AlarmType)
|
||||
}
|
||||
|
||||
// 添加 StreamPath 过滤
|
||||
if req.StreamPath != "" {
|
||||
if strings.Contains(req.StreamPath, "*") {
|
||||
// 支持通配符搜索
|
||||
query = query.Where("stream_path LIKE ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
|
||||
} else {
|
||||
query = query.Where("stream_path = ?", req.StreamPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 StreamName 过滤
|
||||
if req.StreamName != "" {
|
||||
if strings.Contains(req.StreamName, "*") {
|
||||
// 支持通配符搜索
|
||||
query = query.Where("stream_name LIKE ?", strings.ReplaceAll(req.StreamName, "*", "%"))
|
||||
} else {
|
||||
query = query.Where("stream_name = ?", req.StreamName)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总记录数
|
||||
var total int64
|
||||
if err = query.Count(&total).Error; err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "查询告警信息总数失败: " + err.Error()
|
||||
return res, nil
|
||||
}
|
||||
res.Total = int32(total)
|
||||
|
||||
// 如果没有记录,直接返回
|
||||
if total == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 处理分页参数
|
||||
if req.PageNum <= 0 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 查询分页数据
|
||||
var alarmInfoList []AlarmInfo
|
||||
offset := (req.PageNum - 1) * req.PageSize
|
||||
if err = query.Order("created_at DESC").
|
||||
Offset(int(offset)).
|
||||
Limit(int(req.PageSize)).
|
||||
Find(&alarmInfoList).Error; err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "查询告警信息失败: " + err.Error()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 转换为 protobuf 格式
|
||||
res.Data = make([]*pb.AlarmInfo, len(alarmInfoList))
|
||||
for i, alarm := range alarmInfoList {
|
||||
res.Data[i] = &pb.AlarmInfo{
|
||||
Id: uint32(alarm.ID),
|
||||
ServerInfo: alarm.ServerInfo,
|
||||
StreamName: alarm.StreamName,
|
||||
StreamPath: alarm.StreamPath,
|
||||
AlarmDesc: alarm.AlarmDesc,
|
||||
AlarmName: alarm.AlarmName,
|
||||
AlarmType: int32(alarm.AlarmType),
|
||||
IsSent: alarm.IsSent,
|
||||
CreatedAt: timestamppb.New(alarm.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(alarm.UpdatedAt),
|
||||
FilePath: alarm.FilePath,
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
324
api_config.go
Normal file
324
api_config.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package m7s
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func getIndent(line string) int {
|
||||
return len(line) - len(strings.TrimLeft(line, " "))
|
||||
}
|
||||
|
||||
func addCommentsToYAML(yamlData []byte) []byte {
|
||||
lines := strings.Split(string(yamlData), "\n")
|
||||
var result strings.Builder
|
||||
var commentBuffer []string
|
||||
var keyLineBuffer string
|
||||
var keyLineIndent int
|
||||
inMultilineValue := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
indent := getIndent(line)
|
||||
|
||||
if strings.HasPrefix(trimmedLine, "_description:") {
|
||||
description := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_description:"))
|
||||
commentBuffer = append(commentBuffer, "# "+description)
|
||||
} else if strings.HasPrefix(trimmedLine, "_enum:") {
|
||||
enum := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_enum:"))
|
||||
commentBuffer = append(commentBuffer, "# 可选值: "+enum)
|
||||
} else if strings.HasPrefix(trimmedLine, "_value:") {
|
||||
valueStr := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_value:"))
|
||||
if valueStr != "" && valueStr != "{}" && valueStr != "[]" {
|
||||
// Single line value
|
||||
result.WriteString(strings.Repeat(" ", keyLineIndent))
|
||||
result.WriteString(keyLineBuffer)
|
||||
result.WriteString(": ")
|
||||
result.WriteString(valueStr)
|
||||
if len(commentBuffer) > 0 {
|
||||
result.WriteString(" ")
|
||||
for j, c := range commentBuffer {
|
||||
c = strings.TrimSpace(strings.TrimPrefix(c, "#"))
|
||||
result.WriteString("# " + c)
|
||||
if j < len(commentBuffer)-1 {
|
||||
result.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
result.WriteString("\n")
|
||||
} else {
|
||||
// Multi-line value (struct/map)
|
||||
for _, comment := range commentBuffer {
|
||||
result.WriteString(strings.Repeat(" ", keyLineIndent))
|
||||
result.WriteString(comment)
|
||||
result.WriteString("\n")
|
||||
}
|
||||
result.WriteString(strings.Repeat(" ", keyLineIndent))
|
||||
result.WriteString(keyLineBuffer)
|
||||
result.WriteString(":")
|
||||
result.WriteString("\n")
|
||||
inMultilineValue = true
|
||||
}
|
||||
commentBuffer = nil
|
||||
keyLineBuffer = ""
|
||||
keyLineIndent = 0
|
||||
} else if strings.Contains(trimmedLine, ":") {
|
||||
// This is a key line
|
||||
if keyLineBuffer != "" { // flush previous key line
|
||||
result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n")
|
||||
}
|
||||
inMultilineValue = false
|
||||
keyLineBuffer = strings.TrimSuffix(trimmedLine, ":")
|
||||
keyLineIndent = indent
|
||||
} else if inMultilineValue {
|
||||
// These are the lines of a multiline value
|
||||
if trimmedLine != "" {
|
||||
result.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
if keyLineBuffer != "" {
|
||||
result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n")
|
||||
}
|
||||
|
||||
// Final cleanup to remove empty lines and special keys
|
||||
finalOutput := []string{}
|
||||
for _, line := range strings.Split(result.String(), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "_") {
|
||||
continue
|
||||
}
|
||||
finalOutput = append(finalOutput, line)
|
||||
}
|
||||
|
||||
return []byte(strings.Join(finalOutput, "\n"))
|
||||
}
|
||||
|
||||
func (s *Server) api_Config_YAML_All(rw http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
filterName := query.Get("name")
|
||||
shouldMergeCommon := query.Get("common") != "false"
|
||||
|
||||
configSections := []struct {
|
||||
name string
|
||||
data any
|
||||
}{}
|
||||
|
||||
// 1. Get common config if it needs to be merged.
|
||||
var commonConfig map[string]any
|
||||
if shouldMergeCommon {
|
||||
if c, ok := extractStructConfig(reflect.ValueOf(s.Plugin.GetCommonConf())).(map[string]any); ok {
|
||||
commonConfig = c
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process global config.
|
||||
if filterName == "" || filterName == "global" {
|
||||
if globalConf, ok := extractStructConfig(reflect.ValueOf(s.ServerConfig)).(map[string]any); ok {
|
||||
if shouldMergeCommon && commonConfig != nil {
|
||||
mergedConf := make(map[string]any)
|
||||
for k, v := range commonConfig {
|
||||
mergedConf[k] = v
|
||||
}
|
||||
for k, v := range globalConf {
|
||||
mergedConf[k] = v // Global overrides common
|
||||
}
|
||||
configSections = append(configSections, struct {
|
||||
name string
|
||||
data any
|
||||
}{"global", mergedConf})
|
||||
} else {
|
||||
configSections = append(configSections, struct {
|
||||
name string
|
||||
data any
|
||||
}{"global", globalConf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Process plugin configs.
|
||||
for _, meta := range plugins {
|
||||
if filterName != "" && meta.Name != filterName {
|
||||
continue
|
||||
}
|
||||
|
||||
configType := meta.Type
|
||||
if configType.Kind() == reflect.Ptr {
|
||||
configType = configType.Elem()
|
||||
}
|
||||
|
||||
if pluginConf, ok := extractStructConfig(reflect.New(configType)).(map[string]any); ok {
|
||||
pluginConf["enable"] = map[string]any{
|
||||
"_value": true,
|
||||
"_description": "在global配置disableall时能启用特定插件",
|
||||
}
|
||||
if shouldMergeCommon && commonConfig != nil {
|
||||
mergedConf := make(map[string]any)
|
||||
for k, v := range commonConfig {
|
||||
mergedConf[k] = v
|
||||
}
|
||||
for k, v := range pluginConf {
|
||||
mergedConf[k] = v // Plugin overrides common
|
||||
}
|
||||
configSections = append(configSections, struct {
|
||||
name string
|
||||
data any
|
||||
}{meta.Name, mergedConf})
|
||||
} else {
|
||||
configSections = append(configSections, struct {
|
||||
name string
|
||||
data any
|
||||
}{meta.Name, pluginConf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Serialize each section and combine.
|
||||
var yamlParts []string
|
||||
for _, section := range configSections {
|
||||
if section.data == nil {
|
||||
continue
|
||||
}
|
||||
partMap := map[string]any{section.name: section.data}
|
||||
partYAML, err := yaml.Marshal(partMap)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
yamlParts = append(yamlParts, string(partYAML))
|
||||
}
|
||||
|
||||
finalYAML := strings.Join(yamlParts, "")
|
||||
|
||||
rw.Header().Set("Content-Type", "text/yaml; charset=utf-8")
|
||||
rw.Write(addCommentsToYAML([]byte(finalYAML)))
|
||||
}
|
||||
|
||||
func extractStructConfig(v reflect.Value) any {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
m := make(map[string]any)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Type().Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
// Filter out Plugin and UnimplementedApiServer
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Name() == "Plugin" || fieldType.Name() == "UnimplementedApiServer" {
|
||||
continue
|
||||
}
|
||||
yamlTag := field.Tag.Get("yaml")
|
||||
if yamlTag == "-" {
|
||||
continue
|
||||
}
|
||||
fieldName := strings.Split(yamlTag, ",")[0]
|
||||
if fieldName == "" {
|
||||
fieldName = strings.ToLower(field.Name)
|
||||
}
|
||||
m[fieldName] = extractFieldConfig(field, v.Field(i))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func extractFieldConfig(field reflect.StructField, value reflect.Value) any {
|
||||
result := make(map[string]any)
|
||||
description := field.Tag.Get("desc")
|
||||
enum := field.Tag.Get("enum")
|
||||
if description != "" {
|
||||
result["_description"] = description
|
||||
}
|
||||
if enum != "" {
|
||||
result["_enum"] = enum
|
||||
}
|
||||
|
||||
kind := value.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
value = reflect.New(value.Type().Elem())
|
||||
}
|
||||
value = value.Elem()
|
||||
kind = value.Kind()
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Struct:
|
||||
if dur, ok := value.Interface().(time.Duration); ok {
|
||||
result["_value"] = extractDurationConfig(field, dur)
|
||||
} else {
|
||||
result["_value"] = extractStructConfig(value)
|
||||
}
|
||||
case reflect.Map, reflect.Slice:
|
||||
if value.IsNil() {
|
||||
result["_value"] = make(map[string]any)
|
||||
if kind == reflect.Slice {
|
||||
result["_value"] = make([]any, 0)
|
||||
}
|
||||
} else {
|
||||
result["_value"] = value.Interface()
|
||||
}
|
||||
default:
|
||||
result["_value"] = extractBasicTypeConfig(field, value)
|
||||
}
|
||||
|
||||
if description == "" && enum == "" {
|
||||
return result["_value"]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func extractBasicTypeConfig(field reflect.StructField, value reflect.Value) any {
|
||||
if value.IsZero() {
|
||||
if defaultValue := field.Tag.Get("default"); defaultValue != "" {
|
||||
return parseDefaultValue(defaultValue, field.Type)
|
||||
}
|
||||
}
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
func extractDurationConfig(field reflect.StructField, value time.Duration) any {
|
||||
if value == 0 {
|
||||
if defaultValue := field.Tag.Get("default"); defaultValue != "" {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
return value.String()
|
||||
}
|
||||
|
||||
func parseDefaultValue(defaultValue string, t reflect.Type) any {
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
return defaultValue
|
||||
case reflect.Bool:
|
||||
return defaultValue == "true"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if v, err := strconv.ParseInt(defaultValue, 10, 64); err == nil {
|
||||
return v
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if v, err := strconv.ParseUint(defaultValue, 10, 64); err == nil {
|
||||
return v
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if v, err := strconv.ParseFloat(defaultValue, 64); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
BIN
architecture.jpg
BIN
architecture.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 625 KiB After Width: | Height: | Size: 659 KiB |
279
doc/arch/auth.md
Normal file
279
doc/arch/auth.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Stream Authentication Mechanism
|
||||
|
||||
Monibuca V5 provides a comprehensive stream authentication mechanism to control access permissions for publishing and subscribing to streams. The authentication mechanism supports multiple methods, including key-based signature authentication and custom authentication handlers.
|
||||
|
||||
## Authentication Principles
|
||||
|
||||
### 1. Authentication Flow Sequence Diagrams
|
||||
|
||||
#### Publishing Authentication Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Publishing Client
|
||||
participant Plugin as Plugin
|
||||
participant AuthHandler as Auth Handler
|
||||
participant Server as Server
|
||||
|
||||
Client->>Plugin: Publishing Request (streamPath, args)
|
||||
Plugin->>Plugin: Check EnableAuth && Type == PublishTypeServer
|
||||
|
||||
alt Authentication Enabled
|
||||
Plugin->>Plugin: Look for custom auth handler
|
||||
|
||||
alt Custom Handler Exists
|
||||
Plugin->>AuthHandler: onAuthPub(publisher)
|
||||
AuthHandler->>AuthHandler: Execute custom auth logic
|
||||
AuthHandler-->>Plugin: Auth result
|
||||
else Use Key-based Auth
|
||||
Plugin->>Plugin: Check if conf.Key exists
|
||||
alt Key Configured
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: Validate timestamp
|
||||
Plugin->>Plugin: Validate secret length
|
||||
Plugin->>Plugin: Calculate MD5 signature
|
||||
Plugin->>Plugin: Compare signatures
|
||||
Plugin-->>Plugin: Auth result
|
||||
end
|
||||
end
|
||||
|
||||
alt Auth Failed
|
||||
Plugin-->>Client: Auth failed, reject publishing
|
||||
else Auth Success
|
||||
Plugin->>Server: Create Publisher and add to stream management
|
||||
Server-->>Plugin: Publishing successful
|
||||
Plugin-->>Client: Publishing established successfully
|
||||
end
|
||||
else Auth Disabled
|
||||
Plugin->>Server: Create Publisher directly
|
||||
Server-->>Plugin: Publishing successful
|
||||
Plugin-->>Client: Publishing established successfully
|
||||
end
|
||||
```
|
||||
|
||||
#### Subscribing Authentication Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Subscribing Client
|
||||
participant Plugin as Plugin
|
||||
participant AuthHandler as Auth Handler
|
||||
participant Server as Server
|
||||
|
||||
Client->>Plugin: Subscribing Request (streamPath, args)
|
||||
Plugin->>Plugin: Check EnableAuth && Type == SubscribeTypeServer
|
||||
|
||||
alt Authentication Enabled
|
||||
Plugin->>Plugin: Look for custom auth handler
|
||||
|
||||
alt Custom Handler Exists
|
||||
Plugin->>AuthHandler: onAuthSub(subscriber)
|
||||
AuthHandler->>AuthHandler: Execute custom auth logic
|
||||
AuthHandler-->>Plugin: Auth result
|
||||
else Use Key-based Auth
|
||||
Plugin->>Plugin: Check if conf.Key exists
|
||||
alt Key Configured
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: Validate timestamp
|
||||
Plugin->>Plugin: Validate secret length
|
||||
Plugin->>Plugin: Calculate MD5 signature
|
||||
Plugin->>Plugin: Compare signatures
|
||||
Plugin-->>Plugin: Auth result
|
||||
end
|
||||
end
|
||||
|
||||
alt Auth Failed
|
||||
Plugin-->>Client: Auth failed, reject subscribing
|
||||
else Auth Success
|
||||
Plugin->>Server: Create Subscriber and wait for Publisher
|
||||
Server->>Server: Wait for stream publishing and track ready
|
||||
Server-->>Plugin: Subscribing ready
|
||||
Plugin-->>Client: Start streaming data transmission
|
||||
end
|
||||
else Auth Disabled
|
||||
Plugin->>Server: Create Subscriber directly
|
||||
Server-->>Plugin: Subscribing successful
|
||||
Plugin-->>Client: Start streaming data transmission
|
||||
end
|
||||
```
|
||||
|
||||
### 2. Authentication Trigger Points
|
||||
|
||||
Authentication is triggered in the following two scenarios:
|
||||
|
||||
- **Publishing Authentication**: Triggered when there's a publishing request in the `PublishWithConfig` method
|
||||
- **Subscribing Authentication**: Triggered when there's a subscribing request in the `SubscribeWithConfig` method
|
||||
|
||||
### 3. Authentication Condition Checks
|
||||
|
||||
Authentication is only executed when the following conditions are met simultaneously:
|
||||
|
||||
```go
|
||||
if p.config.EnableAuth && publisher.Type == PublishTypeServer
|
||||
```
|
||||
|
||||
- `EnableAuth`: Authentication is enabled in the plugin configuration
|
||||
- `Type == PublishTypeServer/SubscribeTypeServer`: Only authenticate server-type publishing/subscribing
|
||||
|
||||
### 4. Authentication Method Priority
|
||||
|
||||
The system executes authentication in the following priority order:
|
||||
|
||||
1. **Custom Authentication Handler** (Highest priority)
|
||||
2. **Key-based Signature Authentication**
|
||||
3. **No Authentication** (Default pass)
|
||||
|
||||
## Custom Authentication Handlers
|
||||
|
||||
### Publishing Authentication Handler
|
||||
|
||||
```go
|
||||
onAuthPub := p.Meta.OnAuthPub
|
||||
if onAuthPub == nil {
|
||||
onAuthPub = p.Server.Meta.OnAuthPub
|
||||
}
|
||||
if onAuthPub != nil {
|
||||
if err = onAuthPub(publisher).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Authentication handler lookup order:
|
||||
1. Plugin-level authentication handler `p.Meta.OnAuthPub`
|
||||
2. Server-level authentication handler `p.Server.Meta.OnAuthPub`
|
||||
|
||||
### Subscribing Authentication Handler
|
||||
|
||||
```go
|
||||
onAuthSub := p.Meta.OnAuthSub
|
||||
if onAuthSub == nil {
|
||||
onAuthSub = p.Server.Meta.OnAuthSub
|
||||
}
|
||||
if onAuthSub != nil {
|
||||
if err = onAuthSub(subscriber).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key-based Signature Authentication
|
||||
|
||||
When there's no custom authentication handler, if a Key is configured, the system will use MD5-based signature authentication mechanism.
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
```go
|
||||
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
|
||||
// 1. Validate expiration time
|
||||
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
|
||||
return fmt.Errorf("auth failed expired")
|
||||
}
|
||||
|
||||
// 2. Validate secret length
|
||||
if len(secret) != 32 {
|
||||
return fmt.Errorf("auth failed secret length must be 32")
|
||||
}
|
||||
|
||||
// 3. Calculate the true secret
|
||||
trueSecret := md5.Sum([]byte(key + streamPath + expire))
|
||||
|
||||
// 4. Compare secrets
|
||||
if secret == hex.EncodeToString(trueSecret[:]) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("auth failed invalid secret")
|
||||
}
|
||||
```
|
||||
|
||||
### Signature Calculation Steps
|
||||
|
||||
1. **Construct signature string**: `key + streamPath + expire`
|
||||
2. **MD5 encryption**: Perform MD5 hash on the signature string
|
||||
3. **Hexadecimal encoding**: Convert MD5 result to 32-character hexadecimal string
|
||||
4. **Verify signature**: Compare calculation result with client-provided secret
|
||||
|
||||
### Parameter Description
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-----------|------|-------------|---------|
|
||||
| key | string | Secret key set in configuration file | "mySecretKey" |
|
||||
| streamPath | string | Stream path | "live/test" |
|
||||
| expire | string | Expiration timestamp (hexadecimal) | "64a1b2c3" |
|
||||
| secret | string | Client-calculated signature (32-char hex) | "5d41402abc4b2a76b9719d911017c592" |
|
||||
|
||||
### Timestamp Handling
|
||||
|
||||
- Expiration time uses hexadecimal Unix timestamp
|
||||
- System validates if current time exceeds expiration time
|
||||
- Timestamp parsing failure or expiration will cause authentication failure
|
||||
|
||||
## API Key Generation
|
||||
|
||||
The system also provides API interfaces for key generation, supporting authentication needs for admin dashboard:
|
||||
|
||||
```go
|
||||
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// JWT Token validation
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
_, err := p.Server.ValidateToken(tokenString)
|
||||
|
||||
// Generate publishing or subscribing key
|
||||
streamPath := r.PathValue("streamPath")
|
||||
t := r.PathValue("type")
|
||||
expire := r.URL.Query().Get("expire")
|
||||
|
||||
if t == "publish" {
|
||||
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
} else if t == "subscribe" {
|
||||
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Enable Authentication
|
||||
|
||||
```yaml
|
||||
# Plugin configuration
|
||||
rtmp:
|
||||
enableAuth: true
|
||||
publish:
|
||||
key: "your-publish-key"
|
||||
subscribe:
|
||||
key: "your-subscribe-key"
|
||||
```
|
||||
|
||||
### Publishing URL Example
|
||||
|
||||
```
|
||||
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
|
||||
```
|
||||
|
||||
### Subscribing URL Example
|
||||
|
||||
```
|
||||
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Key Protection**: Keys in configuration files should be properly secured to prevent leakage
|
||||
2. **Time Window**: Set reasonable expiration times to balance security and usability
|
||||
3. **HTTPS Transport**: Use HTTPS for transmitting authentication parameters in production
|
||||
4. **Logging**: Authentication failures are logged as warnings for security auditing
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common causes of authentication failure:
|
||||
|
||||
- `auth failed expired`: Timestamp expired or format error
|
||||
- `auth failed secret length must be 32`: Incorrect secret length
|
||||
- `auth failed invalid secret`: Signature verification failed
|
||||
- `invalid token`: JWT verification failed during API key generation
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
### Plugin Development
|
||||
|
||||
[plugin/README.md](../plugin/README.md)
|
||||
[plugin/README.md](../../plugin/README.md)
|
||||
|
||||
## Task System
|
||||
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,279 @@
|
||||
# 流鉴权机制
|
||||
|
||||
Monibuca V5 提供了完善的流鉴权机制,用于控制推流和拉流的访问权限。鉴权机制支持多种方式,包括基于密钥的签名鉴权和自定义鉴权处理器。
|
||||
|
||||
## 鉴权原理
|
||||
|
||||
### 1. 鉴权流程时序图
|
||||
|
||||
#### 推流鉴权时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 推流客户端
|
||||
participant Plugin as 插件
|
||||
participant AuthHandler as 鉴权处理器
|
||||
participant Server as 服务器
|
||||
|
||||
Client->>Plugin: 推流请求 (streamPath, args)
|
||||
Plugin->>Plugin: 检查 EnableAuth && Type == PublishTypeServer
|
||||
|
||||
alt 启用鉴权
|
||||
Plugin->>Plugin: 查找自定义鉴权处理器
|
||||
|
||||
alt 存在自定义处理器
|
||||
Plugin->>AuthHandler: onAuthPub(publisher)
|
||||
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
|
||||
AuthHandler-->>Plugin: 鉴权结果
|
||||
else 使用密钥鉴权
|
||||
Plugin->>Plugin: 检查 conf.Key 是否存在
|
||||
alt 配置了Key
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: 验证时间戳
|
||||
Plugin->>Plugin: 验证secret长度
|
||||
Plugin->>Plugin: 计算MD5签名
|
||||
Plugin->>Plugin: 比较签名
|
||||
Plugin-->>Plugin: 鉴权结果
|
||||
end
|
||||
end
|
||||
|
||||
alt 鉴权失败
|
||||
Plugin-->>Client: 鉴权失败,拒绝推流
|
||||
else 鉴权成功
|
||||
Plugin->>Server: 创建Publisher并添加到流管理
|
||||
Server-->>Plugin: 推流成功
|
||||
Plugin-->>Client: 推流建立成功
|
||||
end
|
||||
else 未启用鉴权
|
||||
Plugin->>Server: 直接创建Publisher
|
||||
Server-->>Plugin: 推流成功
|
||||
Plugin-->>Client: 推流建立成功
|
||||
end
|
||||
```
|
||||
|
||||
#### 拉流鉴权时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 拉流客户端
|
||||
participant Plugin as 插件
|
||||
participant AuthHandler as 鉴权处理器
|
||||
participant Server as 服务器
|
||||
|
||||
Client->>Plugin: 拉流请求 (streamPath, args)
|
||||
Plugin->>Plugin: 检查 EnableAuth && Type == SubscribeTypeServer
|
||||
|
||||
alt 启用鉴权
|
||||
Plugin->>Plugin: 查找自定义鉴权处理器
|
||||
|
||||
alt 存在自定义处理器
|
||||
Plugin->>AuthHandler: onAuthSub(subscriber)
|
||||
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
|
||||
AuthHandler-->>Plugin: 鉴权结果
|
||||
else 使用密钥鉴权
|
||||
Plugin->>Plugin: 检查 conf.Key 是否存在
|
||||
alt 配置了Key
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: 验证时间戳
|
||||
Plugin->>Plugin: 验证secret长度
|
||||
Plugin->>Plugin: 计算MD5签名
|
||||
Plugin->>Plugin: 比较签名
|
||||
Plugin-->>Plugin: 鉴权结果
|
||||
end
|
||||
end
|
||||
|
||||
alt 鉴权失败
|
||||
Plugin-->>Client: 鉴权失败,拒绝拉流
|
||||
else 鉴权成功
|
||||
Plugin->>Server: 创建Subscriber并等待Publisher
|
||||
Server->>Server: 等待流发布和轨道就绪
|
||||
Server-->>Plugin: 拉流准备就绪
|
||||
Plugin-->>Client: 开始传输流数据
|
||||
end
|
||||
else 未启用鉴权
|
||||
Plugin->>Server: 直接创建Subscriber
|
||||
Server-->>Plugin: 拉流成功
|
||||
Plugin-->>Client: 开始传输流数据
|
||||
end
|
||||
```
|
||||
|
||||
### 2. 鉴权触发时机
|
||||
|
||||
鉴权在以下两种情况下触发:
|
||||
|
||||
- **推流鉴权**:当有推流请求时,在`PublishWithConfig`方法中触发
|
||||
- **拉流鉴权**:当有拉流请求时,在`SubscribeWithConfig`方法中触发
|
||||
|
||||
### 3. 鉴权条件判断
|
||||
|
||||
鉴权只在以下条件同时满足时才会执行:
|
||||
|
||||
```go
|
||||
if p.config.EnableAuth && publisher.Type == PublishTypeServer
|
||||
```
|
||||
|
||||
- `EnableAuth`:插件配置中启用了鉴权
|
||||
- `Type == PublishTypeServer/SubscribeTypeServer`:只对服务端类型的推流/拉流进行鉴权
|
||||
|
||||
### 4. 鉴权方式优先级
|
||||
|
||||
系统按以下优先级执行鉴权:
|
||||
|
||||
1. **自定义鉴权处理器**(最高优先级)
|
||||
2. **基于密钥的签名鉴权**
|
||||
3. **无鉴权**(默认通过)
|
||||
|
||||
## 自定义鉴权处理器
|
||||
|
||||
### 推流鉴权处理器
|
||||
|
||||
```go
|
||||
onAuthPub := p.Meta.OnAuthPub
|
||||
if onAuthPub == nil {
|
||||
onAuthPub = p.Server.Meta.OnAuthPub
|
||||
}
|
||||
if onAuthPub != nil {
|
||||
if err = onAuthPub(publisher).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
鉴权处理器查找顺序:
|
||||
1. 插件级别的鉴权处理器 `p.Meta.OnAuthPub`
|
||||
2. 服务器级别的鉴权处理器 `p.Server.Meta.OnAuthPub`
|
||||
|
||||
### 拉流鉴权处理器
|
||||
|
||||
```go
|
||||
onAuthSub := p.Meta.OnAuthSub
|
||||
if onAuthSub == nil {
|
||||
onAuthSub = p.Server.Meta.OnAuthSub
|
||||
}
|
||||
if onAuthSub != nil {
|
||||
if err = onAuthSub(subscriber).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基于密钥的签名鉴权
|
||||
|
||||
当没有自定义鉴权处理器时,如果配置了Key,系统将使用基于MD5的签名鉴权机制。
|
||||
|
||||
### 鉴权算法
|
||||
|
||||
```go
|
||||
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
|
||||
// 1. 验证过期时间
|
||||
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
|
||||
return fmt.Errorf("auth failed expired")
|
||||
}
|
||||
|
||||
// 2. 验证secret长度
|
||||
if len(secret) != 32 {
|
||||
return fmt.Errorf("auth failed secret length must be 32")
|
||||
}
|
||||
|
||||
// 3. 计算真实的secret
|
||||
trueSecret := md5.Sum([]byte(key + streamPath + expire))
|
||||
|
||||
// 4. 比较secret
|
||||
if secret == hex.EncodeToString(trueSecret[:]) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("auth failed invalid secret")
|
||||
}
|
||||
```
|
||||
|
||||
### 签名计算步骤
|
||||
|
||||
1. **构造签名字符串**:`key + streamPath + expire`
|
||||
2. **MD5加密**:对签名字符串进行MD5哈希
|
||||
3. **十六进制编码**:将MD5结果转换为32位十六进制字符串
|
||||
4. **验证签名**:比较计算结果与客户端提供的secret
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| key | string | 密钥,在配置文件中设置 | "mySecretKey" |
|
||||
| streamPath | string | 流路径 | "live/test" |
|
||||
| expire | string | 过期时间戳(16进制) | "64a1b2c3" |
|
||||
| secret | string | 客户端计算的签名(32位十六进制) | "5d41402abc4b2a76b9719d911017c592" |
|
||||
|
||||
### 时间戳处理
|
||||
|
||||
- 过期时间使用16进制Unix时间戳
|
||||
- 系统会验证当前时间是否超过过期时间
|
||||
- 时间戳解析失败或已过期都会导致鉴权失败
|
||||
|
||||
## API密钥生成
|
||||
|
||||
系统还提供了API接口用于生成密钥,支持管理后台的鉴权需求:
|
||||
|
||||
```go
|
||||
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// JWT Token验证
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
_, err := p.Server.ValidateToken(tokenString)
|
||||
|
||||
// 生成推流或拉流密钥
|
||||
streamPath := r.PathValue("streamPath")
|
||||
t := r.PathValue("type")
|
||||
expire := r.URL.Query().Get("expire")
|
||||
|
||||
if t == "publish" {
|
||||
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
} else if t == "subscribe" {
|
||||
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 启用鉴权
|
||||
|
||||
```yaml
|
||||
# 插件配置
|
||||
rtmp:
|
||||
enableAuth: true
|
||||
publish:
|
||||
key: "your-publish-key"
|
||||
subscribe:
|
||||
key: "your-subscribe-key"
|
||||
```
|
||||
|
||||
### 推流URL示例
|
||||
|
||||
```
|
||||
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
|
||||
```
|
||||
|
||||
### 拉流URL示例
|
||||
|
||||
```
|
||||
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **密钥保护**:配置文件中的key应当妥善保管,避免泄露
|
||||
2. **时间窗口**:合理设置过期时间,平衡安全性和可用性
|
||||
3. **HTTPS传输**:生产环境建议使用HTTPS传输鉴权参数
|
||||
4. **日志记录**:鉴权失败会记录警告日志,便于安全审计
|
||||
|
||||
## 错误处理
|
||||
|
||||
鉴权失败的常见原因:
|
||||
|
||||
- `auth failed expired`:时间戳已过期或格式错误
|
||||
- `auth failed secret length must be 32`:secret长度不正确
|
||||
- `auth failed invalid secret`:签名验证失败
|
||||
- `invalid token`:API密钥生成时JWT验证失败
|
||||
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项目也将持续探索和优化这一技术,为用户提供更优质的流媒体服务。
|
||||
@@ -7,4 +7,9 @@ rtsp:
|
||||
mp4:
|
||||
enable: true
|
||||
pull:
|
||||
live/test: /Users/dexter/Movies/test.mp4
|
||||
live/test: /Users/dexter/Movies/test.mp4
|
||||
rtmp:
|
||||
enable: true
|
||||
|
||||
debug:
|
||||
enable: true
|
||||
|
||||
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 # 查询截图时允许的最大时间差(秒)
|
||||
13
example/8081/default.yaml
Normal file
13
example/8081/default.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
global:
|
||||
# loglevel: debug
|
||||
http:
|
||||
listenaddr: :8081
|
||||
listenaddrtls: :8555
|
||||
tcp:
|
||||
listenaddr: :50052
|
||||
rtsp:
|
||||
enable: false
|
||||
rtmp:
|
||||
tcp: :1936
|
||||
webrtc:
|
||||
enable: false
|
||||
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,35 +4,56 @@ global:
|
||||
loglevel: debug
|
||||
admin:
|
||||
enablelogin: false
|
||||
subscribe:
|
||||
subaudio: 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
|
||||
enable: false # 是否启用GB28181协议
|
||||
autoinvite: false #建议使用false,开启后会自动邀请设备推流
|
||||
mediaip: 192.168.1.21 #流媒体收流IP,外网情况下使用公网IP,内网情况下使用网卡IP,不要用127.0.0.1
|
||||
sipip: 192.168.1.21 #SIP通讯IP,不管公网还是内网都使用本机网卡IP,不要用127.0.0.1
|
||||
sip:
|
||||
listenaddr:
|
||||
- udp::5060
|
||||
# pull:
|
||||
# live/test: dump/34020000001320000001
|
||||
onsub:
|
||||
pull:
|
||||
.* : $0
|
||||
^\d{20}/\d{20}$: $0
|
||||
^gb_\d+/(.+)$: $1
|
||||
# .* : $0
|
||||
platforms:
|
||||
- enable: false #是否启用平台
|
||||
name: "测试平台" #平台名称
|
||||
servergbid: "34020000002000000002" #上级平台GBID
|
||||
servergbdomain: "3402000000" #上级平台GB域
|
||||
serverip: 192.168.1.106 #上级平台IP
|
||||
serverport: 5061 #上级平台端口
|
||||
devicegbid: "34020000002000000001" #本平台设备GBID
|
||||
deviceip: 192.168.1.106 #本平台设备IP
|
||||
deviceport: 5060 #本平台设备端口
|
||||
username: "34020000002000000001" #SIP账号
|
||||
password: "123456" #SIP密码
|
||||
expires: 3600 #注册有效期,单位秒
|
||||
keeptimeout: 60 #注册保持超时时间,单位秒
|
||||
civilCode: "340200" #行政区划代码
|
||||
manufacturer: "Monibuca" #设备制造商
|
||||
model: "GB28181" #设备型号
|
||||
address: "江苏南京" #设备地址
|
||||
register_way: 1
|
||||
platformchannels:
|
||||
- platformservergbid: "34020000002000000002" #上级平台GBID
|
||||
channeldbid: "34020000001110000003_34020000001320000005" #通道DBID,格式为设备ID_通道ID
|
||||
mp4:
|
||||
# enable: false
|
||||
publish:
|
||||
delayclosetimeout: 3s
|
||||
# publish:
|
||||
# delayclosetimeout: 3s
|
||||
# onpub:
|
||||
# record:
|
||||
# ^live/.+:
|
||||
# fragment: 10s
|
||||
# filepath: record/$0
|
||||
# type: fmp4
|
||||
# type: fmp4
|
||||
# pull:
|
||||
# live/test: /Users/dexter/Movies/1744963190.mp4
|
||||
onsub:
|
||||
pull:
|
||||
^vod_mp4_\d+/(.+)$: $1
|
||||
@@ -72,26 +93,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() {
|
||||
|
||||
53
go.mod
53
go.mod
@@ -1,27 +1,34 @@
|
||||
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/aws/aws-sdk-go v1.55.7
|
||||
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/mark3labs/mcp-go v0.27.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,32 +37,34 @@ 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/qpack v0.5.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/valyala/fasthttp v1.61.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
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.11
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
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 +77,17 @@ 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/jmespath/go-jmespath v0.4.0 // 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,37 +95,28 @@ 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/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/spf13/cast v1.7.1 // 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
|
||||
@@ -130,9 +128,10 @@ require (
|
||||
github.com/valyala/quicktemplate v1.8.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // 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 +148,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
|
||||
)
|
||||
|
||||
250
go.sum
250
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,12 +17,16 @@ 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=
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI=
|
||||
github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
|
||||
@@ -69,11 +71,11 @@ 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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 +86,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 +99,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 +113,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 +121,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=
|
||||
@@ -156,10 +141,14 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
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=
|
||||
@@ -179,6 +168,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/marcboeker/go-duckdb v1.0.5 h1:zIfyrCAJfY9FmXWOZ6jE3DkmWpwK4rlY12zqf9LD2mU=
|
||||
github.com/marcboeker/go-duckdb v1.0.5/go.mod h1:wm91jO2GNKa6iO9NTcjXIRsW+/ykPoJbQcHSXhdAl28=
|
||||
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
|
||||
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -205,17 +196,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 +206,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 +256,10 @@ 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/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/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.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 +275,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=
|
||||
@@ -348,6 +283,8 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@@ -355,16 +292,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 +309,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 +327,22 @@ 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
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 +350,69 @@ 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=
|
||||
@@ -589,8 +423,8 @@ gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkD
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
|
||||
@@ -11,7 +11,7 @@ builds:
|
||||
tags:
|
||||
- sqlite
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Tag}}
|
||||
- -s -w -X m7s.live/v5.Version={{.Tag}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
|
||||
355
pb/auth.pb.go
355
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.6
|
||||
// protoc v6.31.1
|
||||
// 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,79 +440,54 @@ func (x *UserInfoResponse) GetData() *UserInfo {
|
||||
|
||||
var File_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_auth_proto_rawDesc = []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,
|
||||
0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61,
|
||||
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61,
|
||||
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4e, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x53,
|
||||
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x28, 0x0a, 0x08,
|
||||
0x75, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c,
|
||||
0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x75, 0x73,
|
||||
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x63, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x24, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x53, 0x75,
|
||||
0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x25, 0x0a, 0x0d, 0x4c,
|
||||
0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x22, 0x3e, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 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, 0x22, 0x27, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x45, 0x0a, 0x08, 0x55,
|
||||
0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
|
||||
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73,
|
||||
0x41, 0x74, 0x22, 0x62, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x20, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f,
|
||||
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xf4, 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
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,
|
||||
}
|
||||
const file_auth_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"auth.proto\x12\x02pb\x1a\x1cgoogle/api/annotations.proto\"F\n" +
|
||||
"\fLoginRequest\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
|
||||
"\bpassword\x18\x02 \x01(\tR\bpassword\"N\n" +
|
||||
"\fLoginSuccess\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\x12(\n" +
|
||||
"\buserInfo\x18\x02 \x01(\v2\f.pb.UserInfoR\buserInfo\"c\n" +
|
||||
"\rLoginResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12$\n" +
|
||||
"\x04data\x18\x03 \x01(\v2\x10.pb.LoginSuccessR\x04data\"%\n" +
|
||||
"\rLogoutRequest\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\">\n" +
|
||||
"\x0eLogoutResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\"'\n" +
|
||||
"\x0fUserInfoRequest\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\"E\n" +
|
||||
"\bUserInfo\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" +
|
||||
"\n" +
|
||||
"expires_at\x18\x02 \x01(\x03R\texpiresAt\"b\n" +
|
||||
"\x10UserInfoResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12 \n" +
|
||||
"\x04data\x18\x03 \x01(\v2\f.pb.UserInfoR\x04data2\xf4\x01\n" +
|
||||
"\x04Auth\x12H\n" +
|
||||
"\x05Login\x12\x10.pb.LoginRequest\x1a\x11.pb.LoginResponse\"\x1a\x82\xd3\xe4\x93\x02\x14:\x01*\"\x0f/api/auth/login\x12L\n" +
|
||||
"\x06Logout\x12\x11.pb.LogoutRequest\x1a\x12.pb.LogoutResponse\"\x1b\x82\xd3\xe4\x93\x02\x15:\x01*\"\x10/api/auth/logout\x12T\n" +
|
||||
"\vGetUserInfo\x12\x13.pb.UserInfoRequest\x1a\x14.pb.UserInfoResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/auth/userinfoB\x10Z\x0em7s.live/v5/pbb\x06proto3"
|
||||
|
||||
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 +519,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 +534,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
|
||||
}
|
||||
|
||||
168
pb/auth.pb.gw.go
168
pb/auth.pb.gw.go
@@ -10,6 +10,7 @@ package pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -24,116 +25,118 @@ import (
|
||||
)
|
||||
|
||||
// Suppress "imported and not used" errors
|
||||
var _ codes.Code
|
||||
var _ io.Reader
|
||||
var _ status.Status
|
||||
var _ = runtime.String
|
||||
var _ = utilities.NewDoubleArray
|
||||
var _ = metadata.Join
|
||||
var (
|
||||
_ codes.Code
|
||||
_ io.Reader
|
||||
_ status.Status
|
||||
_ = errors.New
|
||||
_ = runtime.String
|
||||
_ = utilities.NewDoubleArray
|
||||
_ = metadata.Join
|
||||
)
|
||||
|
||||
func request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq LoginRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
var (
|
||||
protoReq LoginRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq LoginRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
var (
|
||||
protoReq LoginRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.Login(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Auth_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq LogoutRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
var (
|
||||
protoReq LogoutRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
msg, err := client.Logout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Auth_Logout_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq LogoutRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
var (
|
||||
protoReq LogoutRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.Logout(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_Auth_GetUserInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
var filter_Auth_GetUserInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq UserInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
protoReq UserInfoRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_GetUserInfo_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetUserInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq UserInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
protoReq UserInfoRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_GetUserInfo_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.GetUserInfo(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterAuthHandlerServer registers the http handlers for service Auth to "mux".
|
||||
// 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) {
|
||||
mux.Handle(http.MethodPost, pattern_Auth_Login_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, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -145,20 +148,15 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Auth_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle(http.MethodPost, pattern_Auth_Logout_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, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -170,20 +168,15 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Auth_GetUserInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle(http.MethodGet, pattern_Auth_GetUserInfo_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, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -195,9 +188,7 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_GetUserInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -206,25 +197,24 @@ 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)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterAuthHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
@@ -238,16 +228,13 @@ 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) {
|
||||
mux.Handle(http.MethodPost, pattern_Auth_Login_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, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -258,18 +245,13 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Auth_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle(http.MethodPost, pattern_Auth_Logout_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, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -280,18 +262,13 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Auth_GetUserInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle(http.MethodGet, pattern_Auth_GetUserInfo_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, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -302,26 +279,19 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Auth_GetUserInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, ""))
|
||||
|
||||
pattern_Auth_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "logout"}, ""))
|
||||
|
||||
pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, ""))
|
||||
pattern_Auth_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "logout"}, ""))
|
||||
pattern_Auth_GetUserInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "userinfo"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_Auth_Login_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Auth_Logout_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Auth_Login_0 = runtime.ForwardResponseMessage
|
||||
forward_Auth_Logout_0 = runtime.ForwardResponseMessage
|
||||
forward_Auth_GetUserInfo_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
@@ -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 v6.31.1
|
||||
// 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))
|
||||
|
||||
5066
pb/global.pb.go
5066
pb/global.pb.go
File diff suppressed because it is too large
Load Diff
2786
pb/global.pb.gw.go
2786
pb/global.pb.gw.go
File diff suppressed because it is too large
Load Diff
118
pb/global.proto
118
pb/global.proto
@@ -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,12 +176,12 @@ service api {
|
||||
post: "/api/proxy/pull/remove/{id}"
|
||||
body: "*"
|
||||
additional_bindings {
|
||||
post: "/api/device/add/{id}"
|
||||
post: "/api/device/remove/{id}"
|
||||
body: "*"
|
||||
}
|
||||
};
|
||||
}
|
||||
rpc UpdatePullProxy (PullProxyInfo) returns (SuccessResponse) {
|
||||
rpc UpdatePullProxy (UpdatePullProxyRequest) returns (SuccessResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/proxy/pull/update"
|
||||
body: "*"
|
||||
@@ -213,7 +208,7 @@ service api {
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc UpdatePushProxy (PushProxyInfo) returns (SuccessResponse) {
|
||||
rpc UpdatePushProxy (UpdatePushProxyRequest) returns (SuccessResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/proxy/push/update"
|
||||
body: "*"
|
||||
@@ -229,11 +224,16 @@ service api {
|
||||
get: "/api/transform/list"
|
||||
};
|
||||
}
|
||||
rpc GetRecordList (ReqRecordList) returns (ResponseList) {
|
||||
rpc GetRecordList (ReqRecordList) returns (RecordResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/record/{type}/list/{streamPath=**}"
|
||||
};
|
||||
}
|
||||
rpc GetEventRecordList (ReqRecordList) returns (EventRecordResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/record/{type}/event/list/{streamPath=**}"
|
||||
};
|
||||
}
|
||||
rpc GetRecordCatalog (ReqRecordCatalog) returns (ResponseCatalog) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/record/{type}/catalog"
|
||||
@@ -245,6 +245,11 @@ service api {
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc GetAlarmList (AlarmListRequest) returns (AlarmListResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/alarm/list"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message DisabledPluginsResponse {
|
||||
@@ -566,6 +571,22 @@ message PullProxyInfo {
|
||||
string streamPath = 16; // 流路径
|
||||
}
|
||||
|
||||
message UpdatePullProxyRequest {
|
||||
uint32 ID = 1;
|
||||
optional uint32 parentID = 2; // 父设备ID
|
||||
optional string name = 3; // 设备名称
|
||||
optional string type = 4; // 设备类型
|
||||
optional uint32 status = 5; // 设备状态
|
||||
optional string pullURL = 6; // 拉流地址
|
||||
optional bool pullOnStart = 7; // 启动时拉流
|
||||
optional bool stopOnIdle = 8; // 空闲时停止拉流
|
||||
optional bool audio = 9; // 是否拉取音频
|
||||
optional string description = 10; // 设备描述
|
||||
optional string recordPath = 11; // 录制路径
|
||||
optional google.protobuf.Duration recordFragment = 12; // 录制片段长度
|
||||
optional string streamPath = 13; // 流路径
|
||||
}
|
||||
|
||||
message PushProxyInfo {
|
||||
uint32 ID = 1;
|
||||
google.protobuf.Timestamp createTime = 2;
|
||||
@@ -582,6 +603,20 @@ message PushProxyInfo {
|
||||
string streamPath = 13; // 流路径
|
||||
}
|
||||
|
||||
message UpdatePushProxyRequest {
|
||||
uint32 ID = 1;
|
||||
optional uint32 parentID = 2; // 父设备ID
|
||||
optional string name = 3; // 设备名称
|
||||
optional string type = 4; // 设备类型
|
||||
optional uint32 status = 5; // 设备状态
|
||||
optional string pushURL = 6; // 推流地址
|
||||
optional bool pushOnStart = 7; // 启动时推流
|
||||
optional bool audio = 8; // 是否推音频
|
||||
optional string description = 9; // 设备描述
|
||||
optional uint32 rtt = 10; // 平均RTT
|
||||
optional string streamPath = 11; // 流路径
|
||||
}
|
||||
|
||||
message PushProxyListResponse {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
@@ -669,8 +704,8 @@ message ReqRecordList {
|
||||
string end = 4;
|
||||
uint32 pageNum = 5;
|
||||
uint32 pageSize = 6;
|
||||
string mode = 7;
|
||||
string type = 8;
|
||||
string type = 7;
|
||||
string eventLevel = 8;
|
||||
}
|
||||
|
||||
message RecordFile {
|
||||
@@ -681,15 +716,36 @@ message RecordFile {
|
||||
google.protobuf.Timestamp endTime = 5;
|
||||
}
|
||||
|
||||
message ResponseList {
|
||||
message EventRecordFile {
|
||||
uint32 id = 1;
|
||||
string filePath = 2;
|
||||
string streamPath = 3;
|
||||
google.protobuf.Timestamp startTime = 4;
|
||||
google.protobuf.Timestamp endTime = 5;
|
||||
string eventId = 6;
|
||||
string eventLevel = 7;
|
||||
string eventName = 8;
|
||||
string eventDesc = 9;
|
||||
}
|
||||
|
||||
message RecordResponseList {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint32 totalCount = 3;
|
||||
uint32 total = 3;
|
||||
uint32 pageNum = 4;
|
||||
uint32 pageSize = 5;
|
||||
repeated RecordFile data = 6;
|
||||
}
|
||||
|
||||
message EventRecordResponseList {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint32 total = 3;
|
||||
uint32 pageNum = 4;
|
||||
uint32 pageSize = 5;
|
||||
repeated EventRecordFile data = 6;
|
||||
}
|
||||
|
||||
message Catalog {
|
||||
string streamPath = 1;
|
||||
uint32 count = 2;
|
||||
@@ -720,4 +776,38 @@ message ResponseDelete {
|
||||
|
||||
message ReqRecordCatalog {
|
||||
string type = 1;
|
||||
}
|
||||
|
||||
message AlarmInfo {
|
||||
uint32 id = 1;
|
||||
string serverInfo = 2;
|
||||
string streamName = 3;
|
||||
string streamPath = 4;
|
||||
string alarmDesc = 5;
|
||||
string alarmName = 6;
|
||||
int32 alarmType = 7;
|
||||
bool isSent = 8;
|
||||
string filePath = 9;
|
||||
google.protobuf.Timestamp createdAt = 10;
|
||||
google.protobuf.Timestamp updatedAt = 11;
|
||||
}
|
||||
|
||||
message AlarmListRequest {
|
||||
int32 pageNum = 1;
|
||||
int32 pageSize = 2;
|
||||
string range = 3;
|
||||
string start = 4;
|
||||
string end = 5;
|
||||
int32 alarmType = 6;
|
||||
string streamPath = 7;
|
||||
string streamName = 8;
|
||||
}
|
||||
|
||||
message AlarmListResponse {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
int32 total = 3;
|
||||
int32 pageNum = 4;
|
||||
int32 pageSize = 5;
|
||||
repeated AlarmInfo data = 6;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -65,8 +65,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var _ IAVFrame = (*AnnexB)(nil)
|
||||
|
||||
func (frame *AVFrame) Clone() {
|
||||
|
||||
}
|
||||
|
||||
74
pkg/avframe_convert.go
Normal file
74
pkg/avframe_convert.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"m7s.live/v5/pkg/codec"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
type AVFrameConvert[T IAVFrame] struct {
|
||||
FromTrack, ToTrack *AVTrack
|
||||
lastFromCodecCtx codec.ICodecCtx
|
||||
}
|
||||
|
||||
func NewAVFrameConvert[T IAVFrame](fromTrack *AVTrack, toTrack *AVTrack) *AVFrameConvert[T] {
|
||||
ret := &AVFrameConvert[T]{}
|
||||
ret.FromTrack = fromTrack
|
||||
ret.ToTrack = toTrack
|
||||
if ret.FromTrack == nil {
|
||||
ret.FromTrack = &AVTrack{
|
||||
RingWriter: &RingWriter{
|
||||
Ring: util.NewRing[AVFrame](1),
|
||||
},
|
||||
}
|
||||
}
|
||||
if ret.ToTrack == nil {
|
||||
ret.ToTrack = &AVTrack{
|
||||
RingWriter: &RingWriter{
|
||||
Ring: util.NewRing[AVFrame](1),
|
||||
},
|
||||
}
|
||||
var to T
|
||||
ret.ToTrack.FrameType = reflect.TypeOf(to).Elem()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *AVFrameConvert[T]) ConvertFromAVFrame(avFrame *AVFrame) (to T, err error) {
|
||||
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
|
||||
if c.ToTrack.ICodecCtx == nil {
|
||||
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = avFrame.Demux(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
to.SetAllocator(avFrame.Wraps[0].GetAllocator())
|
||||
to.Mux(c.ToTrack.ICodecCtx, avFrame)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AVFrameConvert[T]) Convert(frame IAVFrame) (to T, err error) {
|
||||
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
|
||||
// Not From Publisher
|
||||
if c.FromTrack.LastValue == nil {
|
||||
err = frame.Parse(c.FromTrack)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.ToTrack.ICodecCtx == nil || c.lastFromCodecCtx != c.FromTrack.ICodecCtx {
|
||||
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.lastFromCodecCtx = c.FromTrack.ICodecCtx
|
||||
if c.FromTrack.Value.Raw, err = frame.Demux(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
to.SetAllocator(frame.GetAllocator())
|
||||
to.Mux(c.ToTrack.ICodecCtx, &c.FromTrack.Value)
|
||||
return
|
||||
}
|
||||
@@ -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,13 @@ func (h265 *H265Ctx) GetBase() ICodecCtx {
|
||||
func (h265 *H265Ctx) GetRecord() []byte {
|
||||
return h265.Record
|
||||
}
|
||||
|
||||
func (h265 *H265Ctx) String() string {
|
||||
// 根据 HEVC 标准格式:hvc1.profile.compatibility.level.constraints
|
||||
profile := h265.RecordInfo.AVCProfileIndication
|
||||
compatibility := h265.RecordInfo.ProfileCompatibility
|
||||
level := h265.RecordInfo.AVCLevelIndication
|
||||
|
||||
// 简单实现,使用可用字段模拟 HEVC 格式
|
||||
return fmt.Sprintf("hvc1.%d.%X.L%d.00", profile, compatibility, level)
|
||||
}
|
||||
|
||||
@@ -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 { // 读取环境变量
|
||||
@@ -201,6 +208,9 @@ func (config *Config) ParseUserFile(conf map[string]any) {
|
||||
}
|
||||
config.File = conf
|
||||
for k, v := range conf {
|
||||
k = strings.ReplaceAll(k, "-", "")
|
||||
k = strings.ReplaceAll(k, "_", "")
|
||||
k = strings.ToLower(k)
|
||||
if config.Has(k) {
|
||||
if prop := config.Get(k); prop.props != nil {
|
||||
if v != nil {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package config
|
||||
|
||||
type DB struct {
|
||||
DBType string `default:"sqlite" desc:"数据库类型"`
|
||||
DSN string `default:"m7s.db" desc:"数据库文件路径"`
|
||||
DBType string `default:"sqlite" desc:"数据库类型"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ type TCP struct {
|
||||
KeyFile string `desc:"私钥文件"`
|
||||
ListenNum int `desc:"同时并行监听数量,0为CPU核心数量"` //同时并行监听数量,0为CPU核心数量
|
||||
NoDelay bool `desc:"是否禁用Nagle算法"` //是否禁用Nagle算法
|
||||
WriteBuffer int `desc:"写缓冲区大小"` //写缓冲区大小
|
||||
ReadBuffer int `desc:"读缓冲区大小"` //读缓冲区大小
|
||||
KeepAlive bool `desc:"是否启用KeepAlive"` //是否启用KeepAlive
|
||||
AutoListen bool `default:"true" desc:"是否自动监听"`
|
||||
}
|
||||
@@ -125,7 +127,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
|
||||
}
|
||||
@@ -141,6 +143,18 @@ func (task *ListenTCPWork) listen(handler TCPHandler) {
|
||||
if !task.NoDelay {
|
||||
tcpConn.SetNoDelay(false)
|
||||
}
|
||||
if task.WriteBuffer > 0 {
|
||||
if err := tcpConn.SetWriteBuffer(task.WriteBuffer); err != nil {
|
||||
task.Error("failed to set write buffer", "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if task.ReadBuffer > 0 {
|
||||
if err := tcpConn.SetReadBuffer(task.ReadBuffer); err != nil {
|
||||
task.Error("failed to set read buffer", "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
tempDelay = 0
|
||||
subTask := handler(tcpConn)
|
||||
task.AddTask(subTask)
|
||||
|
||||
@@ -16,16 +16,50 @@ const (
|
||||
RelayModeRelay = "relay"
|
||||
RelayModeMix = "mix"
|
||||
|
||||
HookOnPublish HookType = "publish"
|
||||
HookOnSubscribe HookType = "subscribe"
|
||||
HookOnPublishEnd HookType = "publish_end"
|
||||
HookOnSubscribeEnd HookType = "subscribe_end"
|
||||
RecordModeAuto RecordMode = "auto"
|
||||
RecordModeEvent RecordMode = "event"
|
||||
|
||||
HookOnServerKeepAlive HookType = "server_keep_alive"
|
||||
HookOnPublishStart HookType = "publish_start"
|
||||
HookOnPublishEnd HookType = "publish_end"
|
||||
HookOnSubscribeStart HookType = "subscribe_start"
|
||||
HookOnSubscribeEnd HookType = "subscribe_end"
|
||||
HookOnPullStart HookType = "pull_start"
|
||||
HookOnPullEnd HookType = "pull_end"
|
||||
HookOnPushStart HookType = "push_start"
|
||||
HookOnPushEnd HookType = "push_end"
|
||||
HookOnRecordStart HookType = "record_start"
|
||||
HookOnRecordEnd HookType = "record_end"
|
||||
HookOnTransformStart HookType = "transform_start"
|
||||
HookOnTransformEnd HookType = "transform_end"
|
||||
HookOnSystemStart HookType = "system_start"
|
||||
HookDefault HookType = "default"
|
||||
|
||||
EventLevelLow EventLevel = "low"
|
||||
EventLevelHigh EventLevel = "high"
|
||||
|
||||
AlarmStorageException = 0x10010 // 存储异常
|
||||
AlarmStorageExceptionRecover = 0x10011 // 存储异常恢复
|
||||
AlarmPullOffline = 0x10012 // 拉流异常,触发一次报警。
|
||||
AlarmPullRecover = 0x10013 // 拉流恢复
|
||||
AlarmDiskSpaceFull = 0x10014 // 磁盘空间满,磁盘占有率,超出最大磁盘空间使用率,触发报警。
|
||||
AlarmStartupRunning = 0x10015 // 启动运行
|
||||
AlarmPublishOffline = 0x10016 // 发布者异常,触发一次报警。
|
||||
AlarmPublishRecover = 0x10017 // 发布者恢复
|
||||
AlarmSubscribeOffline = 0x10018 // 订阅者异常,触发一次报警。
|
||||
AlarmSubscribeRecover = 0x10019 // 订阅者恢复
|
||||
AlarmPushOffline = 0x10020 // 推流异常,触发一次报警。
|
||||
AlarmPushRecover = 0x10021 // 推流恢复
|
||||
AlarmTransformOffline = 0x10022 // 转换异常,触发一次报警。
|
||||
AlarmTransformRecover = 0x10023 // 转换恢复
|
||||
AlarmKeepAliveOnline = 0x10024 // 保活正常,触发一次报警。
|
||||
)
|
||||
|
||||
type (
|
||||
HookType string
|
||||
Publish struct {
|
||||
EventLevel = string
|
||||
RecordMode = string
|
||||
HookType string
|
||||
Publish struct {
|
||||
MaxCount int `default:"0" desc:"最大发布者数量"` // 最大发布者数量
|
||||
PubAudio bool `default:"true" desc:"是否发布音频"`
|
||||
PubVideo bool `default:"true" desc:"是否发布视频"`
|
||||
@@ -36,9 +70,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:混合转发"` // 转发模式
|
||||
@@ -54,18 +88,21 @@ type (
|
||||
SyncMode int `default:"1" desc:"同步模式" enum:"0:采用时间戳同步,1:采用写入时间同步"` // 0,采用时间戳同步,1,采用写入时间同步
|
||||
IFrameOnly bool `desc:"只要关键帧"` // 只要关键帧
|
||||
WaitTimeout time.Duration `default:"10s" desc:"等待流超时时间"` // 等待流超时
|
||||
WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小
|
||||
Key string `desc:"订阅鉴权key"` // 订阅鉴权key
|
||||
SubType string `desc:"订阅类型"` // 订阅类型
|
||||
WaitTrack string `default:"video" desc:"等待轨道" enum:"audio:等待音频,video:等待视频,all:等待全部"`
|
||||
WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小
|
||||
Key string `desc:"订阅鉴权key"` // 订阅鉴权key
|
||||
SubType string `desc:"订阅类型"` // 订阅类型
|
||||
}
|
||||
HTTPValues map[string][]string
|
||||
Pull struct {
|
||||
URL string `desc:"拉流地址"`
|
||||
Loop int `desc:"拉流循环次数,-1:无限循环"` // 拉流循环次数,-1 表示无限循环
|
||||
MaxRetry int `default:"-1" desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉,高于0 的数代表最大重拉次数
|
||||
RetryInterval time.Duration `default:"5s" desc:"重试间隔"` // 重试间隔
|
||||
Proxy string `desc:"代理地址"` // 代理地址
|
||||
Header HTTPValues
|
||||
Args HTTPValues `gorm:"-:all"` // 拉流参数
|
||||
Args HTTPValues `gorm:"-:all"` // 拉流参数
|
||||
TestMode int `desc:"测试模式,0:关闭,1:只拉流不发布"` // 测试模式
|
||||
}
|
||||
Push struct {
|
||||
URL string `desc:"推送地址"` // 推送地址
|
||||
@@ -74,11 +111,21 @@ type (
|
||||
Proxy string `desc:"代理地址"` // 代理地址
|
||||
Header HTTPValues
|
||||
}
|
||||
RecordEvent struct {
|
||||
EventId string
|
||||
BeforeDuration uint32 `json:"beforeDuration" desc:"事件前缓存时长" gorm:"comment:事件前缓存时长;default:30000"`
|
||||
AfterDuration uint32 `json:"afterDuration" desc:"事件后缓存时长" gorm:"comment:事件后缓存时长;default:30000"`
|
||||
EventDesc string `json:"eventDesc" desc:"事件描述" gorm:"type:varchar(255);comment:事件描述"`
|
||||
EventLevel EventLevel `json:"eventLevel" desc:"事件级别" gorm:"type:varchar(255);comment:事件级别,high表示重要事件,无法删除且表示无需自动删除,low表示非重要事件,达到自动删除时间后,自动删除;default:'low'"`
|
||||
EventName string `json:"eventName" desc:"事件名称" gorm:"type:varchar(255);comment:事件名称"`
|
||||
}
|
||||
Record struct {
|
||||
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
|
||||
FilePath string `desc:"录制文件路径"` // 录制文件路径
|
||||
Fragment time.Duration `desc:"分片时长"` // 分片时长
|
||||
Append bool `desc:"是否追加录制"` // 是否追加录制
|
||||
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式,event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式,event=事件录像模式;default:'auto'"`
|
||||
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
|
||||
FilePath string `desc:"录制文件路径"` // 录制文件路径
|
||||
Fragment time.Duration `desc:"分片时长"` // 分片时长
|
||||
Append bool `desc:"是否追加录制"` // 是否追加录制
|
||||
Event *RecordEvent `json:"event" desc:"事件录像配置" gorm:"-"` // 事件录像配置
|
||||
}
|
||||
TransfromOutput struct {
|
||||
Target string `desc:"转码目标"` // 转码目标
|
||||
@@ -99,13 +146,14 @@ type (
|
||||
Transform map[Regexp]Transform
|
||||
}
|
||||
Webhook struct {
|
||||
URL string `yaml:"url" json:"url"` // Webhook 地址
|
||||
Method string `yaml:"method" json:"method" default:"POST"` // HTTP 方法
|
||||
Headers map[string]string `yaml:"headers" json:"headers"` // 自定义请求头
|
||||
TimeoutSeconds int `yaml:"timeout" json:"timeout" default:"5"` // 超时时间(秒)
|
||||
RetryTimes int `yaml:"retry" json:"retry" default:"3"` // 重试次数
|
||||
RetryInterval time.Duration `yaml:"retryInterval" json:"retryInterval" default:"1s"` // 重试间隔
|
||||
Interval int `yaml:"interval" json:"interval" default:"60"` // 保活间隔(秒)
|
||||
URL string // Webhook 地址
|
||||
Method string `default:"POST"` // HTTP 方法
|
||||
Headers map[string]string // 自定义请求头
|
||||
TimeoutSeconds int `default:"5"` // 超时时间(秒)
|
||||
RetryTimes int `default:"3"` // 重试次数
|
||||
RetryInterval time.Duration `default:"1s"` // 重试间隔
|
||||
Interval int `default:"60"` // 保活间隔(秒)
|
||||
SaveAlarm bool `default:"false"` // 是否保存告警到数据库
|
||||
}
|
||||
Common struct {
|
||||
PublicIP string
|
||||
@@ -164,3 +212,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
|
||||
}
|
||||
|
||||
@@ -9,14 +9,11 @@ import (
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
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
|
||||
gorm.Model
|
||||
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 `gorm:"type:timestamp;default:CURRENT_TIMESTAMP"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook to hash password before saving
|
||||
|
||||
@@ -9,10 +9,12 @@ var (
|
||||
ErrRecordExists = errors.New("record exists")
|
||||
ErrKick = errors.New("kick")
|
||||
ErrDiscard = errors.New("discard")
|
||||
ErrPublishMaxCount = errors.New("publish max count exceeded")
|
||||
ErrPublishTimeout = errors.New("publish timeout")
|
||||
ErrPublishIdleTimeout = errors.New("publish idle timeout")
|
||||
ErrPublishDelayCloseTimeout = errors.New("publish delay close timeout")
|
||||
ErrPushRemoteURLExist = errors.New("push remote url exist")
|
||||
ErrSubscribeMaxCount = errors.New("subscribe max count exceeded")
|
||||
ErrSubscribeTimeout = errors.New("subscribe timeout")
|
||||
ErrRestart = errors.New("restart")
|
||||
ErrInterrupt = errors.New("interrupt")
|
||||
|
||||
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)
|
||||
}
|
||||
19
pkg/raw.go
19
pkg/raw.go
@@ -2,13 +2,14 @@ package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
"io"
|
||||
"m7s.live/v5/pkg/codec"
|
||||
"m7s.live/v5/pkg/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ IAVFrame = (*RawAudio)(nil)
|
||||
@@ -104,6 +105,8 @@ type H26xFrame struct {
|
||||
}
|
||||
|
||||
func (h *H26xFrame) Parse(track *AVTrack) (err error) {
|
||||
var hasVideoFrame bool
|
||||
|
||||
switch h.FourCC {
|
||||
case codec.FourCC_H264:
|
||||
var ctx *codec.H264Ctx
|
||||
@@ -127,6 +130,9 @@ func (h *H26xFrame) Parse(track *AVTrack) (err error) {
|
||||
}
|
||||
case codec.NALU_IDR_Picture:
|
||||
track.Value.IDR = true
|
||||
hasVideoFrame = true
|
||||
case codec.NALU_Non_IDR_Picture:
|
||||
hasVideoFrame = true
|
||||
}
|
||||
}
|
||||
case codec.FourCC_H265:
|
||||
@@ -155,9 +161,18 @@ func (h *H26xFrame) Parse(track *AVTrack) (err error) {
|
||||
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_CRA:
|
||||
track.Value.IDR = true
|
||||
hasVideoFrame = true
|
||||
case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
|
||||
hasVideoFrame = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return ErrSkip if no video frames are present (only metadata NALUs)
|
||||
if !hasVideoFrame {
|
||||
return ErrSkip
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
157
pkg/raw_test.go
Normal file
157
pkg/raw_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"m7s.live/v5/pkg/codec"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
func TestH26xFrame_Parse_VideoFrameDetection(t *testing.T) {
|
||||
// Test H264 IDR Picture (should not skip)
|
||||
t.Run("H264_IDR_Picture", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H264,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x65}), // IDR Picture NALU type
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err == ErrSkip {
|
||||
t.Error("Expected H264 IDR frame to not be skipped, but got ErrSkip")
|
||||
}
|
||||
if !track.Value.IDR {
|
||||
t.Error("Expected IDR flag to be set for H264 IDR frame")
|
||||
}
|
||||
})
|
||||
|
||||
// Test H264 Non-IDR Picture (should not skip)
|
||||
t.Run("H264_Non_IDR_Picture", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H264,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x21}), // Non-IDR Picture NALU type
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err == ErrSkip {
|
||||
t.Error("Expected H264 Non-IDR frame to not be skipped, but got ErrSkip")
|
||||
}
|
||||
})
|
||||
|
||||
// Test H264 metadata only (should skip)
|
||||
t.Run("H264_SPS_Only", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H264,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x67}), // SPS NALU type
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err != ErrSkip {
|
||||
t.Errorf("Expected H264 SPS-only frame to be skipped, but got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Test H264 PPS only (should skip)
|
||||
t.Run("H264_PPS_Only", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H264,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x68}), // PPS NALU type
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err != ErrSkip {
|
||||
t.Errorf("Expected H264 PPS-only frame to be skipped, but got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Test H265 IDR slice (should not skip)
|
||||
t.Run("H265_IDR_Slice", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H265,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x4E, 0x01}), // IDR_W_RADL slice type (19 << 1 = 38 = 0x26, so first byte should be 0x4C, but let's use a simpler approach)
|
||||
// Using NAL_UNIT_CODED_SLICE_IDR_W_RADL which should be type 19
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
|
||||
// Let's use the correct byte pattern for H265 IDR slice
|
||||
// NAL_UNIT_CODED_SLICE_IDR_W_RADL = 19
|
||||
// H265 header: (type << 1) | layer_id_bit
|
||||
idrSliceByte := byte(19 << 1) // 19 * 2 = 38 = 0x26
|
||||
frame.Nalus[0] = util.NewMemory([]byte{idrSliceByte})
|
||||
|
||||
err := frame.Parse(track)
|
||||
if err == ErrSkip {
|
||||
t.Error("Expected H265 IDR slice to not be skipped, but got ErrSkip")
|
||||
}
|
||||
if !track.Value.IDR {
|
||||
t.Error("Expected IDR flag to be set for H265 IDR slice")
|
||||
}
|
||||
})
|
||||
|
||||
// Test H265 metadata only (should skip)
|
||||
t.Run("H265_VPS_Only", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H265,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x40, 0x01}), // VPS NALU type (32 << 1 = 64 = 0x40)
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err != ErrSkip {
|
||||
t.Errorf("Expected H265 VPS-only frame to be skipped, but got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Test mixed H264 frame with SPS and IDR (should not skip)
|
||||
t.Run("H264_Mixed_SPS_And_IDR", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H264,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x67}), // SPS NALU type
|
||||
util.NewMemory([]byte{0x65}), // IDR Picture NALU type
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
err := frame.Parse(track)
|
||||
if err == ErrSkip {
|
||||
t.Error("Expected H264 mixed SPS+IDR frame to not be skipped, but got ErrSkip")
|
||||
}
|
||||
if !track.Value.IDR {
|
||||
t.Error("Expected IDR flag to be set for H264 mixed frame with IDR")
|
||||
}
|
||||
})
|
||||
|
||||
// Test mixed H265 frame with VPS and IDR (should not skip)
|
||||
t.Run("H265_Mixed_VPS_And_IDR", func(t *testing.T) {
|
||||
frame := &H26xFrame{
|
||||
FourCC: codec.FourCC_H265,
|
||||
Nalus: []util.Memory{
|
||||
util.NewMemory([]byte{0x40, 0x01}), // VPS NALU type (32 << 1)
|
||||
util.NewMemory([]byte{0x4C, 0x01}), // IDR_W_RADL slice type (19 << 1)
|
||||
},
|
||||
}
|
||||
track := &AVTrack{}
|
||||
|
||||
// Fix the IDR slice byte for H265
|
||||
idrSliceByte := byte(19 << 1) // NAL_UNIT_CODED_SLICE_IDR_W_RADL = 19
|
||||
frame.Nalus[1] = util.NewMemory([]byte{idrSliceByte, 0x01})
|
||||
|
||||
err := frame.Parse(track)
|
||||
if err == ErrSkip {
|
||||
t.Error("Expected H265 mixed VPS+IDR frame to not be skipped, but got ErrSkip")
|
||||
}
|
||||
if !track.Value.IDR {
|
||||
t.Error("Expected IDR flag to be set for H265 mixed frame with IDR")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
pkg/task/job.go
106
pkg/task/job.go
@@ -32,14 +32,15 @@ func GetNextTaskID() uint32 {
|
||||
// Job include tasks
|
||||
type Job struct {
|
||||
Task
|
||||
cases []reflect.SelectCase
|
||||
addSub chan ITask
|
||||
children []ITask
|
||||
lazyRun sync.Once
|
||||
eventLoopLock sync.Mutex
|
||||
childrenDisposed chan struct{}
|
||||
childDisposeListeners []func(ITask)
|
||||
blocked ITask
|
||||
cases []reflect.SelectCase
|
||||
addSub chan ITask
|
||||
children []ITask
|
||||
lazyRun sync.Once
|
||||
eventLoopLock sync.Mutex
|
||||
childrenDisposed chan struct{}
|
||||
descendantsDisposeListeners []func(ITask)
|
||||
descendantsStartListeners []func(ITask)
|
||||
blocked ITask
|
||||
}
|
||||
|
||||
func (*Job) GetTaskType() TaskType {
|
||||
@@ -55,19 +56,26 @@ func (mt *Job) Blocked() ITask {
|
||||
}
|
||||
|
||||
func (mt *Job) waitChildrenDispose() {
|
||||
if blocked := mt.blocked; blocked != nil {
|
||||
blocked := mt.blocked
|
||||
defer func() {
|
||||
// 忽略由于在任务关闭过程中可能存在竞态条件,当父任务关闭时子任务可能已经被释放。
|
||||
if err := recover(); err != nil {
|
||||
mt.Debug("waitChildrenDispose panic", "err", err)
|
||||
}
|
||||
mt.addSub <- nil
|
||||
<-mt.childrenDisposed
|
||||
}()
|
||||
if blocked != nil {
|
||||
blocked.Stop(mt.StopReason())
|
||||
}
|
||||
mt.addSub <- nil
|
||||
<-mt.childrenDisposed
|
||||
}
|
||||
|
||||
func (mt *Job) OnChildDispose(listener func(ITask)) {
|
||||
mt.childDisposeListeners = append(mt.childDisposeListeners, listener)
|
||||
func (mt *Job) OnDescendantsDispose(listener func(ITask)) {
|
||||
mt.descendantsDisposeListeners = append(mt.descendantsDisposeListeners, listener)
|
||||
}
|
||||
|
||||
func (mt *Job) onDescendantsDispose(descendants ITask) {
|
||||
for _, listener := range mt.childDisposeListeners {
|
||||
for _, listener := range mt.descendantsDisposeListeners {
|
||||
listener(descendants)
|
||||
}
|
||||
if mt.parent != nil {
|
||||
@@ -76,11 +84,28 @@ func (mt *Job) onDescendantsDispose(descendants ITask) {
|
||||
}
|
||||
|
||||
func (mt *Job) onChildDispose(child ITask) {
|
||||
if child.getParent() == mt {
|
||||
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
|
||||
mt.onDescendantsDispose(child)
|
||||
}
|
||||
child.dispose()
|
||||
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
|
||||
mt.onDescendantsDispose(child)
|
||||
}
|
||||
child.dispose()
|
||||
}
|
||||
|
||||
func (mt *Job) OnDescendantsStart(listener func(ITask)) {
|
||||
mt.descendantsStartListeners = append(mt.descendantsStartListeners, listener)
|
||||
}
|
||||
|
||||
func (mt *Job) onDescendantsStart(descendants ITask) {
|
||||
for _, listener := range mt.descendantsStartListeners {
|
||||
listener(descendants)
|
||||
}
|
||||
if mt.parent != nil {
|
||||
mt.parent.onDescendantsStart(descendants)
|
||||
}
|
||||
}
|
||||
|
||||
func (mt *Job) onChildStart(child ITask) {
|
||||
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
|
||||
mt.onDescendantsStart(child)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +182,7 @@ func (mt *Job) AddTask(t ITask, opt ...any) (task *Task) {
|
||||
return
|
||||
}
|
||||
if len(mt.addSub) > 10 {
|
||||
if mt.Logger != nil {
|
||||
mt.Warn("task wait list too many", "count", len(mt.addSub), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType(), "parent", mt.GetOwnerType())
|
||||
}
|
||||
mt.Warn("task wait list too many", "count", len(mt.addSub), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType(), "parent", mt.GetOwnerType())
|
||||
}
|
||||
mt.addSub <- t
|
||||
return
|
||||
@@ -182,9 +205,7 @@ func (mt *Job) run() {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
if mt.Logger != nil {
|
||||
mt.Logger.Error("job panic", "err", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
mt.Error("job panic", "err", err, "stack", string(debug.Stack()))
|
||||
if !ThrowPanic {
|
||||
mt.Stop(errors.Join(err.(error), ErrPanic))
|
||||
} else {
|
||||
@@ -203,31 +224,42 @@ func (mt *Job) run() {
|
||||
mt.blocked = nil
|
||||
if chosen, rev, ok := reflect.Select(mt.cases); chosen == 0 {
|
||||
if rev.IsNil() {
|
||||
mt.Debug("job addSub channel closed, exiting", "taskId", mt.GetTaskID())
|
||||
return
|
||||
}
|
||||
if mt.blocked = rev.Interface().(ITask); mt.blocked.getParent() != mt || mt.blocked.start() {
|
||||
if mt.blocked = rev.Interface().(ITask); mt.blocked.start() {
|
||||
mt.children = append(mt.children, mt.blocked)
|
||||
mt.cases = append(mt.cases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(mt.blocked.GetSignal())})
|
||||
mt.onChildStart(mt.blocked)
|
||||
}
|
||||
} else {
|
||||
taskIndex := chosen - 1
|
||||
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())
|
||||
mt.onChildStart(mt.blocked)
|
||||
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 {
|
||||
|
||||
@@ -24,15 +24,49 @@ func (m *Manager[K, T]) Add(ctx T, opt ...any) *Task {
|
||||
ctx.Stop(ErrExist)
|
||||
return
|
||||
}
|
||||
if m.Logger != nil {
|
||||
m.Logger.Debug("add", "key", ctx.GetKey(), "count", m.Length)
|
||||
}
|
||||
m.Debug("add", "key", ctx.GetKey(), "count", m.Length)
|
||||
})
|
||||
ctx.OnDispose(func() {
|
||||
m.Remove(ctx)
|
||||
if m.Logger != nil {
|
||||
m.Logger.Debug("remove", "key", ctx.GetKey(), "count", m.Length)
|
||||
}
|
||||
m.Debug("remove", "key", ctx.GetKey(), "count", m.Length)
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// SafeFind 用于不同协程获取元素,防止并发请求
|
||||
func (m *Manager[K, T]) SafeFind(f func(T) bool) (item T, ok bool) {
|
||||
if m.L == nil {
|
||||
m.Call(func() error {
|
||||
item, ok = m.Collection.Find(f)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
item, ok = m.Collection.Find(f)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
102
pkg/task/task.go
102
pkg/task/task.go
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"maps"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -53,7 +54,6 @@ type (
|
||||
ITask interface {
|
||||
context.Context
|
||||
keepalive() bool
|
||||
getParent() *Job
|
||||
GetParent() ITask
|
||||
GetTask() *Task
|
||||
GetTaskID() uint32
|
||||
@@ -77,13 +77,16 @@ type (
|
||||
OnDispose(func())
|
||||
GetState() TaskState
|
||||
GetLevel() byte
|
||||
WaitStopped() error
|
||||
WaitStarted() error
|
||||
}
|
||||
IJob interface {
|
||||
ITask
|
||||
getJob() *Job
|
||||
AddTask(ITask, ...any) *Task
|
||||
RangeSubTask(func(yield ITask) bool)
|
||||
OnChildDispose(func(ITask))
|
||||
OnDescendantsDispose(func(ITask))
|
||||
OnDescendantsStart(func(ITask))
|
||||
Blocked() ITask
|
||||
Call(func() error, ...any)
|
||||
Post(func() error, ...any) *Task
|
||||
@@ -115,7 +118,7 @@ type (
|
||||
ID uint32
|
||||
StartTime time.Time
|
||||
StartReason string
|
||||
*slog.Logger
|
||||
Logger *slog.Logger
|
||||
context.Context
|
||||
context.CancelCauseFunc
|
||||
handler ITask
|
||||
@@ -176,10 +179,6 @@ func (task *Task) GetTaskPointer() uintptr {
|
||||
return uintptr(unsafe.Pointer(task))
|
||||
}
|
||||
|
||||
func (task *Task) getParent() *Job {
|
||||
return task.parent
|
||||
}
|
||||
|
||||
func (task *Task) GetKey() uint32 {
|
||||
return task.ID
|
||||
}
|
||||
@@ -200,7 +199,11 @@ func (task *Task) WaitStopped() (err error) {
|
||||
}
|
||||
|
||||
func (task *Task) Trace(msg string, fields ...any) {
|
||||
task.Log(task.Context, TraceLevel, msg, fields...)
|
||||
if task.Logger == nil {
|
||||
slog.Default().Log(task.Context, TraceLevel, msg, fields...)
|
||||
return
|
||||
}
|
||||
task.Logger.Log(task.Context, TraceLevel, msg, fields...)
|
||||
}
|
||||
|
||||
func (task *Task) IsStopped() bool {
|
||||
@@ -227,8 +230,9 @@ func (task *Task) Stop(err error) {
|
||||
panic("task stop with nil error")
|
||||
}
|
||||
if task.CancelCauseFunc != nil {
|
||||
if tt := task.handler.GetTaskType(); task.Logger != nil && tt != TASK_TYPE_CALL {
|
||||
task.Debug("task stop", "reason", err, "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType())
|
||||
if tt := task.handler.GetTaskType(); tt != TASK_TYPE_CALL {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
task.Debug("task stop", "caller", fmt.Sprintf("%s:%d", strings.TrimPrefix(file, sourceFilePathPrefix), line), "reason", err, "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType())
|
||||
}
|
||||
task.CancelCauseFunc(err)
|
||||
}
|
||||
@@ -266,12 +270,10 @@ func (task *Task) checkRetry(err error) bool {
|
||||
if task.retry.MaxRetry < 0 || task.retry.RetryCount < task.retry.MaxRetry {
|
||||
task.retry.RetryCount++
|
||||
task.SetDescription("retryCount", task.retry.RetryCount)
|
||||
if task.Logger != nil {
|
||||
if task.retry.MaxRetry < 0 {
|
||||
task.Warn(fmt.Sprintf("retry %d/∞", task.retry.RetryCount), "taskId", task.ID)
|
||||
} else {
|
||||
task.Warn(fmt.Sprintf("retry %d/%d", task.retry.RetryCount, task.retry.MaxRetry), "taskId", task.ID)
|
||||
}
|
||||
if task.retry.MaxRetry < 0 {
|
||||
task.Warn(fmt.Sprintf("retry %d/∞", task.retry.RetryCount), "taskId", task.ID)
|
||||
} else {
|
||||
task.Warn(fmt.Sprintf("retry %d/%d", task.retry.RetryCount, task.retry.MaxRetry), "taskId", task.ID)
|
||||
}
|
||||
if delta := time.Since(task.StartTime); delta < task.retry.RetryInterval {
|
||||
time.Sleep(task.retry.RetryInterval - delta)
|
||||
@@ -279,9 +281,7 @@ func (task *Task) checkRetry(err error) bool {
|
||||
return true
|
||||
} else {
|
||||
if task.retry.MaxRetry > 0 {
|
||||
if task.Logger != nil {
|
||||
task.Warn(fmt.Sprintf("max retry %d failed", task.retry.MaxRetry))
|
||||
}
|
||||
task.Warn(fmt.Sprintf("max retry %d failed", task.retry.MaxRetry))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -294,15 +294,13 @@ func (task *Task) start() bool {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New(fmt.Sprint(r))
|
||||
if task.Logger != nil {
|
||||
task.Error("panic", "error", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
task.Error("panic", "error", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
}
|
||||
for {
|
||||
task.StartTime = time.Now()
|
||||
if tt := task.handler.GetTaskType(); task.Logger != nil && tt != TASK_TYPE_CALL {
|
||||
if tt := task.handler.GetTaskType(); tt != TASK_TYPE_CALL {
|
||||
task.Debug("task start", "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType(), "reason", task.StartReason)
|
||||
}
|
||||
task.state = TASK_STATE_STARTING
|
||||
@@ -324,6 +322,7 @@ func (task *Task) start() bool {
|
||||
task.ResetRetryCount()
|
||||
if runHandler, ok := task.handler.(TaskBlock); ok {
|
||||
task.state = TASK_STATE_RUNNING
|
||||
task.Debug("task run", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
||||
err = runHandler.Run()
|
||||
if err == nil {
|
||||
err = ErrTaskComplete
|
||||
@@ -334,6 +333,7 @@ func (task *Task) start() bool {
|
||||
if err == nil {
|
||||
if goHandler, ok := task.handler.(TaskGo); ok {
|
||||
task.state = TASK_STATE_GOING
|
||||
task.Debug("task go", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
||||
go task.run(goHandler.Go)
|
||||
}
|
||||
return true
|
||||
@@ -380,19 +380,17 @@ func (task *Task) SetDescriptions(value Description) {
|
||||
func (task *Task) dispose() {
|
||||
taskType, ownerType := task.handler.GetTaskType(), task.GetOwnerType()
|
||||
if task.state < TASK_STATE_STARTED {
|
||||
if task.Logger != nil && taskType != TASK_TYPE_CALL {
|
||||
if taskType != TASK_TYPE_CALL {
|
||||
task.Debug("task dispose canceled", "taskId", task.ID, "taskType", taskType, "ownerType", ownerType, "state", task.state)
|
||||
}
|
||||
return
|
||||
}
|
||||
reason := task.StopReason()
|
||||
task.state = TASK_STATE_DISPOSING
|
||||
if task.Logger != nil {
|
||||
if taskType != TASK_TYPE_CALL {
|
||||
yargs := []any{"reason", reason, "taskId", task.ID, "taskType", taskType, "ownerType", ownerType}
|
||||
task.Debug("task dispose", yargs...)
|
||||
defer task.Debug("task disposed", yargs...)
|
||||
}
|
||||
if taskType != TASK_TYPE_CALL {
|
||||
yargs := []any{"reason", reason, "taskId", task.ID, "taskType", taskType, "ownerType", ownerType}
|
||||
task.Debug("task dispose", yargs...)
|
||||
defer task.Debug("task disposed", yargs...)
|
||||
}
|
||||
befores := len(task.beforeDisposeListeners)
|
||||
for i, listener := range task.beforeDisposeListeners {
|
||||
@@ -427,15 +425,17 @@ func (task *Task) ResetRetryCount() {
|
||||
task.retry.RetryCount = 0
|
||||
}
|
||||
|
||||
func (task *Task) GetRetryCount() int {
|
||||
return task.retry.RetryCount
|
||||
}
|
||||
|
||||
func (task *Task) run(handler func() error) {
|
||||
var err error
|
||||
defer func() {
|
||||
if !ThrowPanic {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New(fmt.Sprint(r))
|
||||
if task.Logger != nil {
|
||||
task.Error("panic", "error", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
task.Error("panic", "error", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
@@ -446,3 +446,39 @@ func (task *Task) run(handler func() error) {
|
||||
}()
|
||||
err = handler()
|
||||
}
|
||||
|
||||
func (task *Task) Debug(msg string, args ...any) {
|
||||
if task.Logger == nil {
|
||||
slog.Default().Debug(msg, args...)
|
||||
return
|
||||
}
|
||||
task.Logger.Debug(msg, args...)
|
||||
}
|
||||
|
||||
func (task *Task) Info(msg string, args ...any) {
|
||||
if task.Logger == nil {
|
||||
slog.Default().Info(msg, args...)
|
||||
return
|
||||
}
|
||||
task.Logger.Info(msg, args...)
|
||||
}
|
||||
|
||||
func (task *Task) Warn(msg string, args ...any) {
|
||||
if task.Logger == nil {
|
||||
slog.Default().Warn(msg, args...)
|
||||
return
|
||||
}
|
||||
task.Logger.Warn(msg, args...)
|
||||
}
|
||||
|
||||
func (task *Task) Error(msg string, args ...any) {
|
||||
if task.Logger == nil {
|
||||
slog.Default().Error(msg, args...)
|
||||
return
|
||||
}
|
||||
task.Logger.Error(msg, args...)
|
||||
}
|
||||
|
||||
func (task *Task) TraceEnabled() bool {
|
||||
return task.Logger.Enabled(task.Context, TraceLevel)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,26 @@ func Test_Hooks(t *testing.T) {
|
||||
root.AddTask(&task).WaitStopped()
|
||||
}
|
||||
|
||||
type startFailTask struct {
|
||||
Task
|
||||
}
|
||||
|
||||
func (task *startFailTask) Start() error {
|
||||
return errors.New("start failed")
|
||||
}
|
||||
|
||||
func (task *startFailTask) Dispose() {
|
||||
task.Logger.Info("Dispose")
|
||||
}
|
||||
|
||||
func Test_StartFail(t *testing.T) {
|
||||
var task startFailTask
|
||||
root.AddTask(&task)
|
||||
if err := task.WaitStarted(); err == nil {
|
||||
t.Errorf("expected start to fail")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//type DemoTask struct {
|
||||
// Task
|
||||
|
||||
119
pkg/track.go
119
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 {
|
||||
@@ -94,9 +168,9 @@ func (t *AVTrack) Ready(err error) {
|
||||
} else {
|
||||
switch ctx := t.ICodecCtx.(type) {
|
||||
case IVideoCodecCtx:
|
||||
t.Info("ready", "info", t.ICodecCtx.GetInfo(), "width", ctx.Width(), "height", ctx.Height())
|
||||
t.Info("ready", "codec", t.ICodecCtx.FourCC(), "info", t.ICodecCtx.GetInfo(), "width", ctx.Width(), "height", ctx.Height())
|
||||
case IAudioCodecCtx:
|
||||
t.Info("ready", "info", t.ICodecCtx.GetInfo(), "channels", ctx.GetChannels(), "sample_rate", ctx.GetSampleRate())
|
||||
t.Info("ready", "codec", t.ICodecCtx.FourCC(), "info", t.ICodecCtx.GetInfo(), "channels", ctx.GetChannels(), "sample_rate", ctx.GetSampleRate())
|
||||
}
|
||||
}
|
||||
t.ready.Fulfill(err)
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,55 @@ package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Buddy struct {
|
||||
size int
|
||||
longests []int
|
||||
size int
|
||||
longests [BuddySize>>(MinPowerOf2-1) - 1]int
|
||||
memoryPool [BuddySize]byte
|
||||
poolStart int64
|
||||
lock sync.Mutex // 保护 longests 数组的并发访问
|
||||
}
|
||||
|
||||
var (
|
||||
InValidParameterErr = errors.New("buddy: invalid parameter")
|
||||
NotFoundErr = errors.New("buddy: can't find block")
|
||||
buddyPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return NewBuddy()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// GetBuddy 从池中获取一个 Buddy 实例
|
||||
func GetBuddy() *Buddy {
|
||||
buddy := buddyPool.Get().(*Buddy)
|
||||
return buddy
|
||||
}
|
||||
|
||||
// PutBuddy 将 Buddy 实例放回池中
|
||||
func PutBuddy(b *Buddy) {
|
||||
buddyPool.Put(b)
|
||||
}
|
||||
|
||||
// NewBuddy creates a buddy instance.
|
||||
// If the parameter isn't valid, return the nil and error as well
|
||||
func NewBuddy(size int) *Buddy {
|
||||
if !isPowerOf2(size) {
|
||||
size = fixSize(size)
|
||||
func NewBuddy() *Buddy {
|
||||
size := BuddySize >> MinPowerOf2
|
||||
ret := &Buddy{
|
||||
size: size,
|
||||
}
|
||||
nodeCount := 2*size - 1
|
||||
longests := make([]int, nodeCount)
|
||||
for nodeSize, i := 2*size, 0; i < nodeCount; i++ {
|
||||
for nodeSize, i := 2*size, 0; i < len(ret.longests); i++ {
|
||||
if isPowerOf2(i + 1) {
|
||||
nodeSize /= 2
|
||||
}
|
||||
longests[i] = nodeSize
|
||||
ret.longests[i] = nodeSize
|
||||
}
|
||||
return &Buddy{size, longests}
|
||||
ret.poolStart = int64(uintptr(unsafe.Pointer(&ret.memoryPool[0])))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Alloc find a unused block according to the size
|
||||
@@ -42,6 +64,8 @@ func (b *Buddy) Alloc(size int) (offset int, err error) {
|
||||
if !isPowerOf2(size) {
|
||||
size = fixSize(size)
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if size > b.longests[0] {
|
||||
err = NotFoundErr
|
||||
return
|
||||
@@ -70,6 +94,8 @@ func (b *Buddy) Free(offset int) error {
|
||||
if offset < 0 || offset >= b.size {
|
||||
return InValidParameterErr
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
nodeSize := 1
|
||||
index := offset + b.size - 1
|
||||
for ; b.longests[index] != 0; index = parent(index) {
|
||||
|
||||
@@ -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", "*")
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -58,53 +56,59 @@ func (r *RecyclableMemory) Recycle() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
memoryPool [BuddySize]byte
|
||||
buddy = NewBuddy(BuddySize >> MinPowerOf2)
|
||||
lock sync.Mutex
|
||||
poolStart = int64(uintptr(unsafe.Pointer(&memoryPool[0])))
|
||||
blockPool = list.New()
|
||||
//EnableCheckSize bool = false
|
||||
)
|
||||
|
||||
type MemoryAllocator struct {
|
||||
allocator *Allocator
|
||||
start int64
|
||||
memory []byte
|
||||
Size int
|
||||
buddy *Buddy
|
||||
}
|
||||
|
||||
// createMemoryAllocator 创建并初始化 MemoryAllocator
|
||||
func createMemoryAllocator(size int, buddy *Buddy, offset int) *MemoryAllocator {
|
||||
ret := &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
buddy: buddy,
|
||||
Size: size,
|
||||
memory: buddy.memoryPool[offset : offset+size],
|
||||
start: buddy.poolStart + int64(offset),
|
||||
}
|
||||
ret.allocator.Init(size)
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetMemoryAllocator(size int) (ret *MemoryAllocator) {
|
||||
lock.Lock()
|
||||
offset, err := buddy.Alloc(size >> MinPowerOf2)
|
||||
if blockPool.Len() > 0 {
|
||||
ret = blockPool.Remove(blockPool.Front()).(*MemoryAllocator)
|
||||
} else {
|
||||
ret = &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
if size < BuddySize {
|
||||
requiredSize := size >> MinPowerOf2
|
||||
// 循环尝试从池中获取可用的 buddy
|
||||
for {
|
||||
buddy := GetBuddy()
|
||||
offset, err := buddy.Alloc(requiredSize)
|
||||
PutBuddy(buddy)
|
||||
if err == nil {
|
||||
// 分配成功,使用这个 buddy
|
||||
return createMemoryAllocator(size, buddy, offset<<MinPowerOf2)
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.Unlock()
|
||||
ret.Size = size
|
||||
ret.allocator.Init(size)
|
||||
if err != nil {
|
||||
ret.memory = make([]byte, size)
|
||||
ret.start = int64(uintptr(unsafe.Pointer(&ret.memory[0])))
|
||||
return
|
||||
// 池中的 buddy 都无法分配或大小不够,使用系统内存
|
||||
memory := make([]byte, size)
|
||||
start := int64(uintptr(unsafe.Pointer(&memory[0])))
|
||||
return &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
Size: size,
|
||||
memory: memory,
|
||||
start: start,
|
||||
}
|
||||
offset = offset << MinPowerOf2
|
||||
ret.memory = memoryPool[offset : offset+size]
|
||||
ret.start = poolStart + int64(offset)
|
||||
return
|
||||
}
|
||||
|
||||
func (ma *MemoryAllocator) Recycle() {
|
||||
ma.allocator.Recycle()
|
||||
lock.Lock()
|
||||
blockPool.PushBack(ma)
|
||||
_ = buddy.Free(int((poolStart - ma.start) >> MinPowerOf2))
|
||||
if ma.buddy != nil {
|
||||
_ = ma.buddy.Free(int((ma.buddy.poolStart - ma.start) >> MinPowerOf2))
|
||||
ma.buddy = nil
|
||||
}
|
||||
ma.memory = nil
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func (ma *MemoryAllocator) Find(size int) (memory []byte) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
479
plugin.go
479
plugin.go
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -25,8 +26,8 @@ import (
|
||||
gatewayRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
myip "github.com/husanpao/ip"
|
||||
"google.golang.org/grpc"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
. "m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/db"
|
||||
@@ -43,13 +44,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 +91,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 +118,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)
|
||||
@@ -137,20 +134,9 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
finalConfig, _ := yaml.Marshal(p.Config.GetMap())
|
||||
p.Logger.Handler().(*MultiLogHandler).SetLevel(ParseLevel(p.config.LogLevel))
|
||||
p.Debug("config", "detail", string(finalConfig))
|
||||
if s.DisableAll {
|
||||
p.Disabled = true
|
||||
}
|
||||
if userConfig["enable"] == false {
|
||||
p.Disabled = true
|
||||
} else if userConfig["enable"] == true {
|
||||
p.Disabled = false
|
||||
}
|
||||
if p.Disabled {
|
||||
if userConfig["enable"] == false || (s.DisableAll && userConfig["enable"] != true) {
|
||||
p.disable("config")
|
||||
p.Warn("plugin disabled")
|
||||
return
|
||||
} else {
|
||||
p.assign()
|
||||
}
|
||||
p.Info("init", "version", plugin.Version)
|
||||
var err error
|
||||
@@ -158,7 +144,7 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
p.DB = s.DB
|
||||
} else if p.config.DSN != "" {
|
||||
if factory, ok := db.Factory[p.config.DBType]; ok {
|
||||
s.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
|
||||
p.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
s.Error("failed to connect database", "error", err, "dsn", s.config.DSN, "type", s.config.DBType)
|
||||
p.disable(fmt.Sprintf("database %v", err))
|
||||
@@ -166,47 +152,65 @@ 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
|
||||
}
|
||||
if err = p.DB.AutoMigrate(&EventRecordStream{}); err != nil {
|
||||
p.disable(fmt.Sprintf("auto migrate event record stream failed %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
s.AddTask(instance)
|
||||
if err := s.AddTask(instance).WaitStarted(); err != nil {
|
||||
p.disable(instance.StopReason().Error())
|
||||
return
|
||||
}
|
||||
var handlers map[string]http.HandlerFunc
|
||||
if v, ok := instance.(IRegisterHandler); ok {
|
||||
handlers = v.RegisterHandler()
|
||||
}
|
||||
p.registerHandler(handlers)
|
||||
s.Plugins.Add(p)
|
||||
return
|
||||
}
|
||||
|
||||
// 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,36 +273,23 @@ 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.Warn("plugin disabled")
|
||||
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
|
||||
s.AddTask(&webHookQueueTask)
|
||||
|
||||
if err = p.listen(); err != nil {
|
||||
return
|
||||
}
|
||||
if err = p.handler.OnInit(); err != nil {
|
||||
return
|
||||
}
|
||||
if p.Meta.ServiceDesc != nil && s.grpcServer != nil {
|
||||
s.grpcServer.RegisterService(p.Meta.ServiceDesc, p.handler)
|
||||
if p.Meta.RegisterGRPCHandler != nil {
|
||||
@@ -310,15 +301,6 @@ func (p *Plugin) Start() (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.Plugins.Add(p)
|
||||
if err = p.listen(); err != nil {
|
||||
p.disable(fmt.Sprintf("listen %v", err))
|
||||
return
|
||||
}
|
||||
if err = p.handler.OnInit(); err != nil {
|
||||
p.disable(fmt.Sprintf("init %v", err))
|
||||
return
|
||||
}
|
||||
if p.config.Hook != nil {
|
||||
if hook, ok := p.config.Hook[config.HookOnServerKeepAlive]; ok && hook.Interval > 0 {
|
||||
p.AddTask(&ServerKeepAliveTask{plugin: p})
|
||||
@@ -337,12 +319,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 {
|
||||
@@ -399,31 +381,100 @@ func (p *Plugin) OnStop() {
|
||||
|
||||
}
|
||||
|
||||
type WebHookQueueTask struct {
|
||||
task.Work
|
||||
}
|
||||
|
||||
var webHookQueueTask WebHookQueueTask
|
||||
|
||||
type WebHookTask struct {
|
||||
task.Task
|
||||
plugin *Plugin
|
||||
hookType config.HookType
|
||||
conf *config.Webhook
|
||||
conf config.Webhook
|
||||
data any
|
||||
jsonData []byte
|
||||
alarm AlarmInfo
|
||||
}
|
||||
|
||||
func (t *WebHookTask) Start() error {
|
||||
if t.conf == nil || t.conf.URL == "" {
|
||||
if t.conf.URL == "" {
|
||||
return task.ErrTaskComplete
|
||||
}
|
||||
|
||||
var err error
|
||||
t.jsonData, err = json.Marshal(t.data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal webhook data: %w", err)
|
||||
// 处理AlarmInfo数据
|
||||
if t.data != nil {
|
||||
// 获取主机名和IP地址
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "unknown"
|
||||
}
|
||||
|
||||
// 获取本机IP地址
|
||||
var ipAddr string
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err == nil {
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ipAddr = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ipAddr == "" {
|
||||
ipAddr = "unknown"
|
||||
}
|
||||
|
||||
// 直接使用t.data作为AlarmInfo
|
||||
alarmInfo, ok := t.data.(AlarmInfo)
|
||||
if !ok {
|
||||
return fmt.Errorf("data is not of type AlarmInfo")
|
||||
}
|
||||
|
||||
// 更新服务器信息
|
||||
if alarmInfo.ServerInfo == "" {
|
||||
alarmInfo.ServerInfo = fmt.Sprintf("%s (%s)", hostname, ipAddr)
|
||||
}
|
||||
|
||||
// 确保时间戳已设置
|
||||
if alarmInfo.CreatedAt.IsZero() {
|
||||
alarmInfo.CreatedAt = time.Now()
|
||||
}
|
||||
if alarmInfo.UpdatedAt.IsZero() {
|
||||
alarmInfo.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// 将AlarmInfo序列化为JSON
|
||||
jsonData, err := json.Marshal(alarmInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal AlarmInfo to json: %w", err)
|
||||
}
|
||||
|
||||
t.jsonData = jsonData
|
||||
t.alarm = alarmInfo
|
||||
}
|
||||
|
||||
t.SetRetry(t.conf.RetryTimes, t.conf.RetryInterval)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WebHookTask) Run() error {
|
||||
func (t *WebHookTask) Go() error {
|
||||
// 检查是否需要保存告警到数据库
|
||||
var dbID uint
|
||||
if t.conf.SaveAlarm && t.plugin.DB != nil {
|
||||
// 默认 IsSent 为 false
|
||||
t.alarm.IsSent = false
|
||||
if err := t.plugin.DB.Create(&t.alarm).Error; err != nil {
|
||||
t.plugin.Error("保存告警到数据库失败", "error", err)
|
||||
} else {
|
||||
dbID = t.alarm.ID
|
||||
t.plugin.Info(""+
|
||||
"", "id", dbID)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(t.conf.Method, t.conf.URL, bytes.NewBuffer(t.jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -440,41 +491,51 @@ func (t *WebHookTask) Run() error {
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.plugin.Error("webhook request failed", "error", err)
|
||||
t.plugin.Error("webhook请求失败", "error", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 如果发送成功且已保存到数据库,则更新IsSent字段为true
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 && t.conf.SaveAlarm && t.plugin.DB != nil && dbID > 0 {
|
||||
t.alarm.IsSent = true
|
||||
if err := t.plugin.DB.Model(&AlarmInfo{}).Where("id = ?", dbID).Update("is_sent", true).Error; err != nil {
|
||||
t.plugin.Error("更新告警发送状态失败", "error", err)
|
||||
} else {
|
||||
t.plugin.Info("告警发送状态已更新", "id", dbID, "is_sent", true)
|
||||
}
|
||||
return task.ErrTaskComplete
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return task.ErrTaskComplete
|
||||
}
|
||||
|
||||
err = fmt.Errorf("webhook request failed with status: %d", resp.StatusCode)
|
||||
t.plugin.Error("webhook response error", "status", resp.StatusCode)
|
||||
err = fmt.Errorf("webhook请求失败,状态码:%d", resp.StatusCode)
|
||||
t.plugin.Error("webhook响应错误", "状态码", resp.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Plugin) SendWebhook(hookType config.HookType, conf config.Webhook, data any) *task.Task {
|
||||
func (p *Plugin) SendWebhook(conf config.Webhook, data any) *task.Task {
|
||||
webhookTask := &WebHookTask{
|
||||
plugin: p,
|
||||
hookType: hookType,
|
||||
conf: &conf,
|
||||
data: data,
|
||||
plugin: p,
|
||||
conf: conf,
|
||||
data: data,
|
||||
}
|
||||
return p.AddTask(webhookTask)
|
||||
return webHookQueueTask.AddTask(webhookTask)
|
||||
}
|
||||
|
||||
// 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 +547,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 +592,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)
|
||||
@@ -556,6 +617,7 @@ func (p *Plugin) OnSubscribe(streamPath string, args url.Values) {
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
func (p *Plugin) PublishWithConfig(ctx context.Context, streamPath string, conf config.Publish) (publisher *Publisher, err error) {
|
||||
publisher = createPublisher(p, streamPath, conf)
|
||||
if p.config.EnableAuth && publisher.Type == PublishTypeServer {
|
||||
@@ -577,10 +639,25 @@ func (p *Plugin) PublishWithConfig(ctx context.Context, streamPath string, conf
|
||||
}
|
||||
err = p.Server.Streams.AddTask(publisher, ctx).WaitStarted()
|
||||
if err == nil {
|
||||
publisher.OnDispose(func() {
|
||||
p.sendPublishEndWebhook(publisher)
|
||||
})
|
||||
p.sendPublishWebhook(publisher)
|
||||
if sender, webhook := p.getHookSender(config.HookOnPublishEnd); sender != nil {
|
||||
publisher.OnDispose(func() {
|
||||
alarmInfo := AlarmInfo{
|
||||
AlarmName: string(config.HookOnPublishEnd),
|
||||
AlarmDesc: publisher.StopReason().Error(),
|
||||
AlarmType: config.AlarmPublishOffline,
|
||||
StreamPath: publisher.StreamPath,
|
||||
}
|
||||
sender(webhook, alarmInfo)
|
||||
})
|
||||
}
|
||||
if sender, webhook := p.getHookSender(config.HookOnPublishStart); sender != nil {
|
||||
alarmInfo := AlarmInfo{
|
||||
AlarmName: string(config.HookOnPublishStart),
|
||||
AlarmType: config.AlarmPublishRecover,
|
||||
StreamPath: publisher.StreamPath,
|
||||
}
|
||||
sender(webhook, alarmInfo)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -612,16 +689,33 @@ func (p *Plugin) SubscribeWithConfig(ctx context.Context, streamPath string, con
|
||||
if err == nil {
|
||||
select {
|
||||
case <-subscriber.waitPublishDone:
|
||||
err = subscriber.Publisher.WaitTrack()
|
||||
waitAudio := conf.WaitTrack == "all" || strings.Contains(conf.WaitTrack, "audio")
|
||||
waitVideo := conf.WaitTrack == "all" || strings.Contains(conf.WaitTrack, "video")
|
||||
err = subscriber.Publisher.WaitTrack(waitAudio, waitVideo)
|
||||
case <-subscriber.Done():
|
||||
err = subscriber.StopReason()
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
subscriber.OnDispose(func() {
|
||||
p.sendSubscribeEndWebhook(subscriber)
|
||||
})
|
||||
p.sendSubscribeWebhook(subscriber)
|
||||
if sender, webhook := p.getHookSender(config.HookOnSubscribeEnd); sender != nil {
|
||||
subscriber.OnDispose(func() {
|
||||
alarmInfo := AlarmInfo{
|
||||
AlarmName: string(config.HookOnSubscribeEnd),
|
||||
AlarmDesc: subscriber.StopReason().Error(),
|
||||
AlarmType: config.AlarmSubscribeOffline,
|
||||
StreamPath: subscriber.StreamPath,
|
||||
}
|
||||
sender(webhook, alarmInfo)
|
||||
})
|
||||
}
|
||||
if sender, webhook := p.getHookSender(config.HookOnSubscribeStart); sender != nil {
|
||||
alarmInfo := AlarmInfo{
|
||||
AlarmName: string(config.HookOnSubscribeStart),
|
||||
AlarmType: config.AlarmSubscribeRecover,
|
||||
StreamPath: subscriber.StreamPath,
|
||||
}
|
||||
sender(webhook, alarmInfo)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -631,7 +725,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 +733,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)
|
||||
}
|
||||
|
||||
@@ -691,10 +785,11 @@ func (p *Plugin) registerHandler(handlers map[string]http.HandlerFunc) {
|
||||
streamPath := r.PathValue("streamPath")
|
||||
t := r.PathValue("type")
|
||||
expire := r.URL.Query().Get("expire")
|
||||
if t == "publish" {
|
||||
switch t {
|
||||
case "publish":
|
||||
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
} else if t == "subscribe" {
|
||||
case "subscribe":
|
||||
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
}
|
||||
@@ -732,121 +827,27 @@ 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
|
||||
func (p *Plugin) getHookSender(hookType config.HookType) (sender func(webhook config.Webhook, data any) *task.Task, conf config.Webhook) {
|
||||
if p.config.Hook != nil {
|
||||
if _, ok := p.config.Hook[hookType]; ok {
|
||||
sender = p.SendWebhook
|
||||
conf = p.config.Hook[hookType]
|
||||
} else if _, ok := p.config.Hook[config.HookDefault]; ok {
|
||||
sender = p.SendWebhook
|
||||
conf = p.config.Hook[config.HookDefault]
|
||||
} else if p.Server.config.Hook != nil {
|
||||
if _, ok := p.Server.config.Hook[hookType]; ok {
|
||||
conf = p.config.Hook[hookType]
|
||||
sender = p.Server.SendWebhook
|
||||
} else if _, ok := p.Server.config.Hook[config.HookDefault]; ok {
|
||||
sender = p.Server.SendWebhook
|
||||
conf = p.config.Hook[config.HookDefault]
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
webhookData := map[string]interface{}{
|
||||
"event": "publish",
|
||||
"streamPath": pub.StreamPath,
|
||||
"args": pub.Args,
|
||||
"publishId": pub.ID,
|
||||
"remoteAddr": pub.RemoteAddr,
|
||||
"type": pub.Type,
|
||||
"pluginName": p.Meta.Name,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
p.SendWebhook(config.HookOnPublish, p.config.Hook[config.HookOnPublish], webhookData)
|
||||
if p.Server.config.Hook == nil {
|
||||
return
|
||||
}
|
||||
p.Server.SendWebhook(config.HookOnPublish, p.Server.config.Hook[config.HookOnPublish], webhookData)
|
||||
}
|
||||
|
||||
func (p *Plugin) sendPublishEndWebhook(pub *Publisher) {
|
||||
if p.config.Hook == nil {
|
||||
return
|
||||
}
|
||||
webhookData := map[string]interface{}{
|
||||
"event": "publish_end",
|
||||
"streamPath": pub.StreamPath,
|
||||
"publishId": pub.ID,
|
||||
"reason": pub.StopReason().Error(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
p.SendWebhook(config.HookOnPublishEnd, p.config.Hook[config.HookOnPublishEnd], webhookData)
|
||||
}
|
||||
|
||||
func (p *Plugin) sendSubscribeWebhook(sub *Subscriber) {
|
||||
if p.config.Hook == nil {
|
||||
return
|
||||
}
|
||||
webhookData := map[string]interface{}{
|
||||
"event": "subscribe",
|
||||
"streamPath": sub.StreamPath,
|
||||
"publishId": sub.Publisher.ID,
|
||||
"subscriberId": sub.ID,
|
||||
"remoteAddr": sub.RemoteAddr,
|
||||
"type": sub.Type,
|
||||
"args": sub.Args,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
p.SendWebhook(config.HookOnSubscribe, p.config.Hook[config.HookOnSubscribe], webhookData)
|
||||
}
|
||||
|
||||
func (p *Plugin) sendSubscribeEndWebhook(sub *Subscriber) {
|
||||
if p.config.Hook == nil {
|
||||
return
|
||||
}
|
||||
webhookData := map[string]interface{}{
|
||||
"event": "subscribe_end",
|
||||
"streamPath": sub.StreamPath,
|
||||
"subscriberId": sub.ID,
|
||||
"reason": sub.StopReason().Error(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
if sub.Publisher != nil {
|
||||
webhookData["publishId"] = sub.Publisher.ID
|
||||
}
|
||||
p.SendWebhook(config.HookOnSubscribeEnd, p.config.Hook[config.HookOnSubscribeEnd], webhookData)
|
||||
}
|
||||
|
||||
func (p *Plugin) sendServerKeepAliveWebhook() {
|
||||
if p.config.Hook == nil {
|
||||
return
|
||||
}
|
||||
s := p.Server
|
||||
webhookData := map[string]interface{}{
|
||||
"event": "server_keep_alive",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"streams": s.Streams.Length,
|
||||
"subscribers": s.Subscribers.Length,
|
||||
"publisherCount": s.Streams.Length,
|
||||
"subscriberCount": s.Subscribers.Length,
|
||||
"uptime": time.Since(s.StartTime).Seconds(),
|
||||
}
|
||||
p.SendWebhook(config.HookOnServerKeepAlive, p.config.Hook[config.HookOnServerKeepAlive], webhookData)
|
||||
}
|
||||
|
||||
type ServerKeepAliveTask struct {
|
||||
task.TickTask
|
||||
plugin *Plugin
|
||||
@@ -857,5 +858,25 @@ func (t *ServerKeepAliveTask) GetTickInterval() time.Duration {
|
||||
}
|
||||
|
||||
func (t *ServerKeepAliveTask) Tick(now any) {
|
||||
t.plugin.sendServerKeepAliveWebhook()
|
||||
sender, webhook := t.plugin.getHookSender(config.HookOnServerKeepAlive)
|
||||
if sender == nil {
|
||||
return
|
||||
}
|
||||
//s := t.plugin.Server
|
||||
alarmInfo := AlarmInfo{
|
||||
AlarmName: string(config.HookOnServerKeepAlive),
|
||||
AlarmType: config.AlarmKeepAliveOnline,
|
||||
StreamPath: "",
|
||||
}
|
||||
sender(webhook, alarmInfo)
|
||||
//webhookData := map[string]interface{}{
|
||||
// "event": config.HookOnServerKeepAlive,
|
||||
// "timestamp": time.Now().Unix(),
|
||||
// "streams": s.Streams.Length,
|
||||
// "subscribers": s.Subscribers.Length,
|
||||
// "publisherCount": s.Streams.Length,
|
||||
// "subscriberCount": s.Subscribers.Length,
|
||||
// "uptime": time.Since(s.StartTime).Seconds(),
|
||||
//}
|
||||
//sender(webhook, webhookData)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
### Install gRPC
|
||||
```shell
|
||||
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
|
||||
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
||||
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
```
|
||||
|
||||
### Install gRPC-Gateway
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
- Cursor
|
||||
### 安装gRPC
|
||||
```shell
|
||||
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
|
||||
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
||||
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
```
|
||||
|
||||
### 安装gRPC-Gateway
|
||||
|
||||
990
plugin/crontab/api.go
Normal file
990
plugin/crontab/api.go
Normal file
@@ -0,0 +1,990 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
cronpb "m7s.live/v5/plugin/crontab/pb"
|
||||
"m7s.live/v5/plugin/crontab/pkg"
|
||||
)
|
||||
|
||||
func (ct *CrontabPlugin) List(ctx context.Context, req *cronpb.ReqPlanList) (*cronpb.PlanResponseList, error) {
|
||||
if req.PageNum < 1 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 从内存中获取所有计划
|
||||
plans := ct.recordPlans.Items
|
||||
total := len(plans)
|
||||
|
||||
// 计算分页
|
||||
start := int(req.PageNum-1) * int(req.PageSize)
|
||||
end := start + int(req.PageSize)
|
||||
if start >= total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
// 获取当前页的数据
|
||||
pagePlans := plans[start:end]
|
||||
|
||||
data := make([]*cronpb.Plan, 0, len(pagePlans))
|
||||
for _, plan := range pagePlans {
|
||||
data = append(data, &cronpb.Plan{
|
||||
Id: uint32(plan.ID),
|
||||
Name: plan.Name,
|
||||
Enable: plan.Enable,
|
||||
CreateTime: timestamppb.New(plan.CreatedAt),
|
||||
UpdateTime: timestamppb.New(plan.UpdatedAt),
|
||||
Plan: plan.Plan,
|
||||
})
|
||||
}
|
||||
|
||||
return &cronpb.PlanResponseList{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
TotalCount: uint32(total),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) Add(ctx context.Context, req *cronpb.Plan) (*cronpb.Response, error) {
|
||||
// 参数验证
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "name is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Plan) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "plan is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查名称是否已存在
|
||||
var count int64
|
||||
if err := ct.DB.Model(&pkg.RecordPlan{}).Where("name = ?", req.Name).Count(&count).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "name already exists",
|
||||
}, nil
|
||||
}
|
||||
|
||||
plan := &pkg.RecordPlan{
|
||||
Name: req.Name,
|
||||
Plan: req.Plan,
|
||||
Enable: req.Enable,
|
||||
}
|
||||
|
||||
if err := ct.DB.Create(plan).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 添加到内存中
|
||||
ct.recordPlans.Add(plan)
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) Update(ctx context.Context, req *cronpb.Plan) (*cronpb.Response, error) {
|
||||
if req.Id == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "name is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Plan) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "plan is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingPlan pkg.RecordPlan
|
||||
if err := ct.DB.First(&existingPlan, req.Id).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查新名称是否与其他记录冲突
|
||||
var count int64
|
||||
if err := ct.DB.Model(&pkg.RecordPlan{}).Where("name = ? AND id != ?", req.Name, req.Id).Count(&count).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "name already exists",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 处理 enable 状态变更
|
||||
enableChanged := existingPlan.Enable != req.Enable
|
||||
|
||||
// 更新记录
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"plan": req.Plan,
|
||||
"enable": req.Enable,
|
||||
}
|
||||
|
||||
if err := ct.DB.Model(&existingPlan).Updates(updates).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 更新内存中的记录
|
||||
existingPlan.Name = req.Name
|
||||
existingPlan.Plan = req.Plan
|
||||
existingPlan.Enable = req.Enable
|
||||
ct.recordPlans.Set(&existingPlan)
|
||||
|
||||
// 处理 enable 状态变更后的操作
|
||||
if enableChanged {
|
||||
if req.Enable {
|
||||
// 从 false 变为 true,需要创建并启动新的定时任务
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{PlanID: existingPlan.ID}
|
||||
if err := ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
} else {
|
||||
// 为每个流创建定时任务
|
||||
for _, stream := range streams {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: &existingPlan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 从 true 变为 false,需要停止相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlan.ID == existingPlan.ID {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) Remove(ctx context.Context, req *cronpb.DeleteRequest) (*cronpb.Response, error) {
|
||||
if req.Id == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingPlan pkg.RecordPlan
|
||||
if err := ct.DB.First(&existingPlan, req.Id).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 先停止所有相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlan.ID == existingPlan.ID {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 执行软删除
|
||||
if err := ct.DB.Delete(&existingPlan).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 从内存中移除
|
||||
ct.recordPlans.RemoveByKey(existingPlan.ID)
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) ListRecordPlanStreams(ctx context.Context, req *cronpb.ReqPlanStreamList) (*cronpb.RecordPlanStreamResponseList, error) {
|
||||
if req.PageNum < 1 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
var total int64
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{}
|
||||
|
||||
// 构建查询条件
|
||||
query := ct.DB.Model(model).
|
||||
Scopes(
|
||||
pkg.ScopeRecordPlanID(uint(req.PlanId)),
|
||||
pkg.ScopeStreamPathLike(req.StreamPath),
|
||||
pkg.ScopeOrderByCreatedAtDesc(),
|
||||
)
|
||||
|
||||
result := query.Count(&total)
|
||||
if result.Error != nil {
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
offset := (req.PageNum - 1) * req.PageSize
|
||||
result = query.Offset(int(offset)).Limit(int(req.PageSize)).Find(&streams)
|
||||
|
||||
if result.Error != nil {
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
data := make([]*cronpb.PlanStream, 0, len(streams))
|
||||
for _, stream := range streams {
|
||||
data = append(data, &cronpb.PlanStream{
|
||||
PlanId: uint32(stream.PlanID),
|
||||
StreamPath: stream.StreamPath,
|
||||
Fragment: stream.Fragment,
|
||||
FilePath: stream.FilePath,
|
||||
CreatedAt: timestamppb.New(stream.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(stream.UpdatedAt),
|
||||
Enable: stream.Enable,
|
||||
})
|
||||
}
|
||||
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
TotalCount: uint32(total),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) AddRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 从内存中获取录制计划
|
||||
plan, ok := ct.recordPlans.Get(uint(req.PlanId))
|
||||
if !ok {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record plan not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的记录
|
||||
var count int64
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Model(&searchModel).Where(&searchModel).Count(&count).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record already exists",
|
||||
}, nil
|
||||
}
|
||||
|
||||
stream := &pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
Fragment: req.Fragment,
|
||||
FilePath: req.FilePath,
|
||||
Enable: req.Enable,
|
||||
RecordType: req.RecordType,
|
||||
}
|
||||
|
||||
if err := ct.DB.Create(stream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 如果计划是启用状态,创建并启动定时任务
|
||||
if plan.Enable {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: plan,
|
||||
RecordPlanStream: stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) UpdateRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingStream pkg.RecordPlanStream
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
existingStream.Fragment = req.Fragment
|
||||
existingStream.FilePath = req.FilePath
|
||||
existingStream.Enable = req.Enable
|
||||
existingStream.RecordType = req.RecordType
|
||||
|
||||
if err := ct.DB.Save(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 停止当前流相关的所有任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlanStream.StreamPath == req.StreamPath {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 查询所有关联此流的记录
|
||||
var streams []pkg.RecordPlanStream
|
||||
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 为每个启用的计划创建新的定时任务
|
||||
for _, stream := range streams {
|
||||
// 从内存中获取对应的计划
|
||||
plan, ok := ct.recordPlans.Get(stream.PlanID)
|
||||
if !ok {
|
||||
ct.Error("record plan not found in memory: %d", stream.PlanID)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果计划是启用状态,创建并启动定时任务
|
||||
if plan.Enable && stream.Enable {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: plan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) RemoveRecordPlanStream(ctx context.Context, req *cronpb.DeletePlanStreamRequest) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingStream pkg.RecordPlanStream
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 停止所有相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlanStream.StreamPath == req.StreamPath && crontab.RecordPlan.ID == uint(req.PlanId) {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 执行删除
|
||||
if err := ct.DB.Delete(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取周几的名称(0=周日,1=周一,...,6=周六)
|
||||
func getWeekdayName(weekday int) string {
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
return weekdays[weekday]
|
||||
}
|
||||
|
||||
// 获取周几的索引(0=周日,1=周一,...,6=周六)
|
||||
func getWeekdayIndex(weekdayName string) int {
|
||||
weekdays := map[string]int{
|
||||
"周日": 0, "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6,
|
||||
}
|
||||
return weekdays[weekdayName]
|
||||
}
|
||||
|
||||
// 获取下一个指定周几的日期
|
||||
func getNextDateForWeekday(now time.Time, targetWeekday int, location *time.Location) time.Time {
|
||||
nowWeekday := int(now.Weekday())
|
||||
daysToAdd := 0
|
||||
|
||||
if targetWeekday >= nowWeekday {
|
||||
daysToAdd = targetWeekday - nowWeekday
|
||||
} else {
|
||||
daysToAdd = 7 - (nowWeekday - targetWeekday)
|
||||
}
|
||||
|
||||
// 如果是同一天但当前时间已经过了最后的时间段,则推到下一周
|
||||
if daysToAdd == 0 {
|
||||
// 这里简化处理,直接加7天到下周同一天
|
||||
daysToAdd = 7
|
||||
}
|
||||
|
||||
return now.AddDate(0, 0, daysToAdd)
|
||||
}
|
||||
|
||||
// 计算计划中的所有时间段
|
||||
func calculateTimeSlots(plan string, now time.Time, location *time.Location) ([]*cronpb.TimeSlotInfo, error) {
|
||||
if len(plan) != 168 {
|
||||
return nil, fmt.Errorf("invalid plan format: length should be 168")
|
||||
}
|
||||
|
||||
var slots []*cronpb.TimeSlotInfo
|
||||
|
||||
// 按周几遍历(0=周日,1=周一,...,6=周六)
|
||||
for weekday := 0; weekday < 7; weekday++ {
|
||||
dayOffset := weekday * 24
|
||||
var startHour int = -1
|
||||
|
||||
// 遍历这一天的每个小时
|
||||
for hour := 0; hour <= 24; hour++ {
|
||||
// 如果到了一天的结尾或者当前小时状态为0
|
||||
isEndOfDay := hour == 24
|
||||
isHourOff := !isEndOfDay && plan[dayOffset+hour] == '0'
|
||||
|
||||
if isEndOfDay || isHourOff {
|
||||
// 如果之前有开始的时间段,现在结束了
|
||||
if startHour != -1 {
|
||||
// 计算下一个该周几的日期
|
||||
targetDate := getNextDateForWeekday(now, weekday, location)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), hour, 0, 0, 0, location)
|
||||
|
||||
// 转换为 UTC 时间
|
||||
startTs := timestamppb.New(startTime.UTC())
|
||||
endTs := timestamppb.New(endTime.UTC())
|
||||
|
||||
slots = append(slots, &cronpb.TimeSlotInfo{
|
||||
Start: startTs,
|
||||
End: endTs,
|
||||
Weekday: getWeekdayName(weekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, hour),
|
||||
})
|
||||
startHour = -1
|
||||
}
|
||||
} else if plan[dayOffset+hour] == '1' && startHour == -1 {
|
||||
// 找到新的开始时间
|
||||
startHour = hour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sort.Slice(slots, func(i, j int) bool {
|
||||
// 先按周几排序
|
||||
weekdayI := getWeekdayIndex(slots[i].Weekday)
|
||||
weekdayJ := getWeekdayIndex(slots[j].Weekday)
|
||||
|
||||
if weekdayI != weekdayJ {
|
||||
return weekdayI < weekdayJ
|
||||
}
|
||||
|
||||
// 同一天按开始时间排序
|
||||
return slots[i].Start.AsTime().Hour() < slots[j].Start.AsTime().Hour()
|
||||
})
|
||||
|
||||
return slots, nil
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
func getNextTimeSlotFromNow(plan string, now time.Time, location *time.Location) (*cronpb.TimeSlotInfo, error) {
|
||||
if len(plan) != 168 {
|
||||
return nil, fmt.Errorf("invalid plan format: length should be 168")
|
||||
}
|
||||
|
||||
// 将当前时间转换为本地时间
|
||||
localNow := now.In(location)
|
||||
currentWeekday := int(localNow.Weekday())
|
||||
currentHour := localNow.Hour()
|
||||
|
||||
// 检查是否在整点边界附近(前后30秒)
|
||||
isNearHourBoundary := localNow.Minute() == 59 && localNow.Second() >= 30 || localNow.Minute() == 0 && localNow.Second() <= 30
|
||||
|
||||
// 首先检查当前时间是否在某个时间段内
|
||||
dayOffset := currentWeekday * 24
|
||||
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
|
||||
// 找到当前小时所在的完整时间段
|
||||
startHour := currentHour
|
||||
// 向前查找时间段的开始
|
||||
for h := currentHour - 1; h >= 0; h-- {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
startHour = h
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 向后查找时间段的结束
|
||||
endHour := currentHour + 1
|
||||
for h := endHour; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
endHour = h + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已经接近当前时间段的结束
|
||||
isNearEndOfTimeSlot := currentHour == endHour-1 && localNow.Minute() >= 59 && localNow.Second() >= 30
|
||||
|
||||
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
|
||||
if isNearEndOfTimeSlot && isNearHourBoundary {
|
||||
// 继续查找下一个时间段
|
||||
} else {
|
||||
// 创建时间段
|
||||
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
// 如果当前时间已经接近或超过了结束时间,调整结束时间
|
||||
if localNow.After(endTime.Add(-30*time.Second)) || localNow.Equal(endTime) {
|
||||
// 继续查找下一个时间段
|
||||
} else {
|
||||
// 返回当前时间段
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(currentWeekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找下一个时间段
|
||||
// 先查找当天剩余时间
|
||||
for h := currentHour + 1; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(currentWeekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当天没有找到,则查找后续日期
|
||||
for d := 1; d <= 7; d++ {
|
||||
nextDay := (currentWeekday + d) % 7
|
||||
dayOffset := nextDay * 24
|
||||
|
||||
for h := 0; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 计算日期
|
||||
nextDate := localNow.AddDate(0, 0, d)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(nextDay),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) ParsePlanTime(ctx context.Context, req *cronpb.ParsePlanRequest) (*cronpb.ParsePlanResponse, error) {
|
||||
if len(req.Plan) != 168 {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 400,
|
||||
Message: "invalid plan format: length should be 168",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查字符串格式是否正确(只包含0和1)
|
||||
for i, c := range req.Plan {
|
||||
if c != '0' && c != '1' {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 400,
|
||||
Message: fmt.Sprintf("invalid character at position %d: %c (should be 0 or 1)", i, c),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有时间段
|
||||
slots, err := calculateTimeSlots(req.Plan, time.Now(), time.Local)
|
||||
if err != nil {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(req.Plan, time.Now(), time.Local)
|
||||
if err != nil {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Slots: slots,
|
||||
NextSlot: nextSlot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 辅助函数:构建任务状态信息
|
||||
func buildCrontabTaskInfo(crontab *Crontab, now time.Time) *cronpb.CrontabTaskInfo {
|
||||
// 基础任务信息
|
||||
taskInfo := &cronpb.CrontabTaskInfo{
|
||||
PlanId: uint32(crontab.RecordPlan.ID),
|
||||
PlanName: crontab.RecordPlan.Name,
|
||||
StreamPath: crontab.StreamPath,
|
||||
FilePath: crontab.FilePath,
|
||||
Fragment: crontab.Fragment,
|
||||
}
|
||||
|
||||
// 获取完整计划时间段列表
|
||||
if crontab.RecordPlan != nil && crontab.RecordPlan.Plan != "" {
|
||||
planSlots, err := calculateTimeSlots(crontab.RecordPlan.Plan, now, time.Local)
|
||||
if err == nil && planSlots != nil && len(planSlots) > 0 {
|
||||
taskInfo.PlanSlots = planSlots
|
||||
}
|
||||
}
|
||||
|
||||
return taskInfo
|
||||
}
|
||||
|
||||
// GetCrontabStatus 获取当前Crontab任务状态
|
||||
func (ct *CrontabPlugin) GetCrontabStatus(ctx context.Context, req *cronpb.CrontabStatusRequest) (*cronpb.CrontabStatusResponse, error) {
|
||||
response := &cronpb.CrontabStatusResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
RunningTasks: []*cronpb.CrontabTaskInfo{},
|
||||
NextTasks: []*cronpb.CrontabTaskInfo{},
|
||||
TotalRunning: 0,
|
||||
TotalPlanned: 0,
|
||||
}
|
||||
|
||||
// 获取当前正在运行的任务
|
||||
runningTasks := make([]*cronpb.CrontabTaskInfo, 0)
|
||||
nextTasks := make([]*cronpb.CrontabTaskInfo, 0)
|
||||
|
||||
// 如果只指定了流路径但未找到对应的任务,也返回该流的计划信息
|
||||
streamPathFound := false
|
||||
|
||||
// 遍历所有Crontab任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
// 如果指定了stream_path过滤条件,且不匹配,则跳过
|
||||
if req.StreamPath != "" && crontab.StreamPath != req.StreamPath {
|
||||
return true // 继续遍历
|
||||
}
|
||||
|
||||
// 标记已找到指定的流
|
||||
if req.StreamPath != "" {
|
||||
streamPathFound = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 构建基本任务信息
|
||||
taskInfo := buildCrontabTaskInfo(crontab, now)
|
||||
|
||||
// 检查是否正在录制
|
||||
if crontab.recording && crontab.currentSlot != nil {
|
||||
// 当前正在录制
|
||||
taskInfo.IsRecording = true
|
||||
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = timestamppb.New(crontab.currentSlot.Start)
|
||||
taskInfo.EndTime = timestamppb.New(crontab.currentSlot.End)
|
||||
|
||||
// 计算已运行时间和剩余时间
|
||||
elapsedDuration := now.Sub(crontab.currentSlot.Start)
|
||||
remainingDuration := crontab.currentSlot.End.Sub(now)
|
||||
taskInfo.ElapsedSeconds = uint32(elapsedDuration.Seconds())
|
||||
taskInfo.RemainingSeconds = uint32(remainingDuration.Seconds())
|
||||
|
||||
// 设置时间范围和周几
|
||||
startHour := crontab.currentSlot.Start.Hour()
|
||||
endHour := crontab.currentSlot.End.Hour()
|
||||
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
|
||||
taskInfo.Weekday = getWeekdayName(int(crontab.currentSlot.Start.Weekday()))
|
||||
|
||||
// 添加到正在运行的任务列表
|
||||
runningTasks = append(runningTasks, taskInfo)
|
||||
} else {
|
||||
// 获取下一个时间段
|
||||
nextSlot := crontab.getNextTimeSlot()
|
||||
if nextSlot != nil {
|
||||
// 设置下一个任务的信息
|
||||
taskInfo.IsRecording = false
|
||||
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = timestamppb.New(nextSlot.Start)
|
||||
taskInfo.EndTime = timestamppb.New(nextSlot.End)
|
||||
|
||||
// 计算等待时间
|
||||
waitingDuration := nextSlot.Start.Sub(now)
|
||||
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
|
||||
|
||||
// 设置时间范围和周几
|
||||
startHour := nextSlot.Start.Hour()
|
||||
endHour := nextSlot.End.Hour()
|
||||
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
|
||||
taskInfo.Weekday = getWeekdayName(int(nextSlot.Start.Weekday()))
|
||||
|
||||
// 添加到计划任务列表
|
||||
nextTasks = append(nextTasks, taskInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return true // 继续遍历
|
||||
})
|
||||
|
||||
// 如果指定了流路径但未找到对应的任务,查询数据库获取该流的计划信息
|
||||
if req.StreamPath != "" && !streamPathFound {
|
||||
// 查询与该流相关的所有计划
|
||||
var streams []pkg.RecordPlanStream
|
||||
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err == nil && len(streams) > 0 {
|
||||
for _, stream := range streams {
|
||||
// 获取对应的计划
|
||||
var plan pkg.RecordPlan
|
||||
if err := ct.DB.First(&plan, stream.PlanID).Error; err == nil && plan.Enable && stream.Enable {
|
||||
now := time.Now()
|
||||
|
||||
// 构建任务信息
|
||||
taskInfo := &cronpb.CrontabTaskInfo{
|
||||
PlanId: uint32(plan.ID),
|
||||
PlanName: plan.Name,
|
||||
StreamPath: stream.StreamPath,
|
||||
FilePath: stream.FilePath,
|
||||
Fragment: stream.Fragment,
|
||||
IsRecording: false,
|
||||
}
|
||||
|
||||
// 获取完整计划时间段列表
|
||||
planSlots, err := calculateTimeSlots(plan.Plan, now, time.Local)
|
||||
if err == nil && planSlots != nil && len(planSlots) > 0 {
|
||||
taskInfo.PlanSlots = planSlots
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(plan.Plan, now, time.Local)
|
||||
if err == nil && nextSlot != nil {
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = nextSlot.Start
|
||||
taskInfo.EndTime = nextSlot.End
|
||||
taskInfo.TimeRange = nextSlot.TimeRange
|
||||
taskInfo.Weekday = nextSlot.Weekday
|
||||
|
||||
// 计算等待时间
|
||||
waitingDuration := nextSlot.Start.AsTime().Sub(now)
|
||||
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
|
||||
|
||||
// 添加到计划任务列表
|
||||
nextTasks = append(nextTasks, taskInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按开始时间排序下一个任务列表
|
||||
sort.Slice(nextTasks, func(i, j int) bool {
|
||||
return nextTasks[i].StartTime.AsTime().Before(nextTasks[j].StartTime.AsTime())
|
||||
})
|
||||
|
||||
// 设置响应结果
|
||||
response.RunningTasks = runningTasks
|
||||
response.NextTasks = nextTasks
|
||||
response.TotalRunning = uint32(len(runningTasks))
|
||||
response.TotalPlanned = uint32(len(nextTasks))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
244
plugin/crontab/api_test.go
Normal file
244
plugin/crontab/api_test.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCalculateTimeSlots(t *testing.T) {
|
||||
// 测试案例:周五的凌晨和上午有开启时间段
|
||||
// 字符串中1的索引是120(0点),122(2点),123(3点),125(5点),130(10点),135(15点)
|
||||
// 000000000000000000000000 - 周日(0-23小时) - 全0
|
||||
// 000000000000000000000000 - 周一(24-47小时) - 全0
|
||||
// 000000000000000000000000 - 周二(48-71小时) - 全0
|
||||
// 000000000000000000000000 - 周三(72-95小时) - 全0
|
||||
// 000000000000000000000000 - 周四(96-119小时) - 全0
|
||||
// 101101000010000100000000 - 周五(120-143小时) - 0,2,3,5,10,15点开启
|
||||
// 000000000000000000000000 - 周六(144-167小时) - 全0
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
|
||||
now := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
|
||||
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(slots), "应该有5个时间段")
|
||||
|
||||
// 检查结果中的时间段(按实际解析结果排序)
|
||||
assert.Equal(t, "周五", slots[0].Weekday)
|
||||
assert.Equal(t, "10:00-11:00", slots[0].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[1].Weekday)
|
||||
assert.Equal(t, "15:00-16:00", slots[1].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[2].Weekday)
|
||||
assert.Equal(t, "00:00-01:00", slots[2].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[3].Weekday)
|
||||
assert.Equal(t, "02:00-04:00", slots[3].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[4].Weekday)
|
||||
assert.Equal(t, "05:00-06:00", slots[4].TimeRange)
|
||||
|
||||
// 打印出所有时间段,便于调试
|
||||
for i, slot := range slots {
|
||||
t.Logf("时间段 %d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextTimeSlotFromNow(t *testing.T) {
|
||||
// 测试案例:周五的凌晨和上午有开启时间段
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
|
||||
// 测试1: 当前是周一,下一个时间段应该是周五凌晨0点
|
||||
now1 := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
|
||||
nextSlot1, err := getNextTimeSlotFromNow(planStr, now1, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot1)
|
||||
assert.Equal(t, "周五", nextSlot1.Weekday)
|
||||
assert.Equal(t, "00:00-01:00", nextSlot1.TimeRange)
|
||||
|
||||
// 测试2: 当前是周五凌晨1点,下一个时间段应该是周五凌晨2点
|
||||
now2 := time.Date(2023, 5, 5, 1, 30, 0, 0, time.Local) // 周五凌晨1:30
|
||||
nextSlot2, err := getNextTimeSlotFromNow(planStr, now2, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot2)
|
||||
assert.Equal(t, "周五", nextSlot2.Weekday)
|
||||
assert.Equal(t, "02:00-04:00", nextSlot2.TimeRange)
|
||||
|
||||
// 测试3: 当前是周五凌晨3点,此时正在一个时间段内
|
||||
now3 := time.Date(2023, 5, 5, 3, 0, 0, 0, time.Local) // 周五凌晨3:00
|
||||
nextSlot3, err := getNextTimeSlotFromNow(planStr, now3, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot3)
|
||||
assert.Equal(t, "周五", nextSlot3.Weekday)
|
||||
assert.Equal(t, "02:00-04:00", nextSlot3.TimeRange)
|
||||
}
|
||||
|
||||
func TestParsePlanFromString(t *testing.T) {
|
||||
// 测试用户提供的案例:字符串的第36-41位表示周一的时间段
|
||||
// 这个案例中,对应周一的12点、14-15点、17点和22点开启
|
||||
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证解析结果
|
||||
var foundMondaySlots bool
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
foundMondaySlots = true
|
||||
t.Logf("找到周一时间段: %s", slot.TimeRange)
|
||||
}
|
||||
}
|
||||
assert.True(t, foundMondaySlots, "应该找到周一的时间段")
|
||||
|
||||
// 预期的周一时间段
|
||||
var mondaySlots []string
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
mondaySlots = append(mondaySlots, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否包含预期的时间段
|
||||
expectedSlots := []string{
|
||||
"12:00-13:00",
|
||||
"14:00-16:00",
|
||||
"17:00-18:00",
|
||||
"22:00-23:00",
|
||||
}
|
||||
|
||||
for _, expected := range expectedSlots {
|
||||
found := false
|
||||
for _, actual := range mondaySlots {
|
||||
if expected == actual {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "应该找到周一时间段:"+expected)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
if nextSlot != nil {
|
||||
t.Logf("下一个时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
}
|
||||
|
||||
// 手动计算字符串长度的辅助函数
|
||||
func TestCountStringLength(t *testing.T) {
|
||||
str1 := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
assert.Equal(t, 168, len(str1), "第一个测试字符串长度应为168")
|
||||
|
||||
str2 := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
assert.Equal(t, 168, len(str2), "第二个测试字符串长度应为168")
|
||||
}
|
||||
|
||||
// 测试用户提供的具体字符串
|
||||
func TestUserProvidedPlanString(t *testing.T) {
|
||||
// 用户提供的测试字符串
|
||||
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// 验证字符串长度
|
||||
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
|
||||
|
||||
// 解析时间段
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 打印所有时间段
|
||||
t.Log("所有时间段:")
|
||||
for i, slot := range slots {
|
||||
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if nextSlot != nil {
|
||||
t.Logf("下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
|
||||
// 验证周一的时间段
|
||||
var mondaySlots []string
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
mondaySlots = append(mondaySlots, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
// 预期周一应该有这些时间段
|
||||
expectedMondaySlots := []string{
|
||||
"12:00-13:00",
|
||||
"14:00-16:00",
|
||||
"17:00-18:00",
|
||||
"22:00-23:00",
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expectedMondaySlots), len(mondaySlots), "周一时间段数量不匹配")
|
||||
|
||||
for i, expected := range expectedMondaySlots {
|
||||
if i < len(mondaySlots) {
|
||||
t.Logf("期望周一时间段 %s, 实际是 %s", expected, mondaySlots[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户提供的第二个字符串
|
||||
func TestUserProvidedPlanString2(t *testing.T) {
|
||||
// 用户提供的第二个测试字符串
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000001011010100001000000000000000000000000100000000000000000000000010000000000000000000000001000000000000000000"
|
||||
|
||||
// 验证字符串长度
|
||||
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
|
||||
|
||||
// 解析时间段
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 打印所有时间段并按周几分组
|
||||
weekdaySlots := make(map[string][]string)
|
||||
for _, slot := range slots {
|
||||
weekdaySlots[slot.Weekday] = append(weekdaySlots[slot.Weekday], slot.TimeRange)
|
||||
}
|
||||
|
||||
t.Log("所有时间段(按周几分组):")
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
for _, weekday := range weekdays {
|
||||
if timeRanges, ok := weekdaySlots[weekday]; ok {
|
||||
t.Logf("%s: %v", weekday, timeRanges)
|
||||
}
|
||||
}
|
||||
|
||||
// 打印所有时间段的详细信息
|
||||
t.Log("\n所有时间段详细信息:")
|
||||
for i, slot := range slots {
|
||||
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if nextSlot != nil {
|
||||
t.Logf("\n下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
}
|
||||
422
plugin/crontab/crontab.go
Normal file
422
plugin/crontab/crontab.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/plugin/crontab/pkg"
|
||||
)
|
||||
|
||||
// 计划时间段
|
||||
type TimeSlot struct {
|
||||
Start time.Time // 开始时间
|
||||
End time.Time // 结束时间
|
||||
}
|
||||
|
||||
// Crontab 定时任务调度器
|
||||
type Crontab struct {
|
||||
task.Job
|
||||
ctp *CrontabPlugin
|
||||
*pkg.RecordPlan
|
||||
*pkg.RecordPlanStream
|
||||
|
||||
stop chan struct{}
|
||||
running bool
|
||||
location *time.Location
|
||||
timer *time.Timer
|
||||
currentSlot *TimeSlot // 当前执行的时间段
|
||||
recording bool // 是否正在录制
|
||||
}
|
||||
|
||||
func (cron *Crontab) GetKey() string {
|
||||
return strconv.Itoa(int(cron.PlanID)) + "_" + cron.StreamPath
|
||||
}
|
||||
|
||||
// 初始化
|
||||
func (cron *Crontab) Start() (err error) {
|
||||
cron.Info("crontab plugin start")
|
||||
if cron.running {
|
||||
return // 已经运行中,不重复启动
|
||||
}
|
||||
|
||||
// 初始化必要字段
|
||||
if cron.stop == nil {
|
||||
cron.stop = make(chan struct{})
|
||||
}
|
||||
if cron.location == nil {
|
||||
cron.location = time.Local
|
||||
}
|
||||
|
||||
cron.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 阻塞运行
|
||||
func (cron *Crontab) Run() (err error) {
|
||||
cron.Info("crontab plugin is running")
|
||||
// 初始化必要字段
|
||||
if cron.stop == nil {
|
||||
cron.stop = make(chan struct{})
|
||||
}
|
||||
if cron.location == nil {
|
||||
cron.location = time.Local
|
||||
}
|
||||
|
||||
cron.Info("调度器启动")
|
||||
|
||||
for {
|
||||
// 获取当前时间
|
||||
now := time.Now().In(cron.location)
|
||||
|
||||
// 首先检查是否需要立即执行操作(如停止录制)
|
||||
if cron.recording && cron.currentSlot != nil &&
|
||||
(now.Equal(cron.currentSlot.End) || now.After(cron.currentSlot.End)) {
|
||||
cron.stopRecording()
|
||||
continue
|
||||
}
|
||||
|
||||
// 确定下一个事件
|
||||
var nextEvent time.Time
|
||||
var isStartEvent bool
|
||||
|
||||
if cron.recording {
|
||||
// 如果正在录制,下一个事件是结束时间
|
||||
nextEvent = cron.currentSlot.End
|
||||
isStartEvent = false
|
||||
} else {
|
||||
// 如果没有录制,计算下一个开始时间
|
||||
nextSlot := cron.getNextTimeSlot()
|
||||
if nextSlot == nil {
|
||||
// 无法确定下次执行时间,使用默认间隔
|
||||
cron.timer = time.NewTimer(1 * time.Hour)
|
||||
cron.Info("无有效计划,等待1小时后重试")
|
||||
|
||||
// 等待定时器或停止信号
|
||||
select {
|
||||
case <-cron.timer.C:
|
||||
continue // 继续循环
|
||||
case <-cron.stop:
|
||||
// 停止调度器
|
||||
if cron.timer != nil {
|
||||
cron.timer.Stop()
|
||||
}
|
||||
cron.Info("调度器停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cron.currentSlot = nextSlot
|
||||
nextEvent = nextSlot.Start
|
||||
isStartEvent = true
|
||||
|
||||
// 如果已过开始时间,立即开始录制
|
||||
if now.Equal(nextEvent) || now.After(nextEvent) {
|
||||
cron.startRecording()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 计算等待时间
|
||||
waitDuration := nextEvent.Sub(now)
|
||||
|
||||
// 如果等待时间为负,立即执行
|
||||
if waitDuration <= 0 {
|
||||
if isStartEvent {
|
||||
cron.startRecording()
|
||||
} else {
|
||||
cron.stopRecording()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 设置定时器
|
||||
timer := time.NewTimer(waitDuration)
|
||||
|
||||
if isStartEvent {
|
||||
cron.Info("下次开始时间: ", nextEvent, "等待时间:", waitDuration)
|
||||
} else {
|
||||
cron.Info("下次结束时间: ", nextEvent, " 等待时间:", waitDuration)
|
||||
}
|
||||
|
||||
// 等待定时器或停止信号
|
||||
select {
|
||||
case now = <-timer.C:
|
||||
// 更新当前时间为定时器触发时间
|
||||
now = now.In(cron.location)
|
||||
|
||||
// 执行任务
|
||||
if isStartEvent {
|
||||
cron.startRecording()
|
||||
} else {
|
||||
cron.stopRecording()
|
||||
}
|
||||
|
||||
case <-cron.stop:
|
||||
// 停止调度器
|
||||
timer.Stop()
|
||||
cron.Info("调度器停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止
|
||||
func (cron *Crontab) Dispose() (err error) {
|
||||
if cron.running {
|
||||
cron.stop <- struct{}{}
|
||||
cron.running = false
|
||||
if cron.timer != nil {
|
||||
cron.timer.Stop()
|
||||
}
|
||||
|
||||
// 如果还在录制,停止录制
|
||||
if cron.recording {
|
||||
cron.stopRecording()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
func (cron *Crontab) getNextTimeSlot() *TimeSlot {
|
||||
if cron.RecordPlan == nil || !cron.RecordPlan.Enable || cron.RecordPlan.Plan == "" {
|
||||
return nil // 无有效计划
|
||||
}
|
||||
|
||||
plan := cron.RecordPlan.Plan
|
||||
if len(plan) != 168 {
|
||||
cron.Error("无效的计划格式: %s, 长度应为168", plan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用当地时间
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Debug("当前本地时间: %v, 星期%d, 小时%d", now.Format("2006-01-02 15:04:05"), now.Weekday(), now.Hour())
|
||||
|
||||
// 当前小时
|
||||
currentWeekday := int(now.Weekday())
|
||||
currentHour := now.Hour()
|
||||
|
||||
// 检查是否在整点边界附近(前后30秒)
|
||||
isNearHourBoundary := now.Minute() == 59 && now.Second() >= 30 || now.Minute() == 0 && now.Second() <= 30
|
||||
|
||||
// 首先检查当前时间是否在某个时间段内
|
||||
dayOffset := currentWeekday * 24
|
||||
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
|
||||
// 找到当前小时所在的完整时间段
|
||||
startHour := currentHour
|
||||
// 向前查找时间段的开始
|
||||
for h := currentHour - 1; h >= 0; h-- {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
startHour = h
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 向后查找时间段的结束
|
||||
endHour := currentHour + 1
|
||||
for h := endHour; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
endHour = h + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查我们是否已经接近当前时间段的结束
|
||||
isNearEndOfTimeSlot := currentHour == endHour-1 && now.Minute() == 59 && now.Second() >= 30
|
||||
|
||||
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
|
||||
if isNearEndOfTimeSlot && isNearHourBoundary {
|
||||
cron.Debug("接近当前时间段结束,准备查找下一个时间段")
|
||||
} else {
|
||||
// 创建时间段
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
// 如果当前时间已经接近或超过了结束时间,调整结束时间
|
||||
if now.After(endTime.Add(-30*time.Second)) || now.Equal(endTime) {
|
||||
cron.Debug("当前时间已接近或超过结束时间,尝试查找下一个时间段")
|
||||
} else {
|
||||
cron.Debug("当前已在有效时间段内: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找下一个时间段
|
||||
// 先查找当天剩余时间
|
||||
for h := currentHour + 1; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
cron.Debug("找到今天的有效时间段: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当天没有找到,则查找后续日期
|
||||
for d := 1; d <= 7; d++ {
|
||||
nextDay := (currentWeekday + d) % 7
|
||||
dayOffset := nextDay * 24
|
||||
|
||||
for h := 0; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 计算日期
|
||||
nextDate := now.AddDate(0, 0, d)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
cron.Debug("找到未来有效时间段: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cron.Debug("未找到有效的时间段")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 开始录制
|
||||
func (cron *Crontab) startRecording() {
|
||||
if cron.recording {
|
||||
return // 已经在录制了
|
||||
}
|
||||
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Info("开始录制任务: %s, 时间: %v, 计划结束时间: %v",
|
||||
cron.RecordPlan.Name, now, cron.currentSlot.End)
|
||||
|
||||
// 构造请求体
|
||||
reqBody := map[string]string{
|
||||
"fragment": cron.Fragment,
|
||||
"filePath": cron.FilePath,
|
||||
}
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
cron.Error("构造请求体失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 HTTP 地址
|
||||
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
|
||||
if addr == "" {
|
||||
addr = ":8080" // 使用默认端口
|
||||
}
|
||||
if addr[0] == ':' {
|
||||
addr = "localhost" + addr
|
||||
}
|
||||
|
||||
// 发送开始录制请求
|
||||
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/start/%s", addr, cron.StreamPath), "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
cron.Error("开始录制失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
cron.Error("开始录制失败,HTTP状态码: %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
cron.recording = true
|
||||
}
|
||||
|
||||
// 停止录制
|
||||
func (cron *Crontab) stopRecording() {
|
||||
if !cron.recording {
|
||||
return // 没有在录制
|
||||
}
|
||||
|
||||
// 立即记录当前时间并重置状态,避免重复调用
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Info("停止录制任务: %s, 时间: %v", cron.RecordPlan.Name, now)
|
||||
|
||||
// 先重置状态,避免循环中重复检测到停止条件
|
||||
wasRecording := cron.recording
|
||||
cron.recording = false
|
||||
savedSlot := cron.currentSlot
|
||||
cron.currentSlot = nil
|
||||
|
||||
// 获取 HTTP 地址
|
||||
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
|
||||
if addr == "" {
|
||||
addr = ":8080" // 使用默认端口
|
||||
}
|
||||
if addr[0] == ':' {
|
||||
addr = "localhost" + addr
|
||||
}
|
||||
|
||||
// 发送停止录制请求
|
||||
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/stop/%s", addr, cron.StreamPath), "application/json", nil)
|
||||
if err != nil {
|
||||
cron.Error("停止录制失败: %v", err)
|
||||
// 如果请求失败,恢复状态以便下次重试
|
||||
if wasRecording {
|
||||
cron.recording = true
|
||||
cron.currentSlot = savedSlot
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
cron.Error("停止录制失败,HTTP状态码: %d", resp.StatusCode)
|
||||
// 如果请求失败,恢复状态以便下次重试
|
||||
if wasRecording {
|
||||
cron.recording = true
|
||||
cron.currentSlot = savedSlot
|
||||
}
|
||||
}
|
||||
}
|
||||
71
plugin/crontab/index.go
Normal file
71
plugin/crontab/index.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"m7s.live/v5/pkg/util"
|
||||
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/plugin/crontab/pb"
|
||||
"m7s.live/v5/plugin/crontab/pkg"
|
||||
)
|
||||
|
||||
type CrontabPlugin struct {
|
||||
m7s.Plugin
|
||||
pb.UnimplementedApiServer
|
||||
crontabs util.Collection[string, *Crontab]
|
||||
recordPlans util.Collection[uint, *pkg.RecordPlan]
|
||||
}
|
||||
|
||||
var _ = m7s.InstallPlugin[CrontabPlugin](m7s.PluginMeta{
|
||||
ServiceDesc: &pb.Api_ServiceDesc,
|
||||
RegisterGRPCHandler: pb.RegisterApiHandler,
|
||||
})
|
||||
|
||||
func (ct *CrontabPlugin) OnInit() (err error) {
|
||||
if ct.DB == nil {
|
||||
ct.Error("DB is nil")
|
||||
} else {
|
||||
err = ct.DB.AutoMigrate(&pkg.RecordPlan{}, &pkg.RecordPlanStream{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto migrate tables error: %v", err)
|
||||
}
|
||||
ct.Info("init database success")
|
||||
|
||||
// 查询所有录制计划
|
||||
var plans []pkg.RecordPlan
|
||||
if err = ct.DB.Find(&plans).Error; err != nil {
|
||||
return fmt.Errorf("query record plans error: %v", err)
|
||||
}
|
||||
|
||||
// 遍历所有计划
|
||||
for _, plan := range plans {
|
||||
// 将计划存入 recordPlans 集合
|
||||
ct.recordPlans.Add(&plan)
|
||||
|
||||
// 如果计划已启用,查询对应的流信息并创建定时任务
|
||||
if plan.Enable {
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{PlanID: plan.ID}
|
||||
if err = ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 为每个流创建定时任务
|
||||
for _, stream := range streams {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: &plan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
1321
plugin/crontab/pb/crontab.pb.go
Normal file
1321
plugin/crontab/pb/crontab.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
831
plugin/crontab/pb/crontab.pb.gw.go
Normal file
831
plugin/crontab/pb/crontab.pb.gw.go
Normal file
@@ -0,0 +1,831 @@
|
||||
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
|
||||
// source: crontab.proto
|
||||
|
||||
/*
|
||||
Package pb is a reverse proxy.
|
||||
|
||||
It translates gRPC into RESTful JSON APIs.
|
||||
*/
|
||||
package pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Suppress "imported and not used" errors
|
||||
var (
|
||||
_ codes.Code
|
||||
_ io.Reader
|
||||
_ status.Status
|
||||
_ = errors.New
|
||||
_ = runtime.String
|
||||
_ = utilities.NewDoubleArray
|
||||
_ = metadata.Join
|
||||
)
|
||||
|
||||
var filter_Api_List_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_List_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_List_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.List(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_Add_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq Plan
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.Add(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_Add_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq Plan
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.Add(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_Update_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq Plan
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
|
||||
}
|
||||
protoReq.Id, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
|
||||
}
|
||||
msg, err := client.Update(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_Update_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq Plan
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
|
||||
}
|
||||
protoReq.Id, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
|
||||
}
|
||||
msg, err := server.Update(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_Remove_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeleteRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
|
||||
}
|
||||
protoReq.Id, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
|
||||
}
|
||||
msg, err := client.Remove(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_Remove_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeleteRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
|
||||
}
|
||||
protoReq.Id, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
|
||||
}
|
||||
msg, err := server.Remove(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
var filter_Api_ListRecordPlanStreams_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanStreamList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.ListRecordPlanStreams(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanStreamList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.ListRecordPlanStreams(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.AddRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.AddRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.UpdateRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.UpdateRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeletePlanStreamRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["planId"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
|
||||
}
|
||||
protoReq.PlanId, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", 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.RemoveRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeletePlanStreamRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["planId"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
|
||||
}
|
||||
protoReq.PlanId, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", 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.RemoveRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ParsePlanRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
val, ok := pathParams["plan"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
|
||||
}
|
||||
protoReq.Plan, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
|
||||
}
|
||||
msg, err := client.ParsePlanTime(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ParsePlanRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
val, ok := pathParams["plan"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
|
||||
}
|
||||
protoReq.Plan, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
|
||||
}
|
||||
msg, err := server.ParsePlanTime(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
var filter_Api_GetCrontabStatus_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CrontabStatusRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.GetCrontabStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CrontabStatusRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.GetCrontabStatus(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(http.MethodGet, pattern_Api_List_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/List", runtime.WithHTTPPathPattern("/plan/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_List_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_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Add_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Add", runtime.WithHTTPPathPattern("/plan/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_Add_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_Add_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Update_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Update", runtime.WithHTTPPathPattern("/plan/api/update/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_Update_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_Update_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Remove_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Remove", runtime.WithHTTPPathPattern("/plan/api/remove/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_Remove_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_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_ListRecordPlanStreams_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_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_AddRecordPlanStream_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_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_UpdateRecordPlanStream_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_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_RemoveRecordPlanStream_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_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_ParsePlanTime_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_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_GetCrontabStatus_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_GetCrontabStatus_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.NewClient(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
return RegisterApiHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterApiHandler registers the http handlers for service Api to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterApiHandlerClient(ctx, mux, NewApiClient(conn))
|
||||
}
|
||||
|
||||
// RegisterApiHandlerClient registers the http handlers for service Api
|
||||
// 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. This client ignores the HTTP middlewares.
|
||||
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
|
||||
mux.Handle(http.MethodGet, pattern_Api_List_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/List", runtime.WithHTTPPathPattern("/plan/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_List_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_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Add_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Add", runtime.WithHTTPPathPattern("/plan/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_Add_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_Add_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Update_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Update", runtime.WithHTTPPathPattern("/plan/api/update/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_Update_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_Update_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_Remove_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Remove", runtime.WithHTTPPathPattern("/plan/api/remove/{id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_Remove_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_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_ListRecordPlanStreams_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_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_AddRecordPlanStream_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_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_UpdateRecordPlanStream_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_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_RemoveRecordPlanStream_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_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_ParsePlanTime_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_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_GetCrontabStatus_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_GetCrontabStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_Api_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "list"}, ""))
|
||||
pattern_Api_Add_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "add"}, ""))
|
||||
pattern_Api_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "update", "id"}, ""))
|
||||
pattern_Api_Remove_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "remove", "id"}, ""))
|
||||
pattern_Api_ListRecordPlanStreams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "list"}, ""))
|
||||
pattern_Api_AddRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "add"}, ""))
|
||||
pattern_Api_UpdateRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "update"}, ""))
|
||||
pattern_Api_RemoveRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 3, 0, 4, 1, 5, 4}, []string{"planstream", "api", "remove", "planId", "streamPath"}, ""))
|
||||
pattern_Api_ParsePlanTime_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 0}, []string{"plan", "api", "parse"}, ""))
|
||||
pattern_Api_GetCrontabStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"crontab", "api", "status"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_Api_List_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Add_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Update_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Remove_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_ListRecordPlanStreams_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_AddRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_UpdateRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_RemoveRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_ParsePlanTime_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_GetCrontabStatus_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
190
plugin/crontab/pb/crontab.proto
Normal file
190
plugin/crontab/pb/crontab.proto
Normal file
@@ -0,0 +1,190 @@
|
||||
syntax = "proto3";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
package crontab;
|
||||
option go_package="m7s.live/v5/plugin/crontab/pb";
|
||||
|
||||
service api {
|
||||
rpc List (ReqPlanList) returns (PlanResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/plan/api/list"
|
||||
};
|
||||
}
|
||||
rpc Add (Plan) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/plan/api/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc Update (Plan) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/plan/api/update/{id}"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc Remove (DeleteRequest) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/plan/api/remove/{id}"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RecordPlanStream 相关接口
|
||||
rpc ListRecordPlanStreams (ReqPlanStreamList) returns (RecordPlanStreamResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/planstream/api/list"
|
||||
};
|
||||
}
|
||||
rpc AddRecordPlanStream (PlanStream) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc UpdateRecordPlanStream (PlanStream) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/update"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc RemoveRecordPlanStream (DeletePlanStreamRequest) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/remove/{planId}/{streamPath=**}"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// 解析计划字符串,返回时间段信息
|
||||
rpc ParsePlanTime (ParsePlanRequest) returns (ParsePlanResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/plan/api/parse/{plan}"
|
||||
};
|
||||
}
|
||||
|
||||
// 获取当前Crontab任务状态
|
||||
rpc GetCrontabStatus (CrontabStatusRequest) returns (CrontabStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/crontab/api/status"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message PlanResponseList {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint32 totalCount = 3;
|
||||
uint32 pageNum = 4;
|
||||
uint32 pageSize = 5;
|
||||
repeated Plan data = 6;
|
||||
}
|
||||
|
||||
message Plan {
|
||||
uint32 id = 1;
|
||||
string name = 2;
|
||||
bool enable = 3;
|
||||
google.protobuf.Timestamp createTime = 4;
|
||||
google.protobuf.Timestamp updateTime = 5;
|
||||
string plan = 6;
|
||||
}
|
||||
|
||||
message ReqPlanList {
|
||||
uint32 pageNum = 1;
|
||||
uint32 pageSize = 2;
|
||||
}
|
||||
|
||||
message DeleteRequest {
|
||||
uint32 id = 1;
|
||||
}
|
||||
|
||||
message Response {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
// RecordPlanStream 相关消息定义
|
||||
message PlanStream {
|
||||
uint32 planId = 1;
|
||||
string stream_path = 2;
|
||||
string fragment = 3;
|
||||
string filePath = 4;
|
||||
string record_type = 5; // 录制类型,例如 "mp4", "flv"
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
bool enable = 8; // 是否启用该录制流
|
||||
}
|
||||
|
||||
message ReqPlanStreamList {
|
||||
uint32 pageNum = 1;
|
||||
uint32 pageSize = 2;
|
||||
uint32 planId = 3; // 可选的按录制计划ID筛选
|
||||
string stream_path = 4; // 可选的按流路径筛选
|
||||
}
|
||||
|
||||
message RecordPlanStreamResponseList {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint32 totalCount = 3;
|
||||
uint32 pageNum = 4;
|
||||
uint32 pageSize = 5;
|
||||
repeated PlanStream data = 6;
|
||||
}
|
||||
|
||||
message DeletePlanStreamRequest {
|
||||
uint32 planId = 1;
|
||||
string streamPath = 2;
|
||||
}
|
||||
|
||||
// 解析计划请求
|
||||
message ParsePlanRequest {
|
||||
string plan = 1; // 168位的0/1字符串,表示一周的每个小时是否录制
|
||||
}
|
||||
|
||||
// 时间段信息
|
||||
message TimeSlotInfo {
|
||||
google.protobuf.Timestamp start = 1; // 开始时间
|
||||
google.protobuf.Timestamp end = 2; // 结束时间
|
||||
string weekday = 3; // 周几(例如:周一)
|
||||
string time_range = 4; // 时间范围(例如:09:00-10:00)
|
||||
}
|
||||
|
||||
// 解析计划响应
|
||||
message ParsePlanResponse {
|
||||
int32 code = 1; // 响应码
|
||||
string message = 2; // 响应消息
|
||||
repeated TimeSlotInfo slots = 3; // 所有计划的时间段
|
||||
TimeSlotInfo next_slot = 4; // 从当前时间开始的下一个时间段
|
||||
}
|
||||
|
||||
// 新增的消息定义
|
||||
// 获取Crontab状态请求
|
||||
message CrontabStatusRequest {
|
||||
// 可以为空,表示获取所有任务
|
||||
string stream_path = 1; // 可选,按流路径过滤
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
message CrontabTaskInfo {
|
||||
uint32 plan_id = 1; // 计划ID
|
||||
string plan_name = 2; // 计划名称
|
||||
string stream_path = 3; // 流路径
|
||||
bool is_recording = 4; // 是否正在录制
|
||||
google.protobuf.Timestamp start_time = 5; // 当前/下一个任务开始时间
|
||||
google.protobuf.Timestamp end_time = 6; // 当前/下一个任务结束时间
|
||||
string time_range = 7; // 时间范围(例如:09:00-10:00)
|
||||
string weekday = 8; // 周几(例如:周一)
|
||||
string file_path = 9; // 文件保存路径
|
||||
string fragment = 10; // 分片设置
|
||||
uint32 elapsed_seconds = 11; // 已运行时间(秒,仅对正在运行的任务有效)
|
||||
uint32 remaining_seconds = 12; // 剩余时间(秒)
|
||||
repeated TimeSlotInfo plan_slots = 13; // 完整的计划时间段列表
|
||||
}
|
||||
|
||||
// 获取Crontab状态响应
|
||||
message CrontabStatusResponse {
|
||||
int32 code = 1; // 响应码
|
||||
string message = 2; // 响应消息
|
||||
repeated CrontabTaskInfo running_tasks = 3; // 当前正在执行的任务列表
|
||||
repeated CrontabTaskInfo next_tasks = 4; // 下一个计划执行的任务列表
|
||||
uint32 total_running = 5; // 正在运行的任务总数
|
||||
uint32 total_planned = 6; // 计划中的任务总数
|
||||
}
|
||||
469
plugin/crontab/pb/crontab_grpc.pb.go
Normal file
469
plugin/crontab/pb/crontab_grpc.pb.go
Normal file
@@ -0,0 +1,469 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// source: crontab.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// 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.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Api_List_FullMethodName = "/crontab.api/List"
|
||||
Api_Add_FullMethodName = "/crontab.api/Add"
|
||||
Api_Update_FullMethodName = "/crontab.api/Update"
|
||||
Api_Remove_FullMethodName = "/crontab.api/Remove"
|
||||
Api_ListRecordPlanStreams_FullMethodName = "/crontab.api/ListRecordPlanStreams"
|
||||
Api_AddRecordPlanStream_FullMethodName = "/crontab.api/AddRecordPlanStream"
|
||||
Api_UpdateRecordPlanStream_FullMethodName = "/crontab.api/UpdateRecordPlanStream"
|
||||
Api_RemoveRecordPlanStream_FullMethodName = "/crontab.api/RemoveRecordPlanStream"
|
||||
Api_ParsePlanTime_FullMethodName = "/crontab.api/ParsePlanTime"
|
||||
Api_GetCrontabStatus_FullMethodName = "/crontab.api/GetCrontabStatus"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ApiClient interface {
|
||||
List(ctx context.Context, in *ReqPlanList, opts ...grpc.CallOption) (*PlanResponseList, error)
|
||||
Add(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
|
||||
Update(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
|
||||
Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*Response, error)
|
||||
// RecordPlanStream 相关接口
|
||||
ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error)
|
||||
AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
|
||||
UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
|
||||
RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error)
|
||||
// 解析计划字符串,返回时间段信息
|
||||
ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error)
|
||||
// 获取当前Crontab任务状态
|
||||
GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error)
|
||||
}
|
||||
|
||||
type apiClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
|
||||
return &apiClient{cc}
|
||||
}
|
||||
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqPlanList, opts ...grpc.CallOption) (*PlanResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PlanResponseList)
|
||||
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) Add(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_Add_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) Update(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_Update_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_Remove_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RecordPlanStreamResponseList)
|
||||
err := c.cc.Invoke(ctx, Api_ListRecordPlanStreams_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_AddRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_UpdateRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_RemoveRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ParsePlanResponse)
|
||||
err := c.cc.Invoke(ctx, Api_ParsePlanTime_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CrontabStatusResponse)
|
||||
err := c.cc.Invoke(ctx, Api_GetCrontabStatus_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
// All implementations must embed UnimplementedApiServer
|
||||
// for forward compatibility.
|
||||
type ApiServer interface {
|
||||
List(context.Context, *ReqPlanList) (*PlanResponseList, error)
|
||||
Add(context.Context, *Plan) (*Response, error)
|
||||
Update(context.Context, *Plan) (*Response, error)
|
||||
Remove(context.Context, *DeleteRequest) (*Response, error)
|
||||
// RecordPlanStream 相关接口
|
||||
ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error)
|
||||
AddRecordPlanStream(context.Context, *PlanStream) (*Response, error)
|
||||
UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error)
|
||||
RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error)
|
||||
// 解析计划字符串,返回时间段信息
|
||||
ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error)
|
||||
// 获取当前Crontab任务状态
|
||||
GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error)
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
// 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, *ReqPlanList) (*PlanResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) Add(context.Context, *Plan) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Add not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) Update(context.Context, *Plan) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Update not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) Remove(context.Context, *DeleteRequest) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRecordPlanStreams not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) AddRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ParsePlanTime not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCrontabStatus 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
|
||||
// result in compilation errors.
|
||||
type UnsafeApiServer interface {
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReqPlanList)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).List(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_List_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).List(ctx, req.(*ReqPlanList))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_Add_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Plan)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).Add(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_Add_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Add(ctx, req.(*Plan))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Plan)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).Update(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_Update_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Update(ctx, req.(*Plan))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeleteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).Remove(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_Remove_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Remove(ctx, req.(*DeleteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_ListRecordPlanStreams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReqPlanStreamList)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).ListRecordPlanStreams(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_ListRecordPlanStreams_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ListRecordPlanStreams(ctx, req.(*ReqPlanStreamList))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_AddRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PlanStream)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).AddRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_AddRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).AddRecordPlanStream(ctx, req.(*PlanStream))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_UpdateRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PlanStream)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).UpdateRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_UpdateRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).UpdateRecordPlanStream(ctx, req.(*PlanStream))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_RemoveRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeletePlanStreamRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).RemoveRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_RemoveRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).RemoveRecordPlanStream(ctx, req.(*DeletePlanStreamRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_ParsePlanTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ParsePlanRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).ParsePlanTime(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_ParsePlanTime_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ParsePlanTime(ctx, req.(*ParsePlanRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_GetCrontabStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CrontabStatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).GetCrontabStatus(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_GetCrontabStatus_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetCrontabStatus(ctx, req.(*CrontabStatusRequest))
|
||||
}
|
||||
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)
|
||||
var Api_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "crontab.api",
|
||||
HandlerType: (*ApiServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "List",
|
||||
Handler: _Api_List_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Add",
|
||||
Handler: _Api_Add_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Update",
|
||||
Handler: _Api_Update_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Remove",
|
||||
Handler: _Api_Remove_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRecordPlanStreams",
|
||||
Handler: _Api_ListRecordPlanStreams_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddRecordPlanStream",
|
||||
Handler: _Api_AddRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateRecordPlanStream",
|
||||
Handler: _Api_UpdateRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveRecordPlanStream",
|
||||
Handler: _Api_RemoveRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ParsePlanTime",
|
||||
Handler: _Api_ParsePlanTime_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetCrontabStatus",
|
||||
Handler: _Api_GetCrontabStatus_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "crontab.proto",
|
||||
}
|
||||
17
plugin/crontab/pkg/recordplan.go
Normal file
17
plugin/crontab/pkg/recordplan.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecordPlan 录制计划模型
|
||||
type RecordPlan struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"default:''"`
|
||||
Plan string `json:"plan" gorm:"type:text"`
|
||||
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
|
||||
}
|
||||
|
||||
func (r *RecordPlan) GetKey() uint {
|
||||
return r.ID
|
||||
}
|
||||
51
plugin/crontab/pkg/recordplanstream.go
Normal file
51
plugin/crontab/pkg/recordplanstream.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RecordPlanStream 录制计划流信息模型
|
||||
type RecordPlanStream struct {
|
||||
PlanID uint `json:"plan_id" gorm:"primaryKey;type:bigint;not null"` // 录制计划ID
|
||||
StreamPath string `json:"stream_path" gorm:"primaryKey;type:varchar(255)"`
|
||||
Fragment string `json:"fragment" gorm:"type:text"`
|
||||
FilePath string `json:"file_path" gorm:"type:varchar(255)"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
|
||||
RecordType string `json:"record_type" gorm:"type:varchar(255)"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (RecordPlanStream) TableName() string {
|
||||
return "record_plans_streams"
|
||||
}
|
||||
|
||||
// ScopeStreamPathLike 模糊查询 StreamPath
|
||||
func ScopeStreamPathLike(streamPath string) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if streamPath != "" {
|
||||
return db.Where("record_plans_streams.stream_path LIKE ?", "%"+streamPath+"%")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeOrderByCreatedAtDesc 按创建时间倒序
|
||||
func ScopeOrderByCreatedAtDesc() func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("record_plans_streams.created_at DESC")
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeRecordPlanID 按录制计划ID查询
|
||||
func ScopeRecordPlanID(recordPlanID uint) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if recordPlanID > 0 {
|
||||
return db.Where(&RecordPlanStream{PlanID: recordPlanID})
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
@@ -40,8 +41,17 @@ type consumer struct {
|
||||
}
|
||||
|
||||
type server struct {
|
||||
task.TickTask
|
||||
consumers []consumer
|
||||
consumersMutex sync.RWMutex
|
||||
data DataStorage
|
||||
lastPause uint32
|
||||
dataMutex sync.RWMutex
|
||||
lastConsumerID uint
|
||||
upgrader websocket.Upgrader
|
||||
prevSysTime float64
|
||||
prevUserTime float64
|
||||
myProcess *process.Process
|
||||
}
|
||||
|
||||
type SimplePair struct {
|
||||
@@ -75,99 +85,91 @@ const (
|
||||
maxCount int = 86400
|
||||
)
|
||||
|
||||
var (
|
||||
data DataStorage
|
||||
lastPause uint32
|
||||
mutex sync.RWMutex
|
||||
lastConsumerID uint
|
||||
s server
|
||||
upgrader = websocket.Upgrader{
|
||||
func (s *server) Start() error {
|
||||
var err error
|
||||
s.myProcess, err = process.NewProcess(int32(os.Getpid()))
|
||||
if err != nil {
|
||||
log.Printf("Failed to get process: %v", err)
|
||||
}
|
||||
// 初始化 WebSocket upgrader
|
||||
s.upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
prevSysTime float64
|
||||
prevUserTime float64
|
||||
myProcess *process.Process
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
myProcess, _ = process.NewProcess(int32(os.Getpid()))
|
||||
|
||||
// preallocate arrays in data, helps save on reallocations caused by append()
|
||||
// when maxCount is large
|
||||
data.BytesAllocated = make([]SimplePair, 0, maxCount)
|
||||
data.GcPauses = make([]SimplePair, 0, maxCount)
|
||||
data.CPUUsage = make([]CPUPair, 0, maxCount)
|
||||
data.Pprof = make([]PprofPair, 0, maxCount)
|
||||
|
||||
go s.gatherData()
|
||||
s.data.BytesAllocated = make([]SimplePair, 0, maxCount)
|
||||
s.data.GcPauses = make([]SimplePair, 0, maxCount)
|
||||
s.data.CPUUsage = make([]CPUPair, 0, maxCount)
|
||||
s.data.Pprof = make([]PprofPair, 0, maxCount)
|
||||
return s.TickTask.Start()
|
||||
}
|
||||
|
||||
func (s *server) gatherData() {
|
||||
timer := time.Tick(time.Second)
|
||||
func (s *server) GetTickInterval() time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
|
||||
for now := range timer {
|
||||
nowUnix := now.Unix()
|
||||
func (s *server) Tick(any) {
|
||||
now := time.Now()
|
||||
nowUnix := now.Unix()
|
||||
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
|
||||
u := update{
|
||||
Ts: nowUnix * 1000,
|
||||
Block: pprof.Lookup("block").Count(),
|
||||
Goroutine: pprof.Lookup("goroutine").Count(),
|
||||
Heap: pprof.Lookup("heap").Count(),
|
||||
Mutex: pprof.Lookup("mutex").Count(),
|
||||
Threadcreate: pprof.Lookup("threadcreate").Count(),
|
||||
}
|
||||
data.Pprof = append(data.Pprof, PprofPair{
|
||||
uint64(nowUnix) * 1000,
|
||||
u.Block,
|
||||
u.Goroutine,
|
||||
u.Heap,
|
||||
u.Mutex,
|
||||
u.Threadcreate,
|
||||
})
|
||||
|
||||
cpuTimes, err := myProcess.Times()
|
||||
if err != nil {
|
||||
cpuTimes = &cpu.TimesStat{}
|
||||
}
|
||||
|
||||
if prevUserTime != 0 {
|
||||
u.CPUUser = cpuTimes.User - prevUserTime
|
||||
u.CPUSys = cpuTimes.System - prevSysTime
|
||||
data.CPUUsage = append(data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
|
||||
}
|
||||
|
||||
prevUserTime = cpuTimes.User
|
||||
prevSysTime = cpuTimes.System
|
||||
|
||||
mutex.Lock()
|
||||
|
||||
bytesAllocated := ms.Alloc
|
||||
u.BytesAllocated = bytesAllocated
|
||||
data.BytesAllocated = append(data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
|
||||
if lastPause == 0 || lastPause != ms.NumGC {
|
||||
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
|
||||
u.GcPause = gcPause
|
||||
data.GcPauses = append(data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
|
||||
lastPause = ms.NumGC
|
||||
}
|
||||
|
||||
if len(data.BytesAllocated) > maxCount {
|
||||
data.BytesAllocated = data.BytesAllocated[len(data.BytesAllocated)-maxCount:]
|
||||
}
|
||||
|
||||
if len(data.GcPauses) > maxCount {
|
||||
data.GcPauses = data.GcPauses[len(data.GcPauses)-maxCount:]
|
||||
}
|
||||
|
||||
mutex.Unlock()
|
||||
|
||||
s.sendToConsumers(u)
|
||||
u := update{
|
||||
Ts: nowUnix * 1000,
|
||||
Block: pprof.Lookup("block").Count(),
|
||||
Goroutine: pprof.Lookup("goroutine").Count(),
|
||||
Heap: pprof.Lookup("heap").Count(),
|
||||
Mutex: pprof.Lookup("mutex").Count(),
|
||||
Threadcreate: pprof.Lookup("threadcreate").Count(),
|
||||
}
|
||||
s.data.Pprof = append(s.data.Pprof, PprofPair{
|
||||
uint64(nowUnix) * 1000,
|
||||
u.Block,
|
||||
u.Goroutine,
|
||||
u.Heap,
|
||||
u.Mutex,
|
||||
u.Threadcreate,
|
||||
})
|
||||
|
||||
cpuTimes, err := s.myProcess.Times()
|
||||
if err != nil {
|
||||
cpuTimes = &cpu.TimesStat{}
|
||||
}
|
||||
|
||||
if s.prevUserTime != 0 {
|
||||
u.CPUUser = cpuTimes.User - s.prevUserTime
|
||||
u.CPUSys = cpuTimes.System - s.prevSysTime
|
||||
s.data.CPUUsage = append(s.data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
|
||||
}
|
||||
|
||||
s.prevUserTime = cpuTimes.User
|
||||
s.prevSysTime = cpuTimes.System
|
||||
|
||||
s.dataMutex.Lock()
|
||||
|
||||
bytesAllocated := ms.Alloc
|
||||
u.BytesAllocated = bytesAllocated
|
||||
s.data.BytesAllocated = append(s.data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
|
||||
if s.lastPause == 0 || s.lastPause != ms.NumGC {
|
||||
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
|
||||
u.GcPause = gcPause
|
||||
s.data.GcPauses = append(s.data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
|
||||
s.lastPause = ms.NumGC
|
||||
}
|
||||
|
||||
if len(s.data.BytesAllocated) > maxCount {
|
||||
s.data.BytesAllocated = s.data.BytesAllocated[len(s.data.BytesAllocated)-maxCount:]
|
||||
}
|
||||
|
||||
if len(s.data.GcPauses) > maxCount {
|
||||
s.data.GcPauses = s.data.GcPauses[len(s.data.GcPauses)-maxCount:]
|
||||
}
|
||||
|
||||
s.dataMutex.Unlock()
|
||||
|
||||
s.sendToConsumers(u)
|
||||
}
|
||||
|
||||
func (s *server) sendToConsumers(u update) {
|
||||
@@ -203,10 +205,10 @@ func (s *server) addConsumer() consumer {
|
||||
s.consumersMutex.Lock()
|
||||
defer s.consumersMutex.Unlock()
|
||||
|
||||
lastConsumerID++
|
||||
s.lastConsumerID++
|
||||
|
||||
c := consumer{
|
||||
id: lastConsumerID,
|
||||
id: s.lastConsumerID,
|
||||
c: make(chan update),
|
||||
}
|
||||
|
||||
@@ -221,7 +223,7 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
lastPong time.Time
|
||||
)
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
@@ -268,9 +270,9 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
func (s *server) dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.dataMutex.RLock()
|
||||
defer s.dataMutex.RUnlock()
|
||||
|
||||
if e := r.ParseForm(); e != nil {
|
||||
log.Print("error parsing form")
|
||||
@@ -284,7 +286,7 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode(data)
|
||||
encoder.Encode(s.data)
|
||||
|
||||
fmt.Fprint(w, ")")
|
||||
}
|
||||
|
||||
219
plugin/debug/envcheck.go
Normal file
219
plugin/debug/envcheck.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package plugin_debug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/yaml.v3"
|
||||
"m7s.live/v5/pb"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
type EnvCheckResult struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"` // info, success, error, complete
|
||||
}
|
||||
|
||||
// 自定义系统信息响应结构体,用于 JSON 解析
|
||||
type SysInfoResponseJSON struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
StartTime string `json:"startTime"`
|
||||
LocalIP string `json:"localIP"`
|
||||
PublicIP string `json:"publicIP"`
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"goVersion"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
CPUs int32 `json:"cpus"`
|
||||
Plugins []struct {
|
||||
Name string `json:"name"`
|
||||
PushAddr []string `json:"pushAddr"`
|
||||
PlayAddr []string `json:"playAddr"`
|
||||
Description map[string]string `json:"description"`
|
||||
} `json:"plugins"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 插件配置响应结构体
|
||||
type PluginConfigResponse struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
File string `json:"file"`
|
||||
Modified string `json:"modified"`
|
||||
Merged string `json:"merged"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// TCP 配置结构体
|
||||
type TCPConfig struct {
|
||||
ListenAddr string `yaml:"listenaddr"`
|
||||
ListenAddrTLS string `yaml:"listenaddrtls"`
|
||||
}
|
||||
|
||||
// 插件配置结构体
|
||||
type PluginConfig struct {
|
||||
TCP TCPConfig `yaml:"tcp"`
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) EnvCheck(w http.ResponseWriter, r *http.Request) {
|
||||
// Get target URL from query parameter
|
||||
targetURL := r.URL.Query().Get("target")
|
||||
if targetURL == "" {
|
||||
r.URL.Path = "/static/envcheck.html"
|
||||
staticFSHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Create SSE connection
|
||||
util.NewSSE(w, r.Context(), func(sse *util.SSE) {
|
||||
// Function to send SSE messages
|
||||
sendMessage := func(message string, msgType string) {
|
||||
result := EnvCheckResult{
|
||||
Message: message,
|
||||
Type: msgType,
|
||||
}
|
||||
sse.WriteJSON(result)
|
||||
}
|
||||
|
||||
// Parse target URL
|
||||
_, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid URL: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we can connect to the target server
|
||||
sendMessage(fmt.Sprintf("Checking connection to %s...", targetURL), "info")
|
||||
|
||||
// Get system info from target server
|
||||
resp, err := http.Get(fmt.Sprintf("%s/api/sysinfo", targetURL))
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to connect to target server: %v", err), "error")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
sendMessage(fmt.Sprintf("Target server returned status code: %d", resp.StatusCode), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Read and parse system info
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to read response: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
var sysInfoJSON SysInfoResponseJSON
|
||||
if err := json.Unmarshal(body, &sysInfoJSON); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse system info: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert JSON response to protobuf response
|
||||
sysInfo := &pb.SysInfoResponse{
|
||||
Code: sysInfoJSON.Code,
|
||||
Message: sysInfoJSON.Message,
|
||||
Data: &pb.SysInfoData{
|
||||
LocalIP: sysInfoJSON.Data.LocalIP,
|
||||
PublicIP: sysInfoJSON.Data.PublicIP,
|
||||
Version: sysInfoJSON.Data.Version,
|
||||
GoVersion: sysInfoJSON.Data.GoVersion,
|
||||
Os: sysInfoJSON.Data.OS,
|
||||
Arch: sysInfoJSON.Data.Arch,
|
||||
Cpus: sysInfoJSON.Data.CPUs,
|
||||
},
|
||||
}
|
||||
|
||||
// Parse start time
|
||||
if startTime, err := time.Parse(time.RFC3339, sysInfoJSON.Data.StartTime); err == nil {
|
||||
sysInfo.Data.StartTime = timestamppb.New(startTime)
|
||||
}
|
||||
|
||||
// Convert plugins
|
||||
for _, pluginJSON := range sysInfoJSON.Data.Plugins {
|
||||
plugin := &pb.PluginInfo{
|
||||
Name: pluginJSON.Name,
|
||||
PushAddr: pluginJSON.PushAddr,
|
||||
PlayAddr: pluginJSON.PlayAddr,
|
||||
Description: pluginJSON.Description,
|
||||
}
|
||||
sysInfo.Data.Plugins = append(sysInfo.Data.Plugins, plugin)
|
||||
}
|
||||
|
||||
// Check each plugin's configuration
|
||||
for _, plugin := range sysInfo.Data.Plugins {
|
||||
// Get plugin configuration
|
||||
configResp, err := http.Get(fmt.Sprintf("%s/api/config/get/%s", targetURL, plugin.Name))
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
defer configResp.Body.Close()
|
||||
|
||||
if configResp.StatusCode != http.StatusOK {
|
||||
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: status code %d", plugin.Name, configResp.StatusCode), "error")
|
||||
continue
|
||||
}
|
||||
|
||||
var configRespJSON PluginConfigResponse
|
||||
if err := json.NewDecoder(configResp.Body).Decode(&configRespJSON); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse YAML configuration
|
||||
var config PluginConfig
|
||||
if err := yaml.Unmarshal([]byte(configRespJSON.Data.Merged), &config); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse YAML configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
// Check TCP configuration
|
||||
if config.TCP.ListenAddr != "" {
|
||||
host, port, err := net.SplitHostPort(config.TCP.ListenAddr)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid listenaddr format for plugin %s: %v", plugin.Name, err), "error")
|
||||
} else {
|
||||
sendMessage(fmt.Sprintf("Checking TCP listenaddr %s for plugin %s...", config.TCP.ListenAddr, plugin.Name), "info")
|
||||
// Try to establish TCP connection
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddr, plugin.Name, err), "error")
|
||||
} else {
|
||||
conn.Close()
|
||||
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is accessible", config.TCP.ListenAddr, plugin.Name), "success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.TCP.ListenAddrTLS != "" {
|
||||
host, port, err := net.SplitHostPort(config.TCP.ListenAddrTLS)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid listenaddrtls format for plugin %s: %v", plugin.Name, err), "error")
|
||||
} else {
|
||||
sendMessage(fmt.Sprintf("Checking TCP TLS listenaddr %s for plugin %s...", config.TCP.ListenAddrTLS, plugin.Name), "info")
|
||||
// Try to establish TCP connection
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddrTLS, plugin.Name, err), "error")
|
||||
} else {
|
||||
conn.Close()
|
||||
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is accessible", config.TCP.ListenAddrTLS, plugin.Name), "success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage("Environment check completed", "complete")
|
||||
})
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/exec" // 新增导入
|
||||
"runtime"
|
||||
runtimePPROF "runtime/pprof"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -32,13 +34,13 @@ type DebugPlugin struct {
|
||||
m7s.Plugin
|
||||
ProfileDuration time.Duration `default:"10s" desc:"profile持续时间"`
|
||||
Profile string `desc:"采集profile存储文件"`
|
||||
ChartPeriod time.Duration `default:"1s" desc:"图表更新周期"`
|
||||
Grfout string `default:"grf.out" desc:"grf输出文件"`
|
||||
|
||||
EnableChart bool `default:"true" desc:"是否启用图表功能"`
|
||||
// 添加缓存字段
|
||||
cpuProfileData *profile.Profile // 缓存 CPU Profile 数据
|
||||
cpuProfileOnce sync.Once // 确保只采集一次
|
||||
cpuProfileLock sync.Mutex // 保护缓存数据
|
||||
chartServer server
|
||||
}
|
||||
|
||||
type WriteToFile struct {
|
||||
@@ -70,6 +72,10 @@ func (p *DebugPlugin) OnInit() error {
|
||||
p.Info("cpu profile done")
|
||||
}()
|
||||
}
|
||||
if p.EnableChart {
|
||||
p.AddTask(&p.chartServer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,11 +104,11 @@ func (p *DebugPlugin) Charts_(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Charts_data(w http.ResponseWriter, r *http.Request) {
|
||||
dataHandler(w, r)
|
||||
p.chartServer.dataHandler(w, r)
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Charts_datafeed(w http.ResponseWriter, r *http.Request) {
|
||||
s.dataFeedHandler(w, r)
|
||||
p.chartServer.dataFeedHandler(w, r)
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Grf(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -193,7 +199,7 @@ func (p *DebugPlugin) GetHeap(ctx context.Context, empty *emptypb.Empty) (*pb.He
|
||||
obj.Size += size
|
||||
totalSize += size
|
||||
|
||||
// 构建引<EFBFBD><EFBFBD><EFBFBD>关系
|
||||
// 构建引用关系
|
||||
for i := 1; i < len(sample.Location); i++ {
|
||||
loc := sample.Location[i]
|
||||
if len(loc.Line) == 0 || loc.Line[0].Function == nil {
|
||||
@@ -443,3 +449,42 @@ func (p *DebugPlugin) GetHeapGraph(ctx context.Context, empty *emptypb.Empty) (*
|
||||
Data: dot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) API_TcpDump(rw http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
args := []string{"-W", "1"}
|
||||
if query.Get("interface") != "" {
|
||||
args = append(args, "-i", query.Get("interface"))
|
||||
}
|
||||
if query.Get("filter") != "" {
|
||||
args = append(args, query.Get("filter"))
|
||||
}
|
||||
if query.Get("extra_args") != "" {
|
||||
args = append(args, strings.Fields(query.Get("extra_args"))...)
|
||||
}
|
||||
if query.Get("duration") == "" {
|
||||
http.Error(rw, "duration is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "text/plain")
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Set("Content-Disposition", "attachment; filename=tcpdump.txt")
|
||||
cmd := exec.CommandContext(p, "tcpdump", args...)
|
||||
p.Info("starting tcpdump", "args", strings.Join(cmd.Args, " "))
|
||||
cmd.Stdout = rw
|
||||
cmd.Stderr = os.Stderr // 将错误输出重定向到标准错误
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf("failed to start tcpdump: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
duration, err := strconv.Atoi(query.Get("duration"))
|
||||
if err != nil {
|
||||
http.Error(rw, "invalid duration", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
<-time.After(time.Duration(duration) * time.Second)
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
p.Error("failed to kill tcpdump process", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.0
|
||||
// protoc v5.29.1
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v5.29.3
|
||||
// source: debug.proto
|
||||
|
||||
package pb
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
_ "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1007,176 +1008,107 @@ func (x *RuntimeStats) GetBlockingTimeNs() uint64 {
|
||||
|
||||
var File_debug_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_debug_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x64,
|
||||
0x65, 0x62, 0x75, 0x67, 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,
|
||||
0x22, 0x42, 0x0a, 0x0a, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18,
|
||||
0x0a, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a,
|
||||
0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a,
|
||||
0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a,
|
||||
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x01, 0x52, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x66, 0x73, 0x18,
|
||||
0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x66, 0x73, 0x22, 0xc7, 0x02, 0x0a, 0x09,
|
||||
0x48, 0x65, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c,
|
||||
0x6f, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x12,
|
||||
0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12,
|
||||
0x10, 0x0a, 0x03, 0x73, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 0x79,
|
||||
0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x75, 0x6d, 0x47, 0x43, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d,
|
||||
0x52, 0x05, 0x6e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x70, 0x41,
|
||||
0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x70,
|
||||
0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73, 0x12,
|
||||
0x1a, 0x0a, 0x08, 0x68, 0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28,
|
||||
0x04, 0x52, 0x08, 0x68, 0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x68,
|
||||
0x65, 0x61, 0x70, 0x49, 0x6e, 0x75, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09,
|
||||
0x68, 0x65, 0x61, 0x70, 0x49, 0x6e, 0x75, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x68, 0x65, 0x61,
|
||||
0x70, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52,
|
||||
0x0c, 0x68, 0x65, 0x61, 0x70, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a,
|
||||
0x0b, 0x68, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01,
|
||||
0x28, 0x04, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12,
|
||||
0x24, 0x0a, 0x0d, 0x67, 0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0d, 0x67, 0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x48, 0x65, 0x61, 0x70, 0x44, 0x61,
|
||||
0x74, 0x61, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x10, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x53, 0x74,
|
||||
0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x07, 0x6f, 0x62,
|
||||
0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x65,
|
||||
0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07,
|
||||
0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73,
|
||||
0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48,
|
||||
0x65, 0x61, 0x70, 0x45, 0x64, 0x67, 0x65, 0x52, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x22, 0x4c,
|
||||
0x0a, 0x08, 0x48, 0x65, 0x61, 0x70, 0x45, 0x64, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72,
|
||||
0x6f, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e,
|
||||
0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x1c,
|
||||
0x0a, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x61, 0x0a, 0x0c,
|
||||
0x48, 0x65, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x23, 0x0a, 0x04, 0x64, 0x61,
|
||||
0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67,
|
||||
0x2e, 0x48, 0x65, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22,
|
||||
0x55, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0d, 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, 0x09,
|
||||
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x54, 0x0a, 0x10, 0x43, 0x70, 0x75, 0x47, 0x72, 0x61,
|
||||
0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f,
|
||||
0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5f, 0x0a, 0x0b,
|
||||
0x43, 0x70, 0x75, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63,
|
||||
0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x22, 0x0a, 0x04, 0x64, 0x61, 0x74,
|
||||
0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e,
|
||||
0x43, 0x70, 0x75, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xc5, 0x02,
|
||||
0x0a, 0x07, 0x43, 0x70, 0x75, 0x44, 0x61, 0x74, 0x61, 0x12, 0x29, 0x0a, 0x11, 0x74, 0x6f, 0x74,
|
||||
0x61, 0x6c, 0x5f, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x70, 0x75, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x4e, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67,
|
||||
0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x04, 0x52, 0x12, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x76, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x34, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x65, 0x62, 0x75,
|
||||
0x67, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c,
|
||||
0x65, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x0a,
|
||||
0x67, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x17, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69,
|
||||
0x6e, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x0a, 0x67, 0x6f, 0x72, 0x6f, 0x75,
|
||||
0x74, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0c, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f,
|
||||
0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x65,
|
||||
0x62, 0x75, 0x67, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x0b,
|
||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x38, 0x0a, 0x0d, 0x72,
|
||||
0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69,
|
||||
0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0c, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65,
|
||||
0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0xbf, 0x01, 0x0a, 0x0f, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e,
|
||||
0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x12, 0x29,
|
||||
0x0a, 0x10, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6c,
|
||||
0x6c, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63,
|
||||
0x61, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x69,
|
||||
0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73,
|
||||
0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x64, 0x22, 0x77, 0x0a, 0x10, 0x47, 0x6f, 0x72, 0x6f, 0x75,
|
||||
0x74, 0x69, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73,
|
||||
0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
|
||||
0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65, 0x4e,
|
||||
0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18,
|
||||
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x63, 0x6b,
|
||||
0x22, 0x56, 0x0a, 0x0a, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e,
|
||||
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x4e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xa4, 0x01, 0x0a, 0x0c, 0x52, 0x75, 0x6e,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x67, 0x63, 0x5f,
|
||||
0x63, 0x70, 0x75, 0x5f, 0x66, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x01, 0x52, 0x0d, 0x67, 0x63, 0x43, 0x70, 0x75, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x63, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x04, 0x52, 0x07, 0x67, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x10,
|
||||
0x67, 0x63, 0x5f, 0x70, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x67, 0x63, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x4e, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e,
|
||||
0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
|
||||
0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x32,
|
||||
0xd9, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12, 0x4f, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x65,
|
||||
0x61, 0x70, 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, 0x13, 0x2e, 0x64, 0x65, 0x62,
|
||||
0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f,
|
||||
0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x61, 0x70, 0x12, 0x5f, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x48,
|
||||
0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 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, 0x18, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x47, 0x72, 0x61,
|
||||
0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93,
|
||||
0x02, 0x17, 0x12, 0x15, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x68,
|
||||
0x65, 0x61, 0x70, 0x2f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x12, 0x57, 0x0a, 0x0b, 0x47, 0x65, 0x74,
|
||||
0x43, 0x70, 0x75, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x11, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67,
|
||||
0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x64, 0x65,
|
||||
0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x64,
|
||||
0x65, 0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x70, 0x75, 0x2f, 0x67, 0x72, 0x61,
|
||||
0x70, 0x68, 0x12, 0x47, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x43, 0x70, 0x75, 0x12, 0x11, 0x2e, 0x64,
|
||||
0x65, 0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x12, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x64, 0x65,
|
||||
0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x70, 0x75, 0x42, 0x1d, 0x5a, 0x1b, 0x6d,
|
||||
0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69,
|
||||
0x6e, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
const file_debug_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vdebug.proto\x12\x05debug\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"B\n" +
|
||||
"\n" +
|
||||
"CpuRequest\x12\x18\n" +
|
||||
"\arefresh\x18\x01 \x01(\bR\arefresh\x12\x1a\n" +
|
||||
"\bduration\x18\x02 \x01(\rR\bduration\"\x94\x01\n" +
|
||||
"\n" +
|
||||
"HeapObject\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" +
|
||||
"\x05count\x18\x02 \x01(\x03R\x05count\x12\x12\n" +
|
||||
"\x04size\x18\x03 \x01(\x03R\x04size\x12\x1a\n" +
|
||||
"\bsizePerc\x18\x04 \x01(\x01R\bsizePerc\x12\x18\n" +
|
||||
"\aaddress\x18\x05 \x01(\tR\aaddress\x12\x12\n" +
|
||||
"\x04refs\x18\x06 \x03(\tR\x04refs\"\xc7\x02\n" +
|
||||
"\tHeapStats\x12\x14\n" +
|
||||
"\x05alloc\x18\x01 \x01(\x04R\x05alloc\x12\x1e\n" +
|
||||
"\n" +
|
||||
"totalAlloc\x18\x02 \x01(\x04R\n" +
|
||||
"totalAlloc\x12\x10\n" +
|
||||
"\x03sys\x18\x03 \x01(\x04R\x03sys\x12\x14\n" +
|
||||
"\x05numGC\x18\x04 \x01(\rR\x05numGC\x12\x1c\n" +
|
||||
"\theapAlloc\x18\x05 \x01(\x04R\theapAlloc\x12\x18\n" +
|
||||
"\aheapSys\x18\x06 \x01(\x04R\aheapSys\x12\x1a\n" +
|
||||
"\bheapIdle\x18\a \x01(\x04R\bheapIdle\x12\x1c\n" +
|
||||
"\theapInuse\x18\b \x01(\x04R\theapInuse\x12\"\n" +
|
||||
"\fheapReleased\x18\t \x01(\x04R\fheapReleased\x12 \n" +
|
||||
"\vheapObjects\x18\n" +
|
||||
" \x01(\x04R\vheapObjects\x12$\n" +
|
||||
"\rgcCPUFraction\x18\v \x01(\x01R\rgcCPUFraction\"\x86\x01\n" +
|
||||
"\bHeapData\x12&\n" +
|
||||
"\x05stats\x18\x01 \x01(\v2\x10.debug.HeapStatsR\x05stats\x12+\n" +
|
||||
"\aobjects\x18\x02 \x03(\v2\x11.debug.HeapObjectR\aobjects\x12%\n" +
|
||||
"\x05edges\x18\x03 \x03(\v2\x0f.debug.HeapEdgeR\x05edges\"L\n" +
|
||||
"\bHeapEdge\x12\x12\n" +
|
||||
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
|
||||
"\x02to\x18\x02 \x01(\tR\x02to\x12\x1c\n" +
|
||||
"\tfieldName\x18\x03 \x01(\tR\tfieldName\"a\n" +
|
||||
"\fHeapResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12#\n" +
|
||||
"\x04data\x18\x03 \x01(\v2\x0f.debug.HeapDataR\x04data\"U\n" +
|
||||
"\x11HeapGraphResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
|
||||
"\x04data\x18\x03 \x01(\tR\x04data\"T\n" +
|
||||
"\x10CpuGraphResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
|
||||
"\x04data\x18\x03 \x01(\tR\x04data\"_\n" +
|
||||
"\vCpuResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\"\n" +
|
||||
"\x04data\x18\x03 \x01(\v2\x0e.debug.CpuDataR\x04data\"\xc5\x02\n" +
|
||||
"\aCpuData\x12)\n" +
|
||||
"\x11total_cpu_time_ns\x18\x01 \x01(\x04R\x0etotalCpuTimeNs\x120\n" +
|
||||
"\x14sampling_interval_ns\x18\x02 \x01(\x04R\x12samplingIntervalNs\x124\n" +
|
||||
"\tfunctions\x18\x03 \x03(\v2\x16.debug.FunctionProfileR\tfunctions\x127\n" +
|
||||
"\n" +
|
||||
"goroutines\x18\x04 \x03(\v2\x17.debug.GoroutineProfileR\n" +
|
||||
"goroutines\x124\n" +
|
||||
"\fsystem_calls\x18\x05 \x03(\v2\x11.debug.SystemCallR\vsystemCalls\x128\n" +
|
||||
"\rruntime_stats\x18\x06 \x01(\v2\x13.debug.RuntimeStatsR\fruntimeStats\"\xbf\x01\n" +
|
||||
"\x0fFunctionProfile\x12#\n" +
|
||||
"\rfunction_name\x18\x01 \x01(\tR\ffunctionName\x12\x1e\n" +
|
||||
"\vcpu_time_ns\x18\x02 \x01(\x04R\tcpuTimeNs\x12)\n" +
|
||||
"\x10invocation_count\x18\x03 \x01(\x04R\x0finvocationCount\x12\x1d\n" +
|
||||
"\n" +
|
||||
"call_stack\x18\x04 \x03(\tR\tcallStack\x12\x1d\n" +
|
||||
"\n" +
|
||||
"is_inlined\x18\x05 \x01(\bR\tisInlined\"w\n" +
|
||||
"\x10GoroutineProfile\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x14\n" +
|
||||
"\x05state\x18\x02 \x01(\tR\x05state\x12\x1e\n" +
|
||||
"\vcpu_time_ns\x18\x03 \x01(\x04R\tcpuTimeNs\x12\x1d\n" +
|
||||
"\n" +
|
||||
"call_stack\x18\x04 \x03(\tR\tcallStack\"V\n" +
|
||||
"\n" +
|
||||
"SystemCall\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" +
|
||||
"\vcpu_time_ns\x18\x02 \x01(\x04R\tcpuTimeNs\x12\x14\n" +
|
||||
"\x05count\x18\x03 \x01(\x04R\x05count\"\xa4\x01\n" +
|
||||
"\fRuntimeStats\x12&\n" +
|
||||
"\x0fgc_cpu_fraction\x18\x01 \x01(\x01R\rgcCpuFraction\x12\x19\n" +
|
||||
"\bgc_count\x18\x02 \x01(\x04R\agcCount\x12'\n" +
|
||||
"\x10gc_pause_time_ns\x18\x03 \x01(\x04R\rgcPauseTimeNs\x12(\n" +
|
||||
"\x10blocking_time_ns\x18\x04 \x01(\x04R\x0eblockingTimeNs2\xd9\x02\n" +
|
||||
"\x03api\x12O\n" +
|
||||
"\aGetHeap\x12\x16.google.protobuf.Empty\x1a\x13.debug.HeapResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/debug/api/heap\x12_\n" +
|
||||
"\fGetHeapGraph\x12\x16.google.protobuf.Empty\x1a\x18.debug.HeapGraphResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/debug/api/heap/graph\x12W\n" +
|
||||
"\vGetCpuGraph\x12\x11.debug.CpuRequest\x1a\x17.debug.CpuGraphResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/debug/api/cpu/graph\x12G\n" +
|
||||
"\x06GetCpu\x12\x11.debug.CpuRequest\x1a\x12.debug.CpuResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/debug/api/cpuB\x1dZ\x1bm7s.live/v5/plugin/debug/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_debug_proto_rawDescOnce sync.Once
|
||||
file_debug_proto_rawDescData = file_debug_proto_rawDesc
|
||||
file_debug_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_debug_proto_rawDescGZIP() []byte {
|
||||
file_debug_proto_rawDescOnce.Do(func() {
|
||||
file_debug_proto_rawDescData = protoimpl.X.CompressGZIP(file_debug_proto_rawDescData)
|
||||
file_debug_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_debug_proto_rawDesc), len(file_debug_proto_rawDesc)))
|
||||
})
|
||||
return file_debug_proto_rawDescData
|
||||
}
|
||||
@@ -1233,7 +1165,7 @@ func file_debug_proto_init() {
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_debug_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_debug_proto_rawDesc), len(file_debug_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 14,
|
||||
NumExtensions: 0,
|
||||
@@ -1244,7 +1176,6 @@ func file_debug_proto_init() {
|
||||
MessageInfos: file_debug_proto_msgTypes,
|
||||
}.Build()
|
||||
File_debug_proto = out.File
|
||||
file_debug_proto_rawDesc = nil
|
||||
file_debug_proto_goTypes = nil
|
||||
file_debug_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ package pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -26,129 +25,136 @@ import (
|
||||
)
|
||||
|
||||
// Suppress "imported and not used" errors
|
||||
var (
|
||||
_ codes.Code
|
||||
_ io.Reader
|
||||
_ status.Status
|
||||
_ = errors.New
|
||||
_ = runtime.String
|
||||
_ = utilities.NewDoubleArray
|
||||
_ = metadata.Join
|
||||
)
|
||||
var _ codes.Code
|
||||
var _ io.Reader
|
||||
var _ status.Status
|
||||
var _ = runtime.String
|
||||
var _ = utilities.NewDoubleArray
|
||||
var _ = metadata.Join
|
||||
|
||||
func request_Api_GetHeap_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
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq emptypb.Empty
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := client.GetHeap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_GetHeap_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq emptypb.Empty
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq emptypb.Empty
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := server.GetHeap(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Api_GetHeapGraph_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
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq emptypb.Empty
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := client.GetHeapGraph(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_GetHeapGraph_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq emptypb.Empty
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq emptypb.Empty
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := server.GetHeapGraph(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var filter_Api_GetCpuGraph_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
var (
|
||||
filter_Api_GetCpuGraph_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_Api_GetCpuGraph_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CpuRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq CpuRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpuGraph_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetCpuGraph(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_GetCpuGraph_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CpuRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq CpuRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpuGraph_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.GetCpuGraph(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var filter_Api_GetCpu_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
var (
|
||||
filter_Api_GetCpu_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_Api_GetCpu_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CpuRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq CpuRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpu_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetCpu(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Api_GetCpu_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CpuRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
var protoReq CpuRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpu_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.GetCpu(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(http.MethodGet, pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetHeap_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -160,15 +166,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetHeapGraph_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -180,15 +191,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetCpuGraph_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -200,15 +216,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetCpuGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetCpu_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)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -220,7 +241,9 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetCpu_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -229,24 +252,25 @@ 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.NewClient(endpoint, opts...)
|
||||
conn, err := grpc.DialContext(ctx, endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterApiHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
@@ -260,13 +284,16 @@ 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. This client ignores the HTTP middlewares.
|
||||
// "ApiClient" to call the correct interceptors.
|
||||
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetHeap_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -277,13 +304,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetHeapGraph_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -294,13 +326,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetCpuGraph_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -311,13 +348,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetCpuGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
mux.Handle("GET", pattern_Api_GetCpu_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)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
@@ -328,21 +370,30 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Api_GetCpu_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_Api_GetHeap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "heap"}, ""))
|
||||
pattern_Api_GetHeap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "heap"}, ""))
|
||||
|
||||
pattern_Api_GetHeapGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "heap", "graph"}, ""))
|
||||
pattern_Api_GetCpuGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "cpu", "graph"}, ""))
|
||||
pattern_Api_GetCpu_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "cpu"}, ""))
|
||||
|
||||
pattern_Api_GetCpuGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "cpu", "graph"}, ""))
|
||||
|
||||
pattern_Api_GetCpu_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "cpu"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_Api_GetHeap_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_GetHeap_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_GetHeapGraph_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_GetCpuGraph_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_GetCpu_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_GetCpuGraph_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Api_GetCpu_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
@@ -132,4 +132,4 @@ message RuntimeStats {
|
||||
uint64 gc_count = 2; // 垃圾回收次数
|
||||
uint64 gc_pause_time_ns = 3; // 垃圾回收暂停时间(纳秒)
|
||||
uint64 blocking_time_ns = 4; // 阻塞时间(纳秒)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.1
|
||||
// - protoc v5.29.3
|
||||
// source: debug.proto
|
||||
|
||||
package pb
|
||||
|
||||
122
plugin/debug/static/envcheck.html
Normal file
122
plugin/debug/static/envcheck.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Environment Check</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 8px;
|
||||
width: 300px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
#log {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Environment Check</h1>
|
||||
<div class="input-group">
|
||||
<input type="text" id="targetUrl" placeholder="Enter target URL (e.g., http://192.168.1.100:8080)">
|
||||
<button onclick="startCheck()">Start Check</button>
|
||||
</div>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function appendLog(message, type = 'info') {
|
||||
const log = document.getElementById('log');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = type;
|
||||
entry.textContent = message;
|
||||
log.appendChild(entry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
function startCheck() {
|
||||
const targetUrl = document.getElementById('targetUrl').value;
|
||||
if (!targetUrl) {
|
||||
appendLog('Please enter a target URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous log
|
||||
document.getElementById('log').innerHTML = '';
|
||||
appendLog('Starting environment check...');
|
||||
|
||||
// Create SSE connection
|
||||
const eventSource = new EventSource(`/debug/envcheck?target=${encodeURIComponent(targetUrl)}`);
|
||||
|
||||
eventSource.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
appendLog(data.message, data.type);
|
||||
|
||||
if (data.type === 'complete') {
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (error) {
|
||||
appendLog('Connection error occurred', 'error');
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,18 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8" />
|
||||
<script src="jquery-2.1.4.min.js"></script>
|
||||
<script src="moment.min.js"></script>
|
||||
<script src="plotly-1.51.3.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container1" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container2" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container3" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container4" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/moment.js/2.11.0/moment.min.js"></script>
|
||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/plotly.js/1.51.3/plotly.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container1" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container2" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container3" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<div id="container4" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
|
||||
<script>
|
||||
var chart1;
|
||||
var chart2;
|
||||
var chart3;
|
||||
var chart4;
|
||||
|
||||
function stackedArea(traces) {
|
||||
for (var i = 1; i < traces.length; i++) {
|
||||
for (var j = 0; j < (Math.min(traces[i]['y'].length, traces[i - 1]['y'].length)); j++) {
|
||||
traces[i]['y'][j] += traces[i - 1]['y'][j];
|
||||
}
|
||||
}
|
||||
return traces;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
|
||||
$.getJSON('/debug/charts/data?callback=?', function (data) {
|
||||
var pDataChart1 = [{ x: [], y: [], type: "scattergl" }];
|
||||
|
||||
for (i = 0; i < data.GcPauses.length; i++) {
|
||||
var d = moment(data.GcPauses[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart1[0].x.push(d);
|
||||
pDataChart1[0].y.push(data.GcPauses[i].Value);
|
||||
}
|
||||
|
||||
chart1 = Plotly.newPlot('container1', pDataChart1, {
|
||||
title: "GC Pauses",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Nanoseconds"
|
||||
}
|
||||
});
|
||||
|
||||
var pDataChart2 = [{ x: [], y: [], type: "scattergl" }];
|
||||
|
||||
for (i = 0; i < data.BytesAllocated.length; i++) {
|
||||
var d = moment(data.BytesAllocated[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart2[0].x.push(d);
|
||||
pDataChart2[0].y.push(data.BytesAllocated[i].Value);
|
||||
}
|
||||
|
||||
chart2 = Plotly.newPlot('container2', pDataChart2, {
|
||||
title: "Memory Allocated",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Bytes"
|
||||
}
|
||||
});
|
||||
|
||||
var pDataChart3 = [
|
||||
{ x: [], y: [], fill: 'tozeroy', name: 'sys', hoverinfo: 'none', type: "scattergl" },
|
||||
{ x: [], y: [], fill: 'tonexty', name: 'user', hoverinfo: 'none', type: "scattergl" }
|
||||
];
|
||||
|
||||
for (i = 0; i < data.CPUUsage.length; i++) {
|
||||
var d = moment(data.CPUUsage[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart3[0].x.push(d);
|
||||
pDataChart3[1].x.push(d);
|
||||
pDataChart3[0].y.push(data.CPUUsage[i].Sys);
|
||||
pDataChart3[1].y.push(data.CPUUsage[i].User);
|
||||
}
|
||||
|
||||
pDataChart3 = stackedArea(pDataChart3);
|
||||
|
||||
chart3 = Plotly.newPlot('container3', pDataChart3, {
|
||||
title: "CPU Usage",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Seconds"
|
||||
}
|
||||
});
|
||||
|
||||
var pprofList = ["Block", "Goroutine", "Heap", "Mutex", "Threadcreate"];
|
||||
var pDataChart4 = [];
|
||||
for (i = 0; i < pprofList.length; i++) {
|
||||
pDataChart4.push({ x: [], y: [], name: pprofList[i].toLowerCase() });
|
||||
}
|
||||
|
||||
for (i = 0; i < data.Pprof.length; i++) {
|
||||
var d = moment(data.Pprof[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
for (j = 0; j < pprofList.length; j++) {
|
||||
pDataChart4[j].x.push(d);
|
||||
pDataChart4[j].y.push(data.Pprof[i][pprofList[j]]);
|
||||
}
|
||||
}
|
||||
|
||||
chart4 = Plotly.newPlot('container4', pDataChart4, {
|
||||
title: "PPROF",
|
||||
xaxis: {
|
||||
type: "date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Count"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function wsurl() {
|
||||
var l = window.location;
|
||||
return ((l.protocol === "https:") ? "wss://" : "ws://") + l.hostname + (((l.port != 80) && (l.port != 443)) ? ":" + l.port : "") + "/debug/charts/datafeed";
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsurl());
|
||||
ws.onopen = function () {
|
||||
ws.onmessage = function (evt) {
|
||||
var data = JSON.parse(evt.data);
|
||||
var d = moment(data.Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (data.GcPause != 0) {
|
||||
Plotly.extendTraces('container1', { x: [[d]], y: [[data.GcPause]] }, [0], 86400);
|
||||
}
|
||||
Plotly.extendTraces('container2', { x: [[d]], y: [[data.BytesAllocated]] }, [0], 86400);
|
||||
Plotly.extendTraces('container3', { x: [[d], [d]], y: [[data.CPUSys], [data.CPUUser]] }, [0, 1], 86400);
|
||||
Plotly.extendTraces('container4', { x: [[d], [d], [d], [d], [d]], y: [[data.Block], [data.Goroutine], [data.Heap], [data.Mutex], [data.Threadcreate]] }, [0, 1, 2, 3, 4], 86400);
|
||||
};
|
||||
};
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
plugin/debug/static/jquery-2.1.4.min.js
vendored
4
plugin/debug/static/jquery-2.1.4.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,122 +0,0 @@
|
||||
var chart1;
|
||||
var chart2;
|
||||
var chart3;
|
||||
var chart4;
|
||||
|
||||
function stackedArea(traces) {
|
||||
for(var i=1; i<traces.length; i++) {
|
||||
for(var j=0; j<(Math.min(traces[i]['y'].length, traces[i-1]['y'].length)); j++) {
|
||||
traces[i]['y'][j] += traces[i-1]['y'][j];
|
||||
}
|
||||
}
|
||||
return traces;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
|
||||
$.getJSON('/debug/charts/data?callback=?', function (data) {
|
||||
var pDataChart1 = [{x: [], y: [], type: "scattergl"}];
|
||||
|
||||
for (i = 0; i < data.GcPauses.length; i++) {
|
||||
var d = moment(data.GcPauses[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart1[0].x.push(d);
|
||||
pDataChart1[0].y.push(data.GcPauses[i].Value);
|
||||
}
|
||||
|
||||
chart1 = Plotly.newPlot('container1', pDataChart1, {
|
||||
title: "GC Pauses",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Nanoseconds"
|
||||
}
|
||||
});
|
||||
|
||||
var pDataChart2 = [{x: [], y: [], type: "scattergl"}];
|
||||
|
||||
for (i = 0; i < data.BytesAllocated.length; i++) {
|
||||
var d = moment(data.BytesAllocated[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart2[0].x.push(d);
|
||||
pDataChart2[0].y.push(data.BytesAllocated[i].Value);
|
||||
}
|
||||
|
||||
chart2 = Plotly.newPlot('container2', pDataChart2, {
|
||||
title: "Memory Allocated",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Bytes"
|
||||
}
|
||||
});
|
||||
|
||||
var pDataChart3 = [
|
||||
{x: [], y: [], fill: 'tozeroy', name: 'sys', hoverinfo: 'none', type: "scattergl"},
|
||||
{x: [], y: [], fill: 'tonexty', name: 'user', hoverinfo: 'none', type: "scattergl"}
|
||||
];
|
||||
|
||||
for (i = 0; i < data.CPUUsage.length; i++) {
|
||||
var d = moment(data.CPUUsage[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
pDataChart3[0].x.push(d);
|
||||
pDataChart3[1].x.push(d);
|
||||
pDataChart3[0].y.push(data.CPUUsage[i].Sys);
|
||||
pDataChart3[1].y.push(data.CPUUsage[i].User);
|
||||
}
|
||||
|
||||
pDataChart3 = stackedArea(pDataChart3);
|
||||
|
||||
chart3 = Plotly.newPlot('container3', pDataChart3, {
|
||||
title: "CPU Usage",
|
||||
xaxis: {
|
||||
type: "date"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Seconds"
|
||||
}
|
||||
});
|
||||
|
||||
var pprofList = ["Block", "Goroutine", "Heap", "Mutex", "Threadcreate"];
|
||||
var pDataChart4 = []
|
||||
for (i = 0; i < pprofList.length; i++) {
|
||||
pDataChart4.push({x: [], y: [], name: pprofList[i].toLowerCase()})
|
||||
}
|
||||
|
||||
for (i = 0; i < data.Pprof.length; i++) {
|
||||
var d = moment(data.Pprof[i].Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
for (j = 0; j < pprofList.length; j++) {
|
||||
pDataChart4[j].x.push(d);
|
||||
pDataChart4[j].y.push(data.Pprof[i][pprofList[j]])
|
||||
}
|
||||
}
|
||||
|
||||
chart4 = Plotly.newPlot('container4', pDataChart4, {
|
||||
title: "PPROF",
|
||||
xaxis: {
|
||||
type: "date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Count"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function wsurl() {
|
||||
var l = window.location;
|
||||
return ((l.protocol === "https:") ? "wss://" : "ws://") + l.hostname + (((l.port != 80) && (l.port != 443)) ? ":" + l.port : "") + "/debug/charts/datafeed";
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsurl());
|
||||
ws.onopen = function () {
|
||||
ws.onmessage = function (evt) {
|
||||
var data = JSON.parse(evt.data);
|
||||
var d = moment(data.Ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (data.GcPause != 0) {
|
||||
Plotly.extendTraces('container1', {x:[[d]],y:[[data.GcPause]]}, [0], 86400);
|
||||
}
|
||||
Plotly.extendTraces('container2', {x:[[d]],y:[[data.BytesAllocated]]}, [0], 86400);
|
||||
Plotly.extendTraces('container3', {x:[[d], [d]],y:[[data.CPUSys], [data.CPUUser]]}, [0, 1], 86400);
|
||||
Plotly.extendTraces('container4', {x:[[d], [d], [d], [d], [d]],y:[[data.Block], [data.Goroutine], [data.Heap], [data.Mutex], [data.Threadcreate]]}, [0, 1, 2, 3, 4], 86400);
|
||||
}
|
||||
};
|
||||
})
|
||||
7
plugin/debug/static/moment.min.js
vendored
7
plugin/debug/static/moment.min.js
vendored
File diff suppressed because one or more lines are too long
7
plugin/debug/static/plotly-1.51.3.min.js
vendored
7
plugin/debug/static/plotly-1.51.3.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,27 +1,15 @@
|
||||
package plugin_flv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"m7s.live/v5/pb"
|
||||
"m7s.live/v5/pkg/util"
|
||||
flvpb "m7s.live/v5/plugin/flv/pb"
|
||||
flv "m7s.live/v5/plugin/flv/pkg"
|
||||
rtmp "m7s.live/v5/plugin/rtmp/pkg"
|
||||
)
|
||||
|
||||
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.ResponseList, err error) {
|
||||
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
|
||||
globalReq := &pb.ReqRecordList{
|
||||
StreamPath: req.StreamPath,
|
||||
Range: req.Range,
|
||||
@@ -29,7 +17,6 @@ func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *p
|
||||
End: req.End,
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
Mode: req.Mode,
|
||||
Type: "flv",
|
||||
}
|
||||
return p.Server.GetRecordList(ctx, globalReq)
|
||||
@@ -52,248 +39,49 @@ func (p *FLVPlugin) Delete(ctx context.Context, req *flvpb.ReqRecordDelete) (res
|
||||
}
|
||||
|
||||
func (plugin *FLVPlugin) Download_(w http.ResponseWriter, r *http.Request) {
|
||||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/download/"), ".flv")
|
||||
singleFile := filepath.Join(plugin.Path, streamPath+".flv")
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(r.URL.Query())
|
||||
// 解析请求参数
|
||||
params, err := plugin.parseRequestParams(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
timeRange := endTime.Sub(startTime)
|
||||
plugin.Info("download", "stream", streamPath, "start", startTime, "end", endTime)
|
||||
dir := filepath.Join(plugin.Path, streamPath)
|
||||
if util.Exist(singleFile) {
|
||||
|
||||
} else if util.Exist(dir) {
|
||||
var fileList []fs.FileInfo
|
||||
var found bool
|
||||
var startOffsetTime time.Duration
|
||||
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
||||
if info.IsDir() || !strings.HasSuffix(info.Name(), ".flv") {
|
||||
return nil
|
||||
}
|
||||
modTime := info.ModTime()
|
||||
//tmp, _ := strconv.Atoi(strings.TrimSuffix(info.Name(), ".flv"))
|
||||
//fileStartTime := time.Unix(tmp, 10)
|
||||
if !found {
|
||||
if modTime.After(startTime) {
|
||||
found = true
|
||||
//fmt.Println(path, modTime, startTime, found)
|
||||
} else {
|
||||
fileList = []fs.FileInfo{info}
|
||||
startOffsetTime = startTime.Sub(modTime)
|
||||
//fmt.Println(path, modTime, startTime, found)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if modTime.After(endTime) {
|
||||
return fs.ErrInvalid
|
||||
}
|
||||
fileList = append(fileList, info)
|
||||
return nil
|
||||
})
|
||||
if !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
plugin.Info("download", "stream", params.streamPath, "start", params.startTime, "end", params.endTime)
|
||||
|
||||
w.Header().Set("Content-Type", "video/x-flv")
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
var writer io.Writer = w
|
||||
flvHead := make([]byte, 9+4)
|
||||
tagHead := make(util.Buffer, 11)
|
||||
var contentLength uint64
|
||||
// 从数据库查询录像记录
|
||||
recordStreams, err := plugin.queryRecordStreams(params)
|
||||
if err != nil {
|
||||
plugin.Error("Failed to query record streams", "err", err)
|
||||
http.Error(w, "Database query failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var amf *rtmp.AMF
|
||||
var metaData rtmp.EcmaArray
|
||||
initMetaData := func(reader io.Reader, dataLen uint32) {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
amf = &rtmp.AMF{
|
||||
Buffer: util.Buffer(data[1+2+len("onMetaData") : len(data)-4]),
|
||||
}
|
||||
var obj any
|
||||
obj, err = amf.Unmarshal()
|
||||
metaData = obj.(rtmp.EcmaArray)
|
||||
}
|
||||
var filepositions []uint64
|
||||
var times []float64
|
||||
for pass := 0; pass < 2; pass++ {
|
||||
offsetTime := startOffsetTime
|
||||
var offsetTimestamp, lastTimestamp uint32
|
||||
var init, seqAudioWritten, seqVideoWritten bool
|
||||
if pass == 1 {
|
||||
metaData["keyframes"] = map[string]any{
|
||||
"filepositions": filepositions,
|
||||
"times": times,
|
||||
}
|
||||
amf.Marshals("onMetaData", metaData)
|
||||
offsetDelta := amf.Len() + 15
|
||||
offset := offsetDelta + len(flvHead)
|
||||
contentLength += uint64(offset)
|
||||
metaData["duration"] = timeRange.Seconds()
|
||||
metaData["filesize"] = contentLength
|
||||
for i := range filepositions {
|
||||
filepositions[i] += uint64(offset)
|
||||
}
|
||||
metaData["keyframes"] = map[string]any{
|
||||
"filepositions": filepositions,
|
||||
"times": times,
|
||||
}
|
||||
amf.Reset()
|
||||
amf.Marshals("onMetaData", metaData)
|
||||
plugin.Info("start download", "metaData", metaData)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(contentLength), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if offsetTime == 0 {
|
||||
init = true
|
||||
} else {
|
||||
offsetTimestamp = -uint32(offsetTime.Milliseconds())
|
||||
}
|
||||
for i, info := range fileList {
|
||||
if r.Context().Err() != nil {
|
||||
return
|
||||
}
|
||||
filePath := filepath.Join(dir, info.Name())
|
||||
plugin.Debug("read", "file", filePath)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
reader := bufio.NewReader(file)
|
||||
if i == 0 {
|
||||
_, err = io.ReadFull(reader, flvHead)
|
||||
if pass == 1 {
|
||||
// 第一次写入头
|
||||
_, err = writer.Write(flvHead)
|
||||
tagHead[0] = flv.FLV_TAG_TYPE_SCRIPT
|
||||
l := amf.Len()
|
||||
tagHead[1] = byte(l >> 16)
|
||||
tagHead[2] = byte(l >> 8)
|
||||
tagHead[3] = byte(l)
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
writer.Write(tagHead)
|
||||
writer.Write(amf.Buffer)
|
||||
l += 11
|
||||
binary.BigEndian.PutUint32(tagHead[:4], uint32(l))
|
||||
writer.Write(tagHead[:4])
|
||||
}
|
||||
} else {
|
||||
// 后面的头跳过
|
||||
_, err = reader.Discard(13)
|
||||
if !init {
|
||||
offsetTime = 0
|
||||
offsetTimestamp = 0
|
||||
}
|
||||
}
|
||||
for err == nil {
|
||||
_, err = io.ReadFull(reader, tagHead)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
tmp := tagHead
|
||||
t := tmp.ReadByte()
|
||||
dataLen := tmp.ReadUint24()
|
||||
lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
|
||||
//fmt.Println(lastTimestamp, tagHead)
|
||||
if init {
|
||||
if t == flv.FLV_TAG_TYPE_SCRIPT {
|
||||
if pass == 0 {
|
||||
initMetaData(reader, dataLen)
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
} else {
|
||||
lastTimestamp += offsetTimestamp
|
||||
if lastTimestamp >= uint32(timeRange.Milliseconds()) {
|
||||
break
|
||||
}
|
||||
if pass == 0 {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
frameType := (data[0] >> 4) & 0b0111
|
||||
idr := frameType == 1 || frameType == 4
|
||||
if idr {
|
||||
filepositions = append(filepositions, contentLength)
|
||||
times = append(times, float64(lastTimestamp)/1000)
|
||||
}
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
} else {
|
||||
//fmt.Println("write", lastTimestamp)
|
||||
flv.PutFlvTimestamp(tagHead, lastTimestamp)
|
||||
_, err = writer.Write(tagHead)
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch t {
|
||||
case flv.FLV_TAG_TYPE_SCRIPT:
|
||||
if pass == 0 {
|
||||
initMetaData(reader, dataLen)
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
case flv.FLV_TAG_TYPE_AUDIO:
|
||||
if !seqAudioWritten {
|
||||
if pass == 0 {
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
seqAudioWritten = true
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
case flv.FLV_TAG_TYPE_VIDEO:
|
||||
if !seqVideoWritten {
|
||||
if pass == 0 {
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
seqVideoWritten = true
|
||||
} else {
|
||||
if lastTimestamp >= uint32(offsetTime.Milliseconds()) {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
frameType := (data[0] >> 4) & 0b0111
|
||||
idr := frameType == 1 || frameType == 4
|
||||
if idr {
|
||||
init = true
|
||||
plugin.Debug("init", "lastTimestamp", lastTimestamp)
|
||||
if pass == 0 {
|
||||
filepositions = append(filepositions, contentLength)
|
||||
times = append(times, float64(lastTimestamp)/1000)
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
_, err = writer.Write(data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
offsetTimestamp = lastTimestamp
|
||||
err = file.Close()
|
||||
}
|
||||
}
|
||||
plugin.Info("end download")
|
||||
} else {
|
||||
// 构建文件信息列表
|
||||
fileInfoList, found := plugin.buildFileInfoList(recordStreams, params.startTime, params.endTime)
|
||||
if !found || len(fileInfoList) == 0 {
|
||||
plugin.Warn("No records found", "stream", params.streamPath, "start", params.startTime, "end", params.endTime)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据记录类型选择处理方式
|
||||
if plugin.hasOnlyMp4Records(fileInfoList) {
|
||||
// 过滤MP4文件并转换为FLV
|
||||
mp4FileList := plugin.filterMp4Files(fileInfoList)
|
||||
if len(mp4FileList) == 0 {
|
||||
plugin.Warn("No valid MP4 files after filtering", "stream", params.streamPath)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
plugin.processMp4ToFlv(w, r, mp4FileList, params)
|
||||
} else {
|
||||
// 过滤FLV文件并处理
|
||||
flvFileList := plugin.filterFlvFiles(fileInfoList)
|
||||
if len(flvFileList) == 0 {
|
||||
plugin.Warn("No valid FLV files after filtering", "stream", params.streamPath)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
plugin.processFlvFiles(w, r, flvFileList, params)
|
||||
}
|
||||
}
|
||||
|
||||
504
plugin/flv/download.go
Normal file
504
plugin/flv/download.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package plugin_flv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
m7s "m7s.live/v5"
|
||||
"m7s.live/v5/pkg/util"
|
||||
flv "m7s.live/v5/plugin/flv/pkg"
|
||||
mp4 "m7s.live/v5/plugin/mp4/pkg"
|
||||
rtmp "m7s.live/v5/plugin/rtmp/pkg"
|
||||
)
|
||||
|
||||
// requestParams 包含请求解析后的参数
|
||||
type requestParams struct {
|
||||
streamPath string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
timeRange time.Duration
|
||||
}
|
||||
|
||||
// fileInfo 包含文件信息
|
||||
type fileInfo struct {
|
||||
filePath string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
startOffsetTime time.Duration
|
||||
recordType string // "flv" 或 "mp4"
|
||||
}
|
||||
|
||||
// parseRequestParams 解析请求参数
|
||||
func (plugin *FLVPlugin) parseRequestParams(r *http.Request) (*requestParams, error) {
|
||||
// 从URL路径中提取流路径,去除前缀 "/download/" 和后缀 ".flv"
|
||||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/download/"), ".flv")
|
||||
|
||||
// 解析URL查询参数中的时间范围(start和end参数)
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(r.URL.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &requestParams{
|
||||
streamPath: streamPath,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
timeRange: endTime.Sub(startTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// queryRecordStreams 从数据库查询录像记录
|
||||
func (plugin *FLVPlugin) queryRecordStreams(params *requestParams) ([]m7s.RecordStream, error) {
|
||||
// 检查数据库是否可用
|
||||
if plugin.DB == nil {
|
||||
return nil, fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
var recordStreams []m7s.RecordStream
|
||||
|
||||
// 首先查询FLV记录
|
||||
query := plugin.DB.Model(&m7s.RecordStream{}).Where("stream_path = ? AND type = ?", params.streamPath, "flv")
|
||||
|
||||
// 添加时间范围查询条件
|
||||
if !params.startTime.IsZero() && !params.endTime.IsZero() {
|
||||
query = query.Where("(start_time <= ? AND end_time >= ?) OR (start_time >= ? AND start_time <= ?)",
|
||||
params.endTime, params.startTime, params.startTime, params.endTime)
|
||||
}
|
||||
|
||||
err := query.Order("start_time ASC").Find(&recordStreams).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果没有找到FLV记录,尝试查询MP4记录
|
||||
if len(recordStreams) == 0 {
|
||||
query = plugin.DB.Model(&m7s.RecordStream{}).Where("stream_path = ? AND type IN (?)", params.streamPath, []string{"mp4", "fmp4"})
|
||||
|
||||
if !params.startTime.IsZero() && !params.endTime.IsZero() {
|
||||
query = query.Where("(start_time <= ? AND end_time >= ?) OR (start_time >= ? AND start_time <= ?)",
|
||||
params.endTime, params.startTime, params.startTime, params.endTime)
|
||||
}
|
||||
|
||||
err = query.Order("start_time ASC").Find(&recordStreams).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return recordStreams, nil
|
||||
}
|
||||
|
||||
// buildFileInfoList 构建文件信息列表
|
||||
func (plugin *FLVPlugin) buildFileInfoList(recordStreams []m7s.RecordStream, startTime, endTime time.Time) ([]*fileInfo, bool) {
|
||||
var fileInfoList []*fileInfo
|
||||
var found bool
|
||||
|
||||
for _, record := range recordStreams {
|
||||
// 检查文件是否存在
|
||||
if !util.Exist(record.FilePath) {
|
||||
plugin.Warn("Record file not found", "filePath", record.FilePath)
|
||||
continue
|
||||
}
|
||||
|
||||
var startOffsetTime time.Duration
|
||||
recordStartTime := record.StartTime
|
||||
recordEndTime := record.EndTime
|
||||
|
||||
// 计算文件内的偏移时间
|
||||
if startTime.After(recordStartTime) {
|
||||
startOffsetTime = startTime.Sub(recordStartTime)
|
||||
}
|
||||
|
||||
// 检查是否在时间范围内
|
||||
if recordEndTime.Before(startTime) || recordStartTime.After(endTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
fileInfoList = append(fileInfoList, &fileInfo{
|
||||
filePath: record.FilePath,
|
||||
startTime: recordStartTime,
|
||||
endTime: recordEndTime,
|
||||
startOffsetTime: startOffsetTime,
|
||||
recordType: record.Type,
|
||||
})
|
||||
|
||||
found = true
|
||||
}
|
||||
|
||||
return fileInfoList, found
|
||||
}
|
||||
|
||||
// hasOnlyMp4Records 检查是否只有MP4记录
|
||||
func (plugin *FLVPlugin) hasOnlyMp4Records(fileInfoList []*fileInfo) bool {
|
||||
if len(fileInfoList) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, info := range fileInfoList {
|
||||
if info.recordType == "flv" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// filterFlvFiles 过滤FLV文件
|
||||
func (plugin *FLVPlugin) filterFlvFiles(fileInfoList []*fileInfo) []*fileInfo {
|
||||
var filteredList []*fileInfo
|
||||
|
||||
for _, info := range fileInfoList {
|
||||
if info.recordType == "flv" {
|
||||
filteredList = append(filteredList, info)
|
||||
}
|
||||
}
|
||||
|
||||
plugin.Debug("FLV files filtered", "original", len(fileInfoList), "filtered", len(filteredList))
|
||||
return filteredList
|
||||
}
|
||||
|
||||
// filterMp4Files 过滤MP4文件
|
||||
func (plugin *FLVPlugin) filterMp4Files(fileInfoList []*fileInfo) []*fileInfo {
|
||||
var filteredList []*fileInfo
|
||||
|
||||
for _, info := range fileInfoList {
|
||||
if info.recordType == "mp4" || info.recordType == "fmp4" {
|
||||
filteredList = append(filteredList, info)
|
||||
}
|
||||
}
|
||||
|
||||
plugin.Debug("MP4 files filtered", "original", len(fileInfoList), "filtered", len(filteredList))
|
||||
return filteredList
|
||||
}
|
||||
|
||||
// processMp4ToFlv 将MP4记录转换为FLV输出
|
||||
func (plugin *FLVPlugin) processMp4ToFlv(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
|
||||
plugin.Info("Converting MP4 records to FLV", "count", len(fileInfoList))
|
||||
|
||||
// 设置HTTP响应头
|
||||
w.Header().Set("Content-Type", "video/x-flv")
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
|
||||
// 创建MP4流列表
|
||||
var mp4Streams []m7s.RecordStream
|
||||
for _, info := range fileInfoList {
|
||||
mp4Streams = append(mp4Streams, m7s.RecordStream{
|
||||
FilePath: info.filePath,
|
||||
StartTime: info.startTime,
|
||||
EndTime: info.endTime,
|
||||
Type: info.recordType,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建DemuxerConverterRange进行MP4解复用和转换
|
||||
demuxer := &mp4.DemuxerConverterRange[*rtmp.RTMPAudio, *rtmp.RTMPVideo]{
|
||||
DemuxerRange: mp4.DemuxerRange{
|
||||
StartTime: params.startTime,
|
||||
EndTime: params.endTime,
|
||||
Streams: mp4Streams,
|
||||
Logger: plugin.Logger.With("demuxer", "mp4_flv"),
|
||||
},
|
||||
}
|
||||
|
||||
// 创建FLV编码器状态
|
||||
flvWriter := flv.NewFlvWriter(w)
|
||||
hasWritten := false
|
||||
ts := int64(0) // 初始化时间戳
|
||||
tsOffset := int64(0) // 偏移时间戳
|
||||
// 执行解复用和转换
|
||||
err := demuxer.Demux(r.Context(),
|
||||
func(audio *rtmp.RTMPAudio) error {
|
||||
if !hasWritten {
|
||||
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 计算调整后的时间戳
|
||||
ts = int64(audio.Timestamp) + tsOffset
|
||||
timestamp := uint32(ts)
|
||||
|
||||
// 写入音频数据帧
|
||||
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_AUDIO, timestamp, uint32(audio.Size), audio.Buffers...)
|
||||
}, func(frame *rtmp.RTMPVideo) error {
|
||||
if !hasWritten {
|
||||
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 计算调整后的时间戳
|
||||
ts = int64(frame.Timestamp) + tsOffset
|
||||
timestamp := uint32(ts)
|
||||
// 写入视频数据帧
|
||||
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(frame.Size), frame.Buffers...)
|
||||
})
|
||||
if err != nil {
|
||||
plugin.Error("MP4 to FLV conversion failed", "err", err)
|
||||
if !hasWritten {
|
||||
http.Error(w, "Conversion failed", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
plugin.Info("MP4 to FLV conversion completed")
|
||||
}
|
||||
|
||||
// processFlvFiles 处理原生FLV文件
|
||||
func (plugin *FLVPlugin) processFlvFiles(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
|
||||
plugin.Info("Processing FLV files", "count", len(fileInfoList))
|
||||
|
||||
// 设置HTTP响应头
|
||||
w.Header().Set("Content-Type", "video/x-flv")
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
|
||||
var writer io.Writer = w
|
||||
flvHead := make([]byte, 9+4)
|
||||
tagHead := make(util.Buffer, 11)
|
||||
var contentLength uint64
|
||||
var startOffsetTime time.Duration
|
||||
|
||||
// 计算第一个文件的偏移时间
|
||||
if len(fileInfoList) > 0 {
|
||||
startOffsetTime = fileInfoList[0].startOffsetTime
|
||||
}
|
||||
|
||||
var amf *rtmp.AMF
|
||||
var metaData rtmp.EcmaArray
|
||||
initMetaData := func(reader io.Reader, dataLen uint32) {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err := io.ReadFull(reader, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
amf = &rtmp.AMF{
|
||||
Buffer: util.Buffer(data[1+2+len("onMetaData") : len(data)-4]),
|
||||
}
|
||||
var obj any
|
||||
obj, err = amf.Unmarshal()
|
||||
if err == nil {
|
||||
metaData = obj.(rtmp.EcmaArray)
|
||||
}
|
||||
}
|
||||
|
||||
var filepositions []uint64
|
||||
var times []float64
|
||||
|
||||
// 两次遍历:第一次计算大小,第二次写入数据
|
||||
for pass := 0; pass < 2; pass++ {
|
||||
offsetTime := startOffsetTime
|
||||
var offsetTimestamp, lastTimestamp uint32
|
||||
var init, seqAudioWritten, seqVideoWritten bool
|
||||
|
||||
if pass == 1 {
|
||||
// 第二次遍历时,准备写入
|
||||
metaData["keyframes"] = map[string]any{
|
||||
"filepositions": filepositions,
|
||||
"times": times,
|
||||
}
|
||||
amf.Marshals("onMetaData", metaData)
|
||||
offsetDelta := amf.Len() + 15
|
||||
offset := offsetDelta + len(flvHead)
|
||||
contentLength += uint64(offset)
|
||||
metaData["duration"] = params.timeRange.Seconds()
|
||||
metaData["filesize"] = contentLength
|
||||
for i := range filepositions {
|
||||
filepositions[i] += uint64(offset)
|
||||
}
|
||||
metaData["keyframes"] = map[string]any{
|
||||
"filepositions": filepositions,
|
||||
"times": times,
|
||||
}
|
||||
amf.Reset()
|
||||
amf.Marshals("onMetaData", metaData)
|
||||
plugin.Info("start download", "metaData", metaData)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(contentLength), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if offsetTime == 0 {
|
||||
init = true
|
||||
} else {
|
||||
offsetTimestamp = -uint32(offsetTime.Milliseconds())
|
||||
}
|
||||
|
||||
for i, info := range fileInfoList {
|
||||
if r.Context().Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plugin.Debug("Processing file", "path", info.filePath)
|
||||
file, err := os.Open(info.filePath)
|
||||
if err != nil {
|
||||
plugin.Error("Failed to open file", "path", info.filePath, "err", err)
|
||||
if pass == 1 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
if i == 0 {
|
||||
_, err = io.ReadFull(reader, flvHead)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
if pass == 1 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if pass == 1 {
|
||||
// 第一次写入头
|
||||
_, err = writer.Write(flvHead)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return
|
||||
}
|
||||
tagHead[0] = flv.FLV_TAG_TYPE_SCRIPT
|
||||
l := amf.Len()
|
||||
tagHead[1] = byte(l >> 16)
|
||||
tagHead[2] = byte(l >> 8)
|
||||
tagHead[3] = byte(l)
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
writer.Write(tagHead)
|
||||
writer.Write(amf.Buffer)
|
||||
l += 11
|
||||
binary.BigEndian.PutUint32(tagHead[:4], uint32(l))
|
||||
writer.Write(tagHead[:4])
|
||||
}
|
||||
} else {
|
||||
// 后面的头跳过
|
||||
_, err = reader.Discard(13)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
continue
|
||||
}
|
||||
if !init {
|
||||
offsetTime = 0
|
||||
offsetTimestamp = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 处理FLV标签
|
||||
for err == nil {
|
||||
_, err = io.ReadFull(reader, tagHead)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
tmp := tagHead
|
||||
t := tmp.ReadByte()
|
||||
dataLen := tmp.ReadUint24()
|
||||
lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
|
||||
|
||||
if init {
|
||||
if t == flv.FLV_TAG_TYPE_SCRIPT {
|
||||
if pass == 0 {
|
||||
initMetaData(reader, dataLen)
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
} else {
|
||||
lastTimestamp += offsetTimestamp
|
||||
if lastTimestamp >= uint32(params.timeRange.Milliseconds()) {
|
||||
break
|
||||
}
|
||||
if pass == 0 {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
if err == nil {
|
||||
frameType := (data[0] >> 4) & 0b0111
|
||||
idr := frameType == 1 || frameType == 4
|
||||
if idr {
|
||||
filepositions = append(filepositions, contentLength)
|
||||
times = append(times, float64(lastTimestamp)/1000)
|
||||
}
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
}
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, lastTimestamp)
|
||||
_, err = writer.Write(tagHead)
|
||||
if err == nil {
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch t {
|
||||
case flv.FLV_TAG_TYPE_SCRIPT:
|
||||
if pass == 0 {
|
||||
initMetaData(reader, dataLen)
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
case flv.FLV_TAG_TYPE_AUDIO:
|
||||
if !seqAudioWritten {
|
||||
if pass == 0 {
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
if err == nil {
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
}
|
||||
seqAudioWritten = true
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
case flv.FLV_TAG_TYPE_VIDEO:
|
||||
if !seqVideoWritten {
|
||||
if pass == 0 {
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
if err == nil {
|
||||
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||
}
|
||||
}
|
||||
seqVideoWritten = true
|
||||
} else {
|
||||
if lastTimestamp >= uint32(offsetTime.Milliseconds()) {
|
||||
data := make([]byte, dataLen+4)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
if err == nil {
|
||||
frameType := (data[0] >> 4) & 0b0111
|
||||
idr := frameType == 1 || frameType == 4
|
||||
if idr {
|
||||
init = true
|
||||
plugin.Debug("init", "lastTimestamp", lastTimestamp)
|
||||
if pass == 0 {
|
||||
filepositions = append(filepositions, contentLength)
|
||||
times = append(times, float64(lastTimestamp)/1000)
|
||||
contentLength += uint64(11 + dataLen + 4)
|
||||
} else {
|
||||
flv.PutFlvTimestamp(tagHead, 0)
|
||||
_, err = writer.Write(tagHead)
|
||||
if err == nil {
|
||||
_, err = writer.Write(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err = reader.Discard(int(dataLen) + 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
offsetTimestamp = lastTimestamp
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
plugin.Info("FLV download completed")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.6
|
||||
// protoc v5.29.3
|
||||
// source: flv.proto
|
||||
|
||||
package pb
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
pb "m7s.live/v5/pb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,26 +27,23 @@ 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"`
|
||||
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_flv_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_flv_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_flv_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)
|
||||
@@ -121,24 +119,21 @@ func (x *ReqRecordList) GetMode() string {
|
||||
}
|
||||
|
||||
type ReqRecordDelete 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"`
|
||||
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
|
||||
|
||||
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"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) Reset() {
|
||||
*x = ReqRecordDelete{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_flv_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_flv_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) String() string {
|
||||
@@ -149,7 +144,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
|
||||
|
||||
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_flv_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)
|
||||
@@ -201,86 +196,58 @@ func (x *ReqRecordDelete) GetRange() string {
|
||||
|
||||
var File_flv_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_flv_proto_rawDesc = []byte{
|
||||
0x0a, 0x09, 0x66, 0x6c, 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x66, 0x6c, 0x76,
|
||||
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, 0x32, 0x98, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69,
|
||||
0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76,
|
||||
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,
|
||||
0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76, 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, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f,
|
||||
0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x70, 0x62,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_flv_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\tflv.proto\x12\x03flv\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\fglobal.proto\"\xb7\x01\n" +
|
||||
"\rReqRecordList\x12\x1e\n" +
|
||||
"\n" +
|
||||
"streamPath\x18\x01 \x01(\tR\n" +
|
||||
"streamPath\x12\x14\n" +
|
||||
"\x05range\x18\x02 \x01(\tR\x05range\x12\x14\n" +
|
||||
"\x05start\x18\x03 \x01(\tR\x05start\x12\x10\n" +
|
||||
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
|
||||
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
|
||||
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
|
||||
"\x04mode\x18\a \x01(\tR\x04mode\"\x91\x01\n" +
|
||||
"\x0fReqRecordDelete\x12\x1e\n" +
|
||||
"\n" +
|
||||
"streamPath\x18\x01 \x01(\tR\n" +
|
||||
"streamPath\x12\x10\n" +
|
||||
"\x03ids\x18\x02 \x03(\rR\x03ids\x12\x1c\n" +
|
||||
"\tstartTime\x18\x03 \x01(\tR\tstartTime\x12\x18\n" +
|
||||
"\aendTime\x18\x04 \x01(\tR\aendTime\x12\x14\n" +
|
||||
"\x05range\x18\x05 \x01(\tR\x05range2\x9e\x02\n" +
|
||||
"\x03api\x12]\n" +
|
||||
"\x04List\x12\x12.flv.ReqRecordList\x1a\x1a.global.RecordResponseList\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/flv/api/list/{streamPath=**}\x12T\n" +
|
||||
"\aCatalog\x12\x16.google.protobuf.Empty\x1a\x17.global.ResponseCatalog\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/flv/api/catalog\x12b\n" +
|
||||
"\x06Delete\x12\x14.flv.ReqRecordDelete\x1a\x16.global.ResponseDelete\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/flv/api/delete/{streamPath=**}B\x1bZ\x19m7s.live/v5/plugin/flv/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_flv_proto_rawDescOnce sync.Once
|
||||
file_flv_proto_rawDescData = file_flv_proto_rawDesc
|
||||
file_flv_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_flv_proto_rawDescGZIP() []byte {
|
||||
file_flv_proto_rawDescOnce.Do(func() {
|
||||
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(file_flv_proto_rawDescData)
|
||||
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)))
|
||||
})
|
||||
return file_flv_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_flv_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_flv_proto_goTypes = []interface{}{
|
||||
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
|
||||
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
(*pb.ResponseList)(nil), // 3: global.ResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
|
||||
var file_flv_proto_goTypes = []any{
|
||||
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
|
||||
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
(*pb.RecordResponseList)(nil), // 3: global.RecordResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
|
||||
}
|
||||
var file_flv_proto_depIdxs = []int32{
|
||||
0, // 0: flv.api.List:input_type -> flv.ReqRecordList
|
||||
2, // 1: flv.api.Catalog:input_type -> google.protobuf.Empty
|
||||
1, // 2: flv.api.Delete:input_type -> flv.ReqRecordDelete
|
||||
3, // 3: flv.api.List:output_type -> global.ResponseList
|
||||
3, // 3: flv.api.List:output_type -> global.RecordResponseList
|
||||
4, // 4: flv.api.Catalog:output_type -> global.ResponseCatalog
|
||||
5, // 5: flv.api.Delete:output_type -> global.ResponseDelete
|
||||
3, // [3:6] is the sub-list for method output_type
|
||||
@@ -295,37 +262,11 @@ func file_flv_proto_init() {
|
||||
if File_flv_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_flv_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_flv_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
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_flv_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
@@ -336,7 +277,6 @@ func file_flv_proto_init() {
|
||||
MessageInfos: file_flv_proto_msgTypes,
|
||||
}.Build()
|
||||
File_flv_proto = out.File
|
||||
file_flv_proto_rawDesc = nil
|
||||
file_flv_proto_goTypes = nil
|
||||
file_flv_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ package flv;
|
||||
option go_package="m7s.live/v5/plugin/flv/pb";
|
||||
|
||||
service api {
|
||||
rpc List (ReqRecordList) returns (global.ResponseList) {
|
||||
rpc List (ReqRecordList) returns (global.RecordResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/flv/api/list/{streamPath=**}"
|
||||
};
|
||||
|
||||
@@ -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.29.3
|
||||
// source: flv.proto
|
||||
|
||||
package pb
|
||||
@@ -17,14 +17,20 @@ 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 = "/flv.api/List"
|
||||
Api_Catalog_FullMethodName = "/flv.api/Catalog"
|
||||
Api_Delete_FullMethodName = "/flv.api/Delete"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ApiClient interface {
|
||||
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error)
|
||||
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error)
|
||||
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error)
|
||||
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
|
||||
}
|
||||
@@ -37,9 +43,10 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
|
||||
return &apiClient{cc}
|
||||
}
|
||||
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
|
||||
out := new(pb.ResponseList)
|
||||
err := c.cc.Invoke(ctx, "/flv.api/List", in, out, opts...)
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.RecordResponseList)
|
||||
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,8 +54,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, "/flv.api/Catalog", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,8 +64,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, "/flv.api/Delete", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,19 +75,22 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
|
||||
|
||||
// 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)
|
||||
List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error)
|
||||
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
|
||||
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, 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) {
|
||||
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
|
||||
@@ -88,6 +100,7 @@ func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.Res
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Delete 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
|
||||
@@ -97,6 +110,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)
|
||||
}
|
||||
|
||||
@@ -110,7 +130,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/List",
|
||||
FullMethod: Api_List_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
|
||||
@@ -128,7 +148,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/Catalog",
|
||||
FullMethod: Api_Catalog_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
|
||||
@@ -146,7 +166,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/Delete",
|
||||
FullMethod: Api_Delete_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))
|
||||
|
||||
@@ -2,6 +2,7 @@ package flv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg/util"
|
||||
@@ -15,6 +16,10 @@ type Puller struct {
|
||||
func (p *Puller) Run() (err error) {
|
||||
reader := util.NewBufReader(p.ReadCloser)
|
||||
publisher := p.PullJob.Publisher
|
||||
if publisher == nil {
|
||||
io.Copy(io.Discard, p.ReadCloser)
|
||||
return
|
||||
}
|
||||
var hasAudio, hasVideo bool
|
||||
var absTS uint32
|
||||
var head util.Memory
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
m7s "m7s.live/v5"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
@@ -47,6 +48,9 @@ func (p *RecordReader) Dispose() {
|
||||
func (p *RecordReader) Run() (err error) {
|
||||
pullJob := &p.PullJob
|
||||
publisher := pullJob.Publisher
|
||||
if publisher == nil {
|
||||
return pkg.ErrDisabled
|
||||
}
|
||||
allocator := util.NewScalableMemoryAllocator(1 << 10)
|
||||
var tagHeader [11]byte
|
||||
var ts int64
|
||||
@@ -60,6 +64,7 @@ func (p *RecordReader) Run() (err error) {
|
||||
publisher.OnGetPosition = func() time.Time {
|
||||
return realTime
|
||||
}
|
||||
|
||||
for loop := 0; loop < p.Loop; loop++ {
|
||||
nextStream:
|
||||
for i, stream := range p.Streams {
|
||||
@@ -85,15 +90,15 @@ func (p *RecordReader) Run() (err error) {
|
||||
err = head.NewReader().ReadByteTo(&flvHead[0], &flvHead[1], &flvHead[2], &version, &flag)
|
||||
hasAudio := (flag & 0x04) != 0
|
||||
hasVideo := (flag & 0x01) != 0
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !hasAudio {
|
||||
publisher.NoAudio()
|
||||
}
|
||||
if !hasVideo {
|
||||
publisher.NoVideo()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if flvHead != [3]byte{'F', 'L', 'V'} {
|
||||
return errors.New("not flv file")
|
||||
}
|
||||
@@ -194,7 +199,7 @@ func (p *RecordReader) Run() (err error) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
publisher.Info("script", name, obj)
|
||||
p.Info("script", name, obj)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unknown tag type: %d", t)
|
||||
|
||||
@@ -5,15 +5,12 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
rtmp "m7s.live/v5/plugin/rtmp/pkg"
|
||||
)
|
||||
|
||||
@@ -144,7 +141,8 @@ func NewRecorder(conf config.Record) m7s.IRecorder {
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
stream m7s.RecordStream
|
||||
writer *FlvWriter
|
||||
file *os.File
|
||||
}
|
||||
|
||||
var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
@@ -155,48 +153,34 @@ var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
}
|
||||
|
||||
func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
r.stream = m7s.RecordStream{
|
||||
StartTime: start,
|
||||
StreamPath: sub.StreamPath,
|
||||
FilePath: CustomFileName(&r.RecordJob),
|
||||
EventId: recordJob.EventId,
|
||||
EventDesc: recordJob.EventDesc,
|
||||
EventName: recordJob.EventName,
|
||||
EventLevel: recordJob.EventLevel,
|
||||
BeforeDuration: recordJob.BeforeDuration,
|
||||
AfterDuration: recordJob.AfterDuration,
|
||||
Mode: recordJob.Mode,
|
||||
Type: "flv",
|
||||
}
|
||||
dir := filepath.Dir(r.stream.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
r.RecordJob.RecConf.Type = "flv"
|
||||
err = r.CreateStream(start, CustomFileName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
|
||||
if r.file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
_, err = r.file.Write(FLVHead)
|
||||
r.writer = NewFlvWriter(r.file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Recorder) writeTailer(end time.Time) {
|
||||
if r.stream.EndTime.After(r.stream.StartTime) {
|
||||
if r.Event.EndTime.After(r.Event.StartTime) {
|
||||
return
|
||||
}
|
||||
r.stream.EndTime = end
|
||||
r.Event.EndTime = end
|
||||
if r.RecordJob.Plugin.DB != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.stream)
|
||||
writeMetaTagQueueTask.AddTask(&eventRecordCheck{
|
||||
DB: r.RecordJob.Plugin.DB,
|
||||
streamPath: r.stream.StreamPath,
|
||||
})
|
||||
if r.RecordJob.Event != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event)
|
||||
} else {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event.RecordStream)
|
||||
}
|
||||
writeMetaTagQueueTask.AddTask(m7s.NewEventRecordCheck(r.Event.Type, r.Event.StreamPath, r.RecordJob.Plugin.DB))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,42 +188,7 @@ func (r *Recorder) Dispose() {
|
||||
r.writeTailer(time.Now())
|
||||
}
|
||||
|
||||
type eventRecordCheck struct {
|
||||
task.Task
|
||||
DB *gorm.DB
|
||||
streamPath string
|
||||
}
|
||||
|
||||
func (t *eventRecordCheck) Run() (err error) {
|
||||
var eventRecordStreams []m7s.RecordStream
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelHigh,
|
||||
Mode: m7s.RecordModeEvent,
|
||||
Type: "flv",
|
||||
}
|
||||
t.DB.Where(&queryRecord).Find(&eventRecordStreams, "stream_path=?", t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
|
||||
if len(eventRecordStreams) > 0 {
|
||||
for _, recordStream := range eventRecordStreams {
|
||||
var unimportantEventRecordStreams []m7s.RecordStream
|
||||
queryRecord.EventLevel = m7s.EventLevelLow
|
||||
query := `(start_time BETWEEN ? AND ?)
|
||||
OR (end_time BETWEEN ? AND ?)
|
||||
OR (? BETWEEN start_time AND end_time)
|
||||
OR (? BETWEEN start_time AND end_time) AND stream_path=? `
|
||||
t.DB.Where(&queryRecord).Where(query, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StreamPath).Find(&unimportantEventRecordStreams)
|
||||
if len(unimportantEventRecordStreams) > 0 {
|
||||
for _, unimportantEventRecordStream := range unimportantEventRecordStreams {
|
||||
unimportantEventRecordStream.EventLevel = m7s.EventLevelHigh
|
||||
t.DB.Save(&unimportantEventRecordStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Recorder) Run() (err error) {
|
||||
var file *os.File
|
||||
var filepositions []uint64
|
||||
var times []float64
|
||||
var offset int64
|
||||
@@ -247,82 +196,27 @@ func (r *Recorder) Run() (err error) {
|
||||
ctx := &r.RecordJob
|
||||
suber := ctx.Subscriber
|
||||
noFragment := ctx.RecConf.Fragment == 0 || ctx.RecConf.Append
|
||||
startTime := time.Now()
|
||||
if ctx.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-ctx.BeforeDuration)
|
||||
}
|
||||
if err = r.createStream(startTime); err != nil {
|
||||
return
|
||||
}
|
||||
if noFragment {
|
||||
file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR|util.Conditional(ctx.RecConf.Append, os.O_APPEND, os.O_TRUNC), 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer writeMetaTag(file, suber, filepositions, times, &duration)
|
||||
}
|
||||
if ctx.RecConf.Append {
|
||||
var metaData rtmp.EcmaArray
|
||||
metaData, err = ReadMetaData(file)
|
||||
keyframes := metaData["keyframes"].(map[string]any)
|
||||
filepositions = slices.Collect(func(yield func(uint64) bool) {
|
||||
for _, v := range keyframes["filepositions"].([]float64) {
|
||||
yield(uint64(v))
|
||||
}
|
||||
})
|
||||
times = keyframes["times"].([]float64)
|
||||
if _, err = file.Seek(-4, io.SeekEnd); err != nil {
|
||||
ctx.Error("seek file failed", "err", err)
|
||||
_, err = file.Write(FLVHead)
|
||||
} else {
|
||||
tmp := make(util.Buffer, 4)
|
||||
tmp2 := tmp
|
||||
_, err = file.Read(tmp)
|
||||
tagSize := tmp.ReadUint32()
|
||||
tmp = tmp2
|
||||
_, err = file.Seek(int64(tagSize), io.SeekEnd)
|
||||
_, err = file.Read(tmp2)
|
||||
ts := tmp2.ReadUint24() | (uint32(tmp[3]) << 24)
|
||||
ctx.Info("append flv", "last tagSize", tagSize, "last ts", ts)
|
||||
suber.StartAudioTS = time.Duration(ts) * time.Millisecond
|
||||
suber.StartVideoTS = time.Duration(ts) * time.Millisecond
|
||||
offset, err = file.Seek(0, io.SeekEnd)
|
||||
}
|
||||
} else if ctx.RecConf.Fragment == 0 {
|
||||
_, err = file.Write(FLVHead)
|
||||
} else {
|
||||
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = file.Write(FLVHead)
|
||||
}
|
||||
writer := NewFlvWriter(file)
|
||||
checkFragment := func(absTime uint32) {
|
||||
checkFragment := func(absTime uint32, writeTime time.Time) {
|
||||
if duration = int64(absTime); time.Duration(duration)*time.Millisecond >= ctx.RecConf.Fragment {
|
||||
writeMetaTag(file, suber, filepositions, times, &duration)
|
||||
r.writeTailer(time.Now())
|
||||
writeMetaTag(r.file, suber, filepositions, times, &duration)
|
||||
r.writeTailer(writeTime)
|
||||
filepositions = []uint64{0}
|
||||
times = []float64{0}
|
||||
offset = 0
|
||||
if err = r.createStream(time.Now()); err != nil {
|
||||
if err = r.createStream(writeTime); err != nil {
|
||||
return
|
||||
}
|
||||
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = file.Write(FLVHead)
|
||||
writer = NewFlvWriter(file)
|
||||
if vr := suber.VideoReader; vr != nil {
|
||||
vr.ResetAbsTime()
|
||||
seq := vr.Track.SequenceFrame.(*rtmp.RTMPVideo)
|
||||
err = writer.WriteTag(FLV_TAG_TYPE_VIDEO, 0, uint32(seq.Size), seq.Buffers...)
|
||||
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, 0, uint32(seq.Size), seq.Buffers...)
|
||||
offset = int64(seq.Size + 15)
|
||||
}
|
||||
if ar := suber.AudioReader; ar != nil {
|
||||
ar.ResetAbsTime()
|
||||
if ar.Track.SequenceFrame != nil {
|
||||
seq := ar.Track.SequenceFrame.(*rtmp.RTMPAudio)
|
||||
err = writer.WriteTag(FLV_TAG_TYPE_AUDIO, 0, uint32(seq.Size), seq.Buffers...)
|
||||
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, 0, uint32(seq.Size), seq.Buffers...)
|
||||
offset += int64(seq.Size + 15)
|
||||
}
|
||||
}
|
||||
@@ -330,21 +224,33 @@ func (r *Recorder) Run() (err error) {
|
||||
}
|
||||
|
||||
return m7s.PlayBlock(ctx.Subscriber, func(audio *rtmp.RTMPAudio) (err error) {
|
||||
if suber.VideoReader == nil && !noFragment {
|
||||
checkFragment(suber.AudioReader.AbsTime)
|
||||
if r.Event.StartTime.IsZero() {
|
||||
err = r.createStream(suber.AudioReader.Value.WriteTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = writer.WriteTag(FLV_TAG_TYPE_AUDIO, suber.AudioReader.AbsTime, uint32(audio.Size), audio.Buffers...)
|
||||
if suber.VideoReader == nil && !noFragment {
|
||||
checkFragment(suber.AudioReader.AbsTime, suber.AudioReader.Value.WriteTime)
|
||||
}
|
||||
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, suber.AudioReader.AbsTime, uint32(audio.Size), audio.Buffers...)
|
||||
offset += int64(audio.Size + 15)
|
||||
return
|
||||
}, func(video *rtmp.RTMPVideo) (err error) {
|
||||
if r.Event.StartTime.IsZero() {
|
||||
err = r.createStream(suber.VideoReader.Value.WriteTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if suber.VideoReader.Value.IDR {
|
||||
filepositions = append(filepositions, uint64(offset))
|
||||
times = append(times, float64(suber.VideoReader.AbsTime)/1000)
|
||||
if !noFragment {
|
||||
checkFragment(suber.VideoReader.AbsTime)
|
||||
checkFragment(suber.VideoReader.AbsTime, suber.VideoReader.Value.WriteTime)
|
||||
}
|
||||
}
|
||||
err = writer.WriteTag(FLV_TAG_TYPE_VIDEO, suber.VideoReader.AbsTime, uint32(video.Size), video.Buffers...)
|
||||
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, suber.VideoReader.AbsTime, uint32(video.Size), video.Buffers...)
|
||||
offset += int64(video.Size + 15)
|
||||
return
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user