Compare commits

...

24 Commits

Author SHA1 Message Date
banshan
f89c279388 fix: onvif pull stream 2025-01-15 15:44:51 +08:00
banshan
14a76cd7cf fix: rename function name 2025-01-15 15:09:39 +08:00
banshan
c66f0b7147 feat: add onvif pluign 2025-01-15 15:09:39 +08:00
banshan
fc6d4645e3 feat: add mutillang convert to en 2025-01-15 15:09:39 +08:00
langhuihui
064e84ee53 doc: update readme 2025-01-15 15:07:52 +08:00
langhuihui
99274b104d fix: publisher pause twice 2025-01-15 13:23:52 +08:00
langhuihui
a0648f4086 fix: alias db 2025-01-15 10:25:14 +08:00
langhuihui
44eb5d4ed4 fix: range remove 2025-01-14 19:16:01 +08:00
langhuihui
413b83c215 fix: alias bug 2025-01-14 18:56:58 +08:00
langhuihui
8effa750c5 feat: add scale and drop 2025-01-13 20:28:29 +08:00
langhuihui
1b58ad3fdd fix: flv seek 2025-01-13 17:16:31 +08:00
langhuihui
5892b2e0c4 fix: flv pull record seek 2025-01-13 10:19:18 +08:00
langhuihui
af2a7ccf5f fix: pull add reg match 2025-01-12 21:55:42 +08:00
langhuihui
d80dac852b fix: get recordlist with out pageNum 2025-01-10 11:01:27 +08:00
langhuihui
2960f99b7b feat: add location like nginx 2025-01-09 17:56:14 +08:00
langhuihui
32d158372a fix: flv pull record 2025-01-08 20:53:12 +08:00
langhuihui
777004b404 chore: alais primarykey 2025-01-08 17:14:57 +08:00
langhuihui
ebb188e7ae fix: switch alias 2025-01-08 15:27:06 +08:00
langhuihui
baf79cfc7f feat: alias store to db 2025-01-08 10:45:17 +08:00
langhuihui
373a885e12 feat: add task doc 2025-01-07 16:57:21 +08:00
pg
7223935247 fix: new fragment when h264 resolution has changed 2025-01-07 15:34:55 +08:00
langhuihui
142d9c26a6 feat: add flv recorder 2025-01-07 13:14:30 +08:00
langhuihui
0b1bd41192 feat: add init push proxies 2025-01-06 08:58:00 +08:00
banshan
8bad53bffc fix: snap i-frame interval fontspacing config 2025-01-05 16:47:24 +08:00
65 changed files with 8147 additions and 2243 deletions

165
README.md
View File

@@ -1,26 +1,105 @@
<!-- Improved compatibility of back to top link -->
<a id="readme-top"></a>
# Introduction
Monibuca is a highly scalable high-performance streaming server development framework developed purely for Go
# Usage
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![MIT License][license-shield]][license-url]
```go
package main
<!-- PROJECT LOGO -->
<br />
<div align="center">
<h1 align="center">Monibuca v5</h1>
import (
"context"
<p align="center">
A highly scalable high-performance streaming server development framework developed purely in Go
<br />
<a href="./README_CN.md">中文文档</a>
·
<a href="https://github.com/Monibuca/v5/wiki"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="https://github.com/Monibuca/v5/issues">Report Bug</a>
·
<a href="https://github.com/Monibuca/v5/issues">Request Feature</a>
</p>
</div>
"m7s.live/v5"
_ "m7s.live/v5/plugin/debug"
_ "m7s.live/v5/plugin/flv"
_ "m7s.live/v5/plugin/rtmp"
)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li><a href="#about">About</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#usage">Usage</a></li>
<li><a href="#build-tags">Build Tags</a></li>
<li><a href="#monitoring">Monitoring</a></li>
<li><a href="#plugin-development">Plugin Development</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
</ol>
</details>
func main() {
m7s.Run(context.Background(), "config.yaml")
}
## About
Monibuca is a powerful streaming server framework written entirely in Go. It's designed to be:
- 🚀 **High Performance** - Lock-free design, partial manual memory management, multi-core computing
-**Low Latency** - Zero-wait forwarding, sub-second latency across the entire chain
- 📦 **Modular** - Load on demand, unlimited extensibility
- 🔧 **Flexible** - Highly configurable to meet various streaming scenarios
- 💪 **Scalable** - Supports distributed deployment, easily handles large-scale scenarios
- 🔍 **Debug Friendly** - Built-in debug plugin, real-time performance monitoring and analysis
- 🎥 **Media Processing** - Supports screenshot, transcoding, SEI data processing
- 🔄 **Cluster Capability** - Built-in cascade and room management
- 🎮 **Preview Features** - Supports video preview, multi-screen preview, custom screen layouts
- 🔐 **Security** - Provides encrypted transmission and stream authentication
- 📊 **Performance Monitoring** - Supports stress testing and performance metrics collection
- 📝 **Log Management** - Log rotation, auto cleanup, custom extensions
- 🎬 **Recording & Playback** - Supports MP4, HLS, FLV formats, speed control, seeking, pause
- ⏱️ **Dynamic Time-Shift** - Dynamic cache design, supports live time-shift playback
- 🌐 **Remote Call** - Supports gRPC interface for cross-language integration
- 🏷️ **Stream Alias** - Supports dynamic stream alias, flexible multi-stream management
- 🤖 **AI Capabilities** - Integrated inference engine, ONNX model support, custom pre/post processing
- 🪝 **WebHook** - Subscribe to stream lifecycle events for business system integration
- 🔒 **Private Protocol** - Supports custom private protocols for special business needs
- 🔄 **Supported Protocols**: RTMP, RTSP, HTTP-FLV, WS-FLV, HLS, WebRTC, GB28181, ONVIF, SRT
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Getting Started
### Prerequisites
- Go 1.23 or higher
- Basic understanding of streaming protocols
### Run with Default Configuration
```bash
cd example/default
go run -tags sqlite main.go
```
## build tags
### Web UI
Place the `admin.zip` file (do not unzip) in the same directory as your configuration file.
Then visit http://localhost:8080 to access the UI.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Examples
For more examples, please check out the [example](./example) documentation.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Build Tags
The following build tags can be used to customize your build:
| Build Tag | Description |
|-----------|-------------|
@@ -32,11 +111,11 @@ func main() {
| duckdb | Enables the duckdb DB |
| taskpanic | Throws panic, for testing |
## More Example
<p align="right">(<a href="#readme-top">back to top</a>)</p>
see example directory
## Monitoring
# Prometheus
Monibuca supports Prometheus monitoring out of the box. Add the following to your Prometheus configuration:
```yaml
scrape_configs:
@@ -46,6 +125,50 @@ scrape_configs:
- targets: ["localhost:8080"]
```
# Create Plugin
<p align="right">(<a href="#readme-top">back to top</a>)</p>
see [plugin](./plugin/README.md)
## Plugin Development
Monibuca's functionality can be extended through plugins. For information on creating plugins, see the [plugin guide](./plugin/README.md).
<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**.
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## License
Distributed under the MIT License. See `LICENSE` for more information.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- CONTACT -->
## Contact
monibuca - [@m7server](https://x.com/m7server) - service@monibuca.com
Project Link: [https://github.com/langhuihui/monibuca](https://github.com/langhuihui/monibuca)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
[contributors-shield]: https://img.shields.io/github/contributors/langhuihui/monibuca.svg?style=for-the-badge
[contributors-url]: https://github.com/langhuihui/monibuca/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/langhuihui/monibuca.svg?style=for-the-badge
[forks-url]: https://github.com/langhuihui/monibuca/network/members
[stars-shield]: https://img.shields.io/github/stars/langhuihui/monibuca.svg?style=for-the-badge
[stars-url]: https://github.com/langhuihui/monibuca/stargazers
[issues-shield]: https://img.shields.io/github/issues/langhuihui/monibuca.svg?style=for-the-badge
[issues-url]: https://github.com/langhuihui/monibuca/issues
[license-shield]: https://img.shields.io/github/license/langhuihui/monibuca.svg?style=for-the-badge
[license-url]: https://github.com/langhuihui/monibuca/blob/v5/LICENSE

View File

@@ -1,114 +1,123 @@
# Monibuca v5
<a id="readme-top"></a>
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![AGPL License][license-shield]][license-url]
[![Go Reference](https://pkg.go.dev/badge/m7s.live/v5.svg)](https://pkg.go.dev/m7s.live/v5)
Monibuca简称 m7s是一款纯 Go 开发的开源流媒体服务器开发框架,支持多种流媒体协议。
<br />
<div align="center">
<a href="https://monibuca.com">
<img src="https://monibuca.com/svg/logo.svg" alt="Logo" width="200">
</a>
## 特性
<h1 align="center">Monibuca v5</h1>
<p align="center">
强大的纯 Go 开发的流媒体服务器开发框架
<br />
<a href="https://monibuca.com"><strong>官方网站 »</strong></a>
<br />
<br />
<a href="https://github.com/langhuihui/monibuca/issues">报告问题</a>
·
<a href="https://github.com/langhuihui/monibuca/issues">功能建议</a>
</p>
</div>
- 🚀 高性能:采用纯 Go 开发,充分利用 Go 的并发特性
- 🔌 插件化架构:核心功能都以插件形式提供,可按需加载
- 🛠 可扩展性强:支持自定义插件开发
- 📽 多协议支持:
- RTMP
- HTTP-FLV
- HLS
- WebRTC
- GB28181
- SRT
- 🎯 低延迟:针对实时性场景优化
- 📊 实时监控:支持 Prometheus 监控集成
- 🔄 集群支持:支持分布式部署
<!-- 目录 -->
<details>
<summary>目录</summary>
<ol>
<li><a href="#项目介绍">项目介绍</a></li>
<li><a href="#快速开始">快速开始</a></li>
<li><a href="#使用示例">使用示例</a></li>
<li><a href="#构建选项">构建选项</a></li>
<li><a href="#监控系统">监控系统</a></li>
<li><a href="#插件开发">插件开发</a></li>
<li><a href="#贡献指南">贡献指南</a></li>
<li><a href="#许可证">许可证</a></li>
</ol>
</details>
## 项目介绍
Monibuca简称 m7s是一款纯 Go 开发的开源流媒体服务器开发框架。它具有以下特点:
- 🚀 **高性能** - 无锁设计、部分手动管理内存、多核计算
-**低延迟** - 0 等待转发、全链路亚秒级延迟
- 📦 **插件化** - 按需加载,无限扩展能力
- 🔧 **灵活性** - 高度可配置,满足各种流媒体场景需求
- 💪 **可扩展** - 支持分布式部署,轻松应对大规模场景
- 🔍 **调试友好** - 内置调试插件,支持实时性能监控和分析
- 🎥 **媒体处理** - 支持截图、转码、SEI 数据处理
- 🔄 **集群能力** - 内置级联和房间管理功能
- 🎮 **预览功能** - 支持视频预览、分屏预览、自定义分屏
- 🔐 **安全加密** - 提供加密传输和流鉴权能力
- 📊 **性能监控** - 支持压力测试和性能指标采集
- 📝 **日志管理** - 日志轮转、自动清理、自定义扩展
- 🎬 **录制回放** - 支持 MP4、HLS、FLV 格式录制、倍速播放、拖拽快进、暂停能力
- ⏱️ **动态时移** - 动态缓存设计,支持直播时移回看
- 🌐 **远程调用** - 支持 gRPC 接口,方便跨语言集成
- 🏷️ **流别名** - 支持动态设置流别名,灵活管理多路流,实现导播功能
- 🤖 **AI 能力** - 集成推理引擎,支持 ONNX 模型,支持自定义的前置处理,后置处理,以及画框
- 🪝 **WebHook** - 支持订阅流的生命周期事件,实现业务系统联动
- 🔒 **私有协议** - 支持自定义私有协议,满足特殊业务需求
- 🔄 **多协议支持**RTMP、RTSP、HTTP-FLV、WS-FLV、HLS、WebRTC、GB28181、ONVIF、SRT
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 快速开始
### 安装
### 环境要求
1. 确保已安装 Go 1.23 或更高版本
2. 创建新项目并初始化:
- Go 1.23 或更高版本
- 了解基本的流媒体协议
### 运行默认配置
```bash
mkdir my-m7s-server && cd my-m7s-server
go mod init my-m7s-server
cd example/default
go run -tags sqlite main.go
```
### UI 界面
3. 创建主程序:
将 admin.zip (不要解压)放在和配置文件相同目录下。
```go
package main
然后访问 http://localhost:8080 即可。
import (
"context"
"m7s.live/v5"
_ "m7s.live/v5/plugin/cascade"
_ "m7s.live/v5/plugin/debug"
_ "m7s.live/v5/plugin/flv"
_ "m7s.live/v5/plugin/gb28181"
_ "m7s.live/v5/plugin/hls"
_ "m7s.live/v5/plugin/logrotate"
_ "m7s.live/v5/plugin/monitor"
_ "m7s.live/v5/plugin/mp4"
_ "m7s.live/v5/plugin/preview"
_ "m7s.live/v5/plugin/rtmp"
_ "m7s.live/v5/plugin/rtsp"
_ "m7s.live/v5/plugin/sei"
_ "m7s.live/v5/plugin/snap"
_ "m7s.live/v5/plugin/srt"
_ "m7s.live/v5/plugin/stress"
_ "m7s.live/v5/plugin/transcode"
_ "m7s.live/v5/plugin/webrtc"
)
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
func main() {
m7s.Run(context.Background(), "config.yaml")
}
```
## 使用示例
### 配置说明
更多示例请查看 [example](./example/READEME_CN.md) 文档。
创建 `config.yaml` 配置文件:
```yaml
# 全局配置
global:
http: :8080
# 插件配置
rtmp:
tcp: :1935
```
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 构建选项
| 构建标签 | 描述 |
| ---------- | ---------------------- |
| disable_rm | 禁用内存池 |
| sqlite | 启用 SQLite 存储 |
| sqliteCGO | 启用 SQLite CGO 版本 |
| mysql | 启用 MySQL 存储 |
| postgres | 启用 PostgreSQL 存储 |
| duckdb | 启用 DuckDB 存储 |
| taskpanic | 抛出 panic用于测试 |
可以使用以下构建标签来自定义构建:
## 项目结构
| 构建标签 | 描述 |
|----------|------|
| disable_rm | 禁用内存池 |
| sqlite | 启用 SQLite 存储 |
| sqliteCGO | 启用 SQLite CGO 版本 |
| mysql | 启用 MySQL 存储 |
| postgres | 启用 PostgreSQL 存储 |
| duckdb | 启用 DuckDB 存储 |
| taskpanic | 抛出 panic用于测试 |
```
monibuca/
├── plugin/ # 官方插件目录
├── pkg/ # 核心包
├── example/ # 示例代码
├── doc/ # 文档
└── scripts/ # 实用脚本
```
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 插件开发
## 监控系统
查看 [plugin/README_CN.md](./plugin/README_CN.md) 了解如何开发自定义插件。
## Prometheus 监控
配置 Prometheus
Monibuca 内置支持 Prometheus 监控。在 Prometheus 配置中添加:
```yaml
scrape_configs:
@@ -118,20 +127,48 @@ scrape_configs:
- targets: ["localhost:8080"]
```
## 示例
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
更多使用示例请查看 [example](./example) 目录。
## 插件开发
Monibuca 支持通过插件扩展功能。查看[插件开发指南](./plugin/README_CN.md)了解详情。
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 贡献指南
欢迎提交 Pull Request 或 Issue。
我们非常欢迎社区贡献,您的参与将使开源社区变得更加精彩!
1. Fork 本项目
2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交您的修改 (`git commit -m '添加一些特性'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 发起 Pull Request
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 许可证
本项目采用 AGPL 许可证,详见 [LICENSE](./LICENSE) 文件。
## 相关资源
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
- [官方文档](https://docs.m7s.live/)
- [API 参考](https://pkg.go.dev/m7s.live/v5)
- [示例代码](./example)
## 联系方式
- 微信公众号:不卡科技
- QQ群751639168
- QQ频道p0qq0crz08
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
[contributors-shield]: https://img.shields.io/github/contributors/langhuihui/monibuca.svg?style=for-the-badge
[contributors-url]: https://github.com/langhuihui/monibuca/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/langhuihui/monibuca.svg?style=for-the-badge
[forks-url]: https://github.com/langhuihui/monibuca/network/members
[stars-shield]: https://img.shields.io/github/stars/langhuihui/monibuca.svg?style=for-the-badge
[stars-url]: https://github.com/langhuihui/monibuca/stargazers
[issues-shield]: https://img.shields.io/github/issues/langhuihui/monibuca.svg?style=for-the-badge
[issues-url]: https://github.com/langhuihui/monibuca/issues
[license-shield]: https://img.shields.io/github/license/langhuihui/monibuca.svg?style=for-the-badge
[license-url]: https://github.com/langhuihui/monibuca/blob/v5/LICENSE

252
alias.go Normal file
View File

@@ -0,0 +1,252 @@
package m7s
import (
"context"
"net/url"
"strings"
"time"
"google.golang.org/protobuf/types/known/emptypb"
"m7s.live/v5/pb"
)
type AliasStream struct {
*Publisher `gorm:"-:all"`
AutoRemove bool
StreamPath string
Alias string `gorm:"primarykey"`
}
func (a *AliasStream) GetKey() string {
return a.Alias
}
// StreamAliasDB 用于存储流别名的数据库模型
type StreamAliasDB struct {
AliasStream
CreatedAt time.Time `yaml:"-"`
UpdatedAt time.Time `yaml:"-"`
}
func (StreamAliasDB) TableName() string {
return "stream_alias"
}
func (s *Server) initStreamAlias() {
if s.DB == nil {
return
}
var aliases []StreamAliasDB
s.DB.Find(&aliases)
for _, alias := range aliases {
s.AliasStreams.Add(&alias.AliasStream)
if publisher, ok := s.Streams.Get(alias.StreamPath); ok {
alias.Publisher = publisher
}
}
}
func (s *Server) GetStreamAlias(ctx context.Context, req *emptypb.Empty) (res *pb.StreamAliasListResponse, err error) {
res = &pb.StreamAliasListResponse{}
s.Streams.Call(func() error {
for alias := range s.AliasStreams.Range {
info := &pb.StreamAlias{
StreamPath: alias.StreamPath,
Alias: alias.Alias,
AutoRemove: alias.AutoRemove,
}
if s.Streams.Has(alias.Alias) {
info.Status = 2
} else if alias.Publisher != nil {
info.Status = 1
}
res.Data = append(res.Data, info)
}
return nil
})
return
}
func (s *Server) SetStreamAlias(ctx context.Context, req *pb.SetStreamAliasRequest) (res *pb.SuccessResponse, err error) {
res = &pb.SuccessResponse{}
s.Streams.Call(func() error {
if req.StreamPath != "" {
u, err := url.Parse(req.StreamPath)
if err != nil {
return err
}
req.StreamPath = strings.TrimPrefix(u.Path, "/")
publisher, canReplace := s.Streams.Get(req.StreamPath)
if !canReplace {
defer s.OnSubscribe(req.StreamPath, u.Query())
}
if aliasInfo, ok := s.AliasStreams.Get(req.Alias); ok { //modify alias
oldStreamPath := aliasInfo.StreamPath
aliasInfo.AutoRemove = req.AutoRemove
if aliasInfo.StreamPath != req.StreamPath {
aliasInfo.StreamPath = req.StreamPath
if canReplace {
if aliasInfo.Publisher != nil {
aliasInfo.TransferSubscribers(publisher) // replace stream
aliasInfo.Publisher = publisher
} else {
aliasInfo.Publisher = publisher
s.Waiting.WakeUp(req.Alias, publisher)
}
}
}
// 更新数据库中的别名
if s.DB != nil {
s.DB.Where("alias = ?", req.Alias).Assign(aliasInfo).FirstOrCreate(&StreamAliasDB{
AliasStream: *aliasInfo,
})
}
s.Info("modify alias", "alias", req.Alias, "oldStreamPath", oldStreamPath, "streamPath", req.StreamPath, "replace", ok && canReplace)
} else { // create alias
aliasInfo := AliasStream{
AutoRemove: req.AutoRemove,
StreamPath: req.StreamPath,
Alias: req.Alias,
}
var pubId uint32
s.AliasStreams.Add(&aliasInfo)
aliasStream, ok := s.Streams.Get(aliasInfo.Alias)
if canReplace {
aliasInfo.Publisher = publisher
if ok {
aliasStream.TransferSubscribers(publisher) // replace stream
} else {
s.Waiting.WakeUp(req.Alias, publisher)
}
} else if ok {
aliasInfo.Publisher = aliasStream
}
if aliasInfo.Publisher != nil {
pubId = aliasInfo.Publisher.ID
}
// 保存到数据库
if s.DB != nil {
s.DB.Create(&StreamAliasDB{
AliasStream: aliasInfo,
})
}
s.Info("add alias", "alias", req.Alias, "streamPath", req.StreamPath, "replace", ok && canReplace, "pub", pubId)
}
} else {
s.Info("remove alias", "alias", req.Alias)
if aliasStream, ok := s.AliasStreams.Get(req.Alias); ok {
s.AliasStreams.Remove(aliasStream)
// 从数据库中删除
if s.DB != nil {
s.DB.Where("alias = ?", req.Alias).Delete(&StreamAliasDB{})
}
if aliasStream.Publisher != nil {
if publisher, hasTarget := s.Streams.Get(req.Alias); hasTarget { // restore stream
aliasStream.TransferSubscribers(publisher)
} else {
var args url.Values
for sub := range aliasStream.Publisher.SubscriberRange {
if sub.StreamPath == req.Alias {
aliasStream.Publisher.RemoveSubscriber(sub)
s.Waiting.Wait(sub)
args = sub.Args
}
}
if args != nil {
s.OnSubscribe(req.Alias, args)
}
}
}
}
}
return nil
})
return
}
func (p *Publisher) processAliasOnStart() {
s := p.Plugin.Server
for alias := range s.AliasStreams.Range {
if alias.StreamPath != p.StreamPath {
continue
}
if alias.Publisher == nil {
alias.Publisher = p
s.Waiting.WakeUp(alias.Alias, p)
} else if alias.Publisher.StreamPath != alias.StreamPath {
alias.Publisher.TransferSubscribers(p)
alias.Publisher = p
}
}
}
func (p *Publisher) processAliasOnDispose() {
s := p.Plugin.Server
var relatedAlias []*AliasStream
for alias := range s.AliasStreams.Range {
if alias.StreamPath == p.StreamPath {
if alias.AutoRemove {
defer s.AliasStreams.Remove(alias)
if s.DB != nil {
defer s.DB.Where("alias = ?", alias.Alias).Delete(&StreamAliasDB{})
}
}
alias.Publisher = nil
relatedAlias = append(relatedAlias, alias)
}
}
if p.Subscribers.Length > 0 {
SUBSCRIBER:
for subscriber := range p.SubscriberRange {
for _, alias := range relatedAlias {
if subscriber.StreamPath == alias.Alias {
if originStream, ok := s.Streams.Get(alias.Alias); ok {
originStream.AddSubscriber(subscriber)
continue SUBSCRIBER
}
}
}
s.Waiting.Wait(subscriber)
}
p.Subscribers.Clear()
}
}
func (s *Subscriber) processAliasOnStart() (hasInvited bool, done bool) {
server := s.Plugin.Server
if alias, ok := server.AliasStreams.Get(s.StreamPath); ok {
if alias.Publisher != nil {
alias.Publisher.AddSubscriber(s)
done = true
return
} else {
server.OnSubscribe(alias.StreamPath, s.Args)
hasInvited = true
}
} else {
for reg, alias := range server.StreamAlias {
if streamPath := reg.Replace(s.StreamPath, alias); streamPath != "" {
as := AliasStream{
StreamPath: streamPath,
Alias: s.StreamPath,
}
server.AliasStreams.Set(&as)
if server.DB != nil {
server.DB.Where("alias = ?", s.StreamPath).Assign(as).FirstOrCreate(&StreamAliasDB{
AliasStream: as,
})
}
if publisher, ok := server.Streams.Get(streamPath); ok {
publisher.AddSubscriber(s)
done = true
return
} else {
server.OnSubscribe(streamPath, s.Args)
hasInvited = true
}
break
}
}
}
return
}

534
api.go
View File

@@ -13,11 +13,9 @@ import (
"strings"
"time"
"m7s.live/v5/pkg/db"
"m7s.live/v5/pkg/task"
myip "github.com/husanpao/ip"
"github.com/mcuadros/go-defaults"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/mem"
@@ -33,12 +31,6 @@ import (
)
var localIP string
var empty = &emptypb.Empty{}
func init() {
// Add auto-migration for User model
db.AutoMigrations = append(db.AutoMigrations, &db.User{})
}
func (s *Server) SysInfo(context.Context, *emptypb.Empty) (res *pb.SysInfoResponse, err error) {
if localIP == "" {
@@ -563,6 +555,8 @@ func (s *Server) SetStreamSpeed(ctx context.Context, req *pb.SetStreamSpeedReque
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
})
@@ -812,476 +806,134 @@ func (s *Server) ModifyConfig(_ context.Context, req *pb.ModifyConfigRequest) (r
return
}
func (s *Server) GetPullProxyList(ctx context.Context, req *emptypb.Empty) (res *pb.PullProxyListResponse, err error) {
res = &pb.PullProxyListResponse{}
s.PullProxies.Call(func() error {
for device := range s.PullProxies.Range {
res.Data = append(res.Data, &pb.PullProxyInfo{
Name: device.Name,
CreateTime: timestamppb.New(device.CreatedAt),
UpdateTime: timestamppb.New(device.UpdatedAt),
Type: device.Type,
PullURL: device.URL,
ParentID: uint32(device.ParentID),
Status: uint32(device.Status),
ID: uint32(device.ID),
PullOnStart: device.PullOnStart,
StopOnIdle: device.StopOnIdle,
Audio: device.Audio,
RecordPath: device.Record.FilePath,
RecordFragment: durationpb.New(device.Record.Fragment),
Description: device.Description,
Rtt: uint32(device.RTT.Milliseconds()),
StreamPath: device.GetStreamPath(),
})
}
return nil
})
return
}
func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *pb.SuccessResponse, err error) {
device := &PullProxy{
server: s,
Name: req.Name,
Type: req.Type,
ParentID: uint(req.ParentID),
PullOnStart: req.PullOnStart,
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Pull)
defaults.SetDefaults(&device.Record)
device.URL = req.PullURL
device.Audio = req.Audio
device.StopOnIdle = req.StopOnIdle
device.Record.FilePath = req.RecordPath
device.Record.Fragment = req.RecordFragment.AsDuration()
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.ResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
s.DB.Create(device)
if req.StreamPath == "" {
device.StreamPath = device.GetStreamPath()
if req.PageSize == 0 {
req.PageSize = 10
}
s.PullProxies.Add(device)
res = &pb.SuccessResponse{}
return
}
if req.PageNum == 0 {
req.PageNum = 1
}
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
var totalCount int64 //总条数
func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
var result []*RecordStream
query := s.DB.Model(&RecordStream{})
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)
}
target := &PullProxy{
server: s,
if req.Mode != "" {
query = query.Where("mode = ?", req.Mode)
}
err = s.DB.First(target, req.ID).Error
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)
}
}
query.Count(&totalCount)
err = query.Offset(int(offset)).Limit(int(req.PageSize)).Order("start_time desc").Find(&result).Error
if err != nil {
return
}
target.Name = req.Name
target.URL = req.PullURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
resp = &pb.ResponseList{
TotalCount: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
}
target.PullOnStart = req.PullOnStart
target.StopOnIdle = req.StopOnIdle
target.Audio = req.Audio
target.Description = req.Description
target.Record.FilePath = req.RecordPath
target.Record.Fragment = req.RecordFragment.AsDuration()
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
target.StreamPath = req.StreamPath
s.DB.Save(target)
var needStopOld *PullProxy
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(req.ID)); ok {
if target.URL != device.URL || device.Audio != target.Audio || device.StreamPath != target.StreamPath || device.Record.FilePath != target.Record.FilePath || device.Record.Fragment != target.Record.Fragment {
device.Stop(task.ErrStopByUser)
needStopOld = device
return nil
}
if device.PullOnStart != target.PullOnStart && target.PullOnStart && device.Handler != nil && device.Status == PullProxyStatusOnline {
device.Handler.Pull()
}
device.Name = target.Name
device.PullOnStart = target.PullOnStart
device.StopOnIdle = target.StopOnIdle
device.Description = target.Description
}
return nil
})
if needStopOld != nil {
needStopOld.WaitStopped()
s.PullProxies.Add(target)
}
res = &pb.SuccessResponse{}
return
}
func (s *Server) RemovePullProxy(ctx context.Context, req *pb.RequestWithId) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
res = &pb.SuccessResponse{}
if req.Id > 0 {
tx := s.DB.Delete(&PullProxy{
ID: uint(req.Id),
for _, recordFile := range result {
resp.Data = append(resp.Data, &pb.RecordFile{
Id: uint32(recordFile.ID),
StartTime: timestamppb.New(recordFile.StartTime),
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
})
err = tx.Error
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(req.Id)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
return
} else if req.StreamPath != "" {
var deviceList []PullProxy
s.DB.Find(&deviceList, "stream_path=?", req.StreamPath)
if len(deviceList) > 0 {
for _, device := range deviceList {
tx := s.DB.Delete(&PullProxy{}, device.ID)
err = tx.Error
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(device.ID)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
}
}
return
} else {
res.Message = "parameter wrong"
return
}
}
func (s *Server) GetStreamAlias(ctx context.Context, req *emptypb.Empty) (res *pb.StreamAliasListResponse, err error) {
res = &pb.StreamAliasListResponse{}
s.Streams.Call(func() error {
for alias := range s.AliasStreams.Range {
info := &pb.StreamAlias{
StreamPath: alias.StreamPath,
Alias: alias.Alias,
AutoRemove: alias.AutoRemove,
}
if s.Streams.Has(alias.Alias) {
info.Status = 2
} else if alias.Publisher != nil {
info.Status = 1
}
res.Data = append(res.Data, info)
}
return nil
})
return
}
func (s *Server) SetStreamAlias(ctx context.Context, req *pb.SetStreamAliasRequest) (res *pb.SuccessResponse, err error) {
res = &pb.SuccessResponse{}
s.Streams.Call(func() error {
if req.StreamPath != "" {
u, err := url.Parse(req.StreamPath)
if err != nil {
return err
}
req.StreamPath = strings.TrimPrefix(u.Path, "/")
publisher, canReplace := s.Streams.Get(req.StreamPath)
if !canReplace {
defer s.OnSubscribe(req.StreamPath, u.Query())
}
if aliasInfo, ok := s.AliasStreams.Get(req.Alias); ok { //modify alias
aliasInfo.AutoRemove = req.AutoRemove
if aliasInfo.StreamPath != req.StreamPath {
aliasInfo.StreamPath = req.StreamPath
if canReplace {
if aliasInfo.Publisher != nil {
aliasInfo.TransferSubscribers(publisher) // replace stream
} else {
s.Waiting.WakeUp(req.Alias, publisher)
}
}
}
} else { // create alias
aliasInfo := &AliasStream{
AutoRemove: req.AutoRemove,
StreamPath: req.StreamPath,
Alias: req.Alias,
}
var pubId uint32
s.AliasStreams.Add(aliasInfo)
aliasStream, ok := s.Streams.Get(aliasInfo.Alias)
if canReplace {
aliasInfo.Publisher = publisher
if ok {
aliasStream.TransferSubscribers(publisher) // replace stream
} else {
s.Waiting.WakeUp(req.Alias, publisher)
}
} else {
aliasInfo.Publisher = aliasStream
}
if aliasInfo.Publisher != nil {
pubId = aliasInfo.Publisher.ID
}
s.Info("add alias", "alias", req.Alias, "streamPath", req.StreamPath, "replace", ok && canReplace, "pub", pubId)
}
} else {
s.Info("remove alias", "alias", req.Alias)
if aliasStream, ok := s.AliasStreams.Get(req.Alias); ok {
s.AliasStreams.Remove(aliasStream)
if aliasStream.Publisher != nil {
if publisher, hasTarget := s.Streams.Get(req.Alias); hasTarget { // restore stream
aliasStream.TransferSubscribers(publisher)
}
} else {
var args url.Values
for sub := range aliasStream.Publisher.SubscriberRange {
if sub.StreamPath == req.Alias {
aliasStream.Publisher.RemoveSubscriber(sub)
s.Waiting.Wait(sub)
args = sub.Args
}
}
if args != nil {
s.OnSubscribe(req.Alias, args)
}
}
}
}
return nil
})
return
}
func (s *Server) GetPushProxyList(ctx context.Context, req *emptypb.Empty) (res *pb.PushProxyListResponse, err error) {
res = &pb.PushProxyListResponse{}
s.PushProxies.Call(func() error {
for device := range s.PushProxies.Range {
res.Data = append(res.Data, &pb.PushProxyInfo{
Name: device.Name,
CreateTime: timestamppb.New(device.CreatedAt),
UpdateTime: timestamppb.New(device.UpdatedAt),
Type: device.Type,
PushURL: device.URL,
ParentID: uint32(device.ParentID),
Status: uint32(device.Status),
ID: uint32(device.ID),
PushOnStart: device.PushOnStart,
Audio: device.Audio,
Description: device.Description,
Rtt: uint32(device.RTT.Milliseconds()),
StreamPath: device.GetStreamPath(),
})
}
return nil
})
return
}
func (s *Server) AddPushProxy(ctx context.Context, req *pb.PushProxyInfo) (res *pb.SuccessResponse, err error) {
device := &PushProxy{
server: s,
Name: req.Name,
Type: req.Type,
ParentID: uint(req.ParentID),
PushOnStart: req.PushOnStart,
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Push)
device.URL = req.PushURL
device.Audio = req.Audio
func (s *Server) GetRecordCatalog(ctx context.Context, req *pb.ReqRecordCatalog) (resp *pb.ResponseCatalog, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
s.DB.Create(device)
s.PushProxies.Add(device)
res = &pb.SuccessResponse{}
return
}
func (s *Server) UpdatePushProxy(ctx context.Context, req *pb.PushProxyInfo) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
resp = &pb.ResponseCatalog{}
var result []struct {
StreamPath string
Count uint
StartTime time.Time
EndTime time.Time
}
target := &PushProxy{
server: s,
query := s.DB.Model(&RecordStream{})
if req.Type != "" {
query = query.Where("type = ?", req.Type)
}
err = s.DB.First(target, req.ID).Error
err = query.Select("stream_path,count(id) as count,min(start_time) as start_time,max(end_time) as end_time").Group("stream_path").Find(&result).Error
if err != nil {
return
}
target.Name = req.Name
target.URL = req.PushURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
for _, row := range result {
resp.Data = append(resp.Data, &pb.Catalog{
StreamPath: row.StreamPath,
Count: uint32(row.Count),
StartTime: timestamppb.New(row.StartTime),
EndTime: timestamppb.New(row.EndTime),
})
}
target.PushOnStart = req.PushOnStart
target.Audio = req.Audio
target.Description = req.Description
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
target.StreamPath = req.StreamPath
s.DB.Save(target)
var needStopOld *PushProxy
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(req.ID)); ok {
if target.URL != device.URL || device.Audio != target.Audio || device.StreamPath != target.StreamPath {
device.Stop(task.ErrStopByUser)
needStopOld = device
return nil
}
if device.PushOnStart != target.PushOnStart && target.PushOnStart && device.Handler != nil && device.Status == PushProxyStatusOnline {
device.Handler.Push()
}
device.Name = target.Name
device.PushOnStart = target.PushOnStart
device.Description = target.Description
}
return nil
})
if needStopOld != nil {
needStopOld.WaitStopped()
s.PushProxies.Add(target)
}
res = &pb.SuccessResponse{}
return
}
func (s *Server) RemovePushProxy(ctx context.Context, req *pb.RequestWithId) (res *pb.SuccessResponse, err error) {
func (s *Server) DeleteRecord(ctx context.Context, req *pb.ReqRecordDelete) (resp *pb.ResponseDelete, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
res = &pb.SuccessResponse{}
if req.Id > 0 {
tx := s.DB.Delete(&PushProxy{
ID: uint(req.Id),
})
err = tx.Error
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(req.Id)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
return
} else if req.StreamPath != "" {
var deviceList []PushProxy
s.DB.Find(&deviceList, "stream_path=?", req.StreamPath)
if len(deviceList) > 0 {
for _, device := range deviceList {
tx := s.DB.Delete(&PushProxy{}, device.ID)
err = tx.Error
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(device.ID)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
}
}
return
ids := req.GetIds()
var result []*RecordStream
if len(ids) > 0 {
s.DB.Find(&result, "stream_path=? AND type=? AND id IN ?", req.StreamPath, req.Type, ids)
} else {
res.Message = "parameter wrong"
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"range": []string{req.Range}, "start": []string{req.StartTime}, "end": []string{req.EndTime}})
if err != nil {
return nil, err
}
s.DB.Find(&result, "stream_path=? AND type=? AND start_time>=? AND end_time<=?", req.StreamPath, req.Type, startTime, endTime)
}
err = s.DB.Delete(result).Error
if err != nil {
return
}
var apiResult []*pb.RecordFile
for _, recordFile := range result {
apiResult = append(apiResult, &pb.RecordFile{
Id: uint32(recordFile.ID),
StartTime: timestamppb.New(recordFile.StartTime),
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
})
err = os.Remove(recordFile.FilePath)
if err != nil {
return
}
}
resp = &pb.ResponseDelete{
Data: apiResult,
}
return
}
func (s *Server) GetTransformList(ctx context.Context, req *emptypb.Empty) (res *pb.TransformListResponse, err error) {

47
doc_CN/relay.md Normal file
View File

@@ -0,0 +1,47 @@
# 核心转发流程
## 发布者
发布者Publisher 是用来在服务器上向 RingBuffer 中写入音视频数据的对象。对外暴露 WriteVideo 和 WriteAudio 方法。
在写入 WriteVideo 和 WriteAudio 时会创建 Track解析数据生成 ICodecCtx。启动发布只需要调用 Plugin 的 Publish 方法即可。
### 接受推流
rtmp、rtsp 等插件会监听一个端口用来接受推流。
### 从远端拉流
- 实现了 OnPullProxyAdd 方法的插件,可以从远端拉流。
- 继承自 HTTPFilePuller 的插件,可以从 http 或文件中拉流。
### 从本地录像文件中拉流
继承自 RecordFilePuller 的插件,可以从本地录像文件中拉流。
## 订阅者
订阅者Subscriber 是用来从 RingBuffer 中读取音视频数据的对象。订阅流分两个步骤:
1. 调用 Plugin 的 Subscribe 方法,传入 StreamPath 和 Subscribe 配置。
2. 调用 PlayBlock 方法,开始读取数据,这个方法会阻塞直到订阅结束。
之所以分两个步骤的原因是,第一步可能会失败(超时等),也可能需要等待第一步成功后进行一些交互工作。
第一步会有一定时间的阻塞,会等待发布者(如果开始没有发布者)、会等待发布者的轨道创建完成。
### 接受拉流
例如 rtmp、rtsp 插件,会监听一个端口,来接受播放的请求。
### 向远端推流
- 实现了 OnPushProxyAdd 方法的插件,可以向远端推流。
### 写入本地文件
包含录像功能的插件,需要先订阅流,才能写入本地文件。
## 按需拉流(发布)
由订阅者触发,当调用 plugin 的 OnSubscribe 时,会通知所有插件,有订阅的需求,此时插件可以响应这个需求,发布一个流。例如拉取录像流,就是这一类。此时必须要注意的是需要通过正则表达式配置,防止同时发布。

104
doc_CN/task.md Normal file
View File

@@ -0,0 +1,104 @@
# 任务机制
任务机制贯穿整个项目,在/pkg/task 目录下定义。任何逻辑在设计时,必须先考虑使用任务机制来实现,这样可以保证被观测,也可以捕获 panic 等等。
## 概念定义
### 继承
任务机制中,所有任务都是通过继承来实现的。
go语言没有继承但可以通过嵌入来实现。
### 宏任务
宏任务又叫父任务,可以包含多个子任务的执行,本身也是一个任务。
### 子任务协程
每一个宏任务都会启动一个协程,用来执行子任务的 Start、 Run、 Dispose 方法。因此,拥有同一个父任务的子任务,可以避免并发执行问题。这个协程可能不会一开始就创建,也就是可能会是懒加载。
## 任务的定义
任务通常通过继承 `task.Task``task.Job``task.Work``task.ChannelTask``task.TickTask` 来定义。
例如:
```go
type MyTask struct {
task.Task
}
```
- `task.Task` 是所有任务的基类,定义了任务的基本属性和方法。
- `task.Job` 可包含子任务子任务全部结束后Job 会结束。
- `task.Work` 同 Job,但子任务结束后Work 会继续执行。
- `task.ChannelTask` 自定义信号的任务,通过 覆盖 `GetSignal` 方法来实现。
- `task.TickTask` 定时任务,继承自 `task.ChannelTask` ,通过 覆盖 `GetTickInterval` 方法来控制定时器间隔。
### 定义任务启动方法
通过定义一个方法 Start() error 来实现任务的启动。
其中返回的 error 可以用来判断任务是否启动成功。如果为空则代表任务成功启动了。否则代表启动失败(特殊情况,返回 Complete 代表任务完成)。
通常在 Start 中包含资源的创建,例如打开文件,建立网络连接等。
Start 方法是可选的,如果没有定义,则默认启动成功。
### 定义任务的执行过程
通过定义一个方法 Run() error 来实现任务的执行过程。
这个方法通常用来执行耗时操作,同时会阻塞父任务的子任务协程。
还有一个非阻塞的方式来运行耗时操作,就是定义 Go() error 方法。
error 返回的值如果是 nil 则代表任务执行成功,否则代表执行失败(特殊情况,返回 Complete 代表任务完成)。
Run 和 Go 也是可选的,如果不定义则任务会处于正在运行状态。
### 定义任务的销毁过程
通过定义一个方法 Dispose() 来实现任务的销毁过程。
这个方法通常用来释放资源,例如关闭文件,关闭网络连接等。
Dispose 方法也是可选的,如果没有定义,则任务结束不做任何操作。
## 钩子机制
通过 OnStart、OnBeforeDispose、OnDispose 方法来实现钩子机制。
## 等待任务开始和结束
通过 WaitStarted() 和 WaitStopped() 方法来实现等待任务开始和结束。这种方式会阻塞当前协程。
## 重试机制
通过设置 Task 的 RetryCount 和 RetryInterval 来实现重试机制。有一个设置方法SetRetry(maxRetry int, retryInterval time.Duration)。
### 触发条件
- 当 Start 失败时,会重试调用 Start 直到成功。
- 当 Run 或者 Go 失败时,则会先调用 Dispose 释放资源后再调用 Start 开启重试流程。
### 终止条件
- 当重试次数满了之后就不再重试了。
- 当 Start 或者 Run、Go 返回 ErrStopByUser 、 ErrExit 、ErrTaskComplete 时,则终止重试。
## 启动一个任务
通过调用父任务的 AddTask 方法来启动一个任务。 不可以直接主动调用任务的 Start 方法。Start 方法必须是被父任务调用。
## 任务的停止
通过调用 Stop(err error) 方法来实现任务的停止。err 不能传入 nil。定义任务时不要覆盖 Stop 方法。
## 任务的停止原因
通过调用 StopReason() 方法可以检查任务的停止原因。
## Call 方法
调用 Job 的 Call 会创建一个临时任务,用来在子任务协程中执行一个函数,通常用来访问 map 等需要防止并发读写的资源。由于该函数会在子任务协程中运行,因此不可以调用 WaitStarted 和 WaitStopped 以及其他阻塞协程的逻辑,否则会导致死锁。

44
example/default/config.yaml Normal file → Executable file
View File

@@ -1,4 +1,6 @@
global:
location:
"^/hdl/(.*)": "/flv/$1"
loglevel: debug
enablelogin: false
# db:
@@ -19,12 +21,13 @@ gb28181:
pull:
.* : $0
mp4:
publish:
delayclosetimeout: 3s
# enable: false
# publish:
# delayclosetimeout: 3s
onsub:
pull:
^vod/(.+)$: $1
^vod_mp4_\d+/(.+)$: $1
cascadeserver:
quic:
listenaddr: :44944
@@ -32,10 +35,20 @@ cascadeserver:
# onpub:
# transform:
# .* : 1s x 7
# flv:
flv:
# onpub:
# record:
# ^live/.+:
# fragment: 1m
# filepath: record/$0
onsub:
pull:
^vod_flv_\d+/(.+)$: $1
# pull:
# live/test: https://livecb.alicdn.com/mediaplatform/afb241b3-408c-42dd-b665-04d22b64f9df.flv?auth_key=1734575216-0-0-c62721303ce751c8e5b2c95a2ec242a0&F=pc&source=34675810_null_live_detail&ali_flv_retain=2
# hls:
hls:
# pull:
# live/test: https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear3/prog_index.m3u8
# onpub:
# transform:
# .* : 5s x 3
@@ -77,4 +90,23 @@ crypto:
iv: your iv
onpub:
transform:
.* : $0
.* : $0
onvif:
enable: false
discoverinterval: 3 # 发现设备的间隔单位秒默认30秒建议比rtsp插件的重连间隔大点
autopull: true
autoadd: true
interfaces: # 设备发现指定网卡以及该网卡对应IP段的全局默认账号密码支持多网卡
- interfacename: 以太网 # 网卡名称 或者"以太网" "eth0"等使用ipconfig 或者 ifconfig 查看网卡名称
username: admin # onvif 账号
password: admin # onvif 密码
# - interfacename: WLAN 2 # 网卡2
# username: admin
# password: admin
# devices: # 可以给指定设备配置单独的密码
# - ip: 192.168.1.1
# username: admin
# password: '123'
# - ip: 192.168.1.2
# username: admin
# password: '456'

View File

@@ -15,6 +15,7 @@ import (
_ "m7s.live/v5/plugin/logrotate"
_ "m7s.live/v5/plugin/monitor"
_ "m7s.live/v5/plugin/mp4"
_ "m7s.live/v5/plugin/onvif"
_ "m7s.live/v5/plugin/preview"
_ "m7s.live/v5/plugin/rtmp"
_ "m7s.live/v5/plugin/rtsp"

8
go.mod
View File

@@ -47,6 +47,7 @@ require (
)
require (
github.com/IOTechSystems/onvif v1.2.0 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
github.com/VictoriaMetrics/metrics v1.35.1 // indirect
@@ -54,12 +55,15 @@ require (
github.com/abema/go-mp4 v1.2.0 // 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
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
@@ -72,6 +76,7 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -83,6 +88,7 @@ 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
@@ -125,7 +131,7 @@ require (
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/sync v0.9.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
)

14
go.sum
View File

@@ -1,5 +1,7 @@
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=
github.com/VictoriaMetrics/VictoriaMetrics v1.102.0/go.mod h1:QZhCsD2l+S+BHTdspVSsE4oiFhdKzgVziSy5Q/FZHcs=
github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc=
@@ -23,6 +25,8 @@ github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflx
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/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=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -45,6 +49,8 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA
github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/cloudwego/goref v0.0.0-20240724113447-685d2a9523c8 h1:K7L7KFg5siEysLit42Bf7n4qNRkGxniPeBtmpsxsfdQ=
github.com/cloudwego/goref v0.0.0-20240724113447-685d2a9523c8/go.mod h1:IMGV1p8Mw3uyZYClI5bA8uqk8LGr/MYFv92V0m88XUk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -61,6 +67,8 @@ github.com/deepch/vdk v0.0.27 h1:j/SHaTiZhA47wRpaue8NRp7P9xwOOO/lunxrDJBwcao=
github.com/deepch/vdk v0.0.27/go.mod h1:JlgGyR2ld6+xOIHa7XAxJh+stSDBAkdNvIPkUIdIywk=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
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/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
@@ -142,6 +150,8 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -184,6 +194,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-sqlite3 v0.18.1 h1:iN8IMZV5EMxpH88NUac9vId23eTKNFUhP7jgY0EBbNc=
@@ -391,6 +403,8 @@ 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=
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=

File diff suppressed because it is too large Load Diff

View File

@@ -1678,6 +1678,228 @@ func local_request_Api_GetTransformList_0(ctx context.Context, marshaler runtime
}
var (
filter_Api_GetRecordList_0 = &utilities.DoubleArray{Encoding: map[string]int{"type": 0, "streamPath": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}
)
func request_Api_GetRecordList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetRecordList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetRecordList_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetRecordList(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetRecordCatalog_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordCatalog
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", err)
}
msg, err := client.GetRecordCatalog(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetRecordCatalog_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordCatalog
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", err)
}
msg, err := server.GetRecordCatalog(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_DeleteRecord_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordDelete
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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.DeleteRecord(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_DeleteRecord_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordDelete
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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.DeleteRecord(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.
@@ -2734,6 +2956,81 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("GET", pattern_Api_GetRecordList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/GetRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetRecordList_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_GetRecordList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetRecordCatalog_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/GetRecordCatalog", runtime.WithHTTPPathPattern("/api/record/{type}/catalog"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetRecordCatalog_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_GetRecordCatalog_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Api_DeleteRecord_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/DeleteRecord", runtime.WithHTTPPathPattern("/api/record/{type}/delete/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_DeleteRecord_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_DeleteRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -3699,6 +3996,72 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("GET", pattern_Api_GetRecordList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/GetRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetRecordList_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_GetRecordList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetRecordCatalog_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/GetRecordCatalog", runtime.WithHTTPPathPattern("/api/record/{type}/catalog"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetRecordCatalog_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_GetRecordCatalog_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Api_DeleteRecord_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/DeleteRecord", runtime.WithHTTPPathPattern("/api/record/{type}/delete/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_DeleteRecord_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_DeleteRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -3786,6 +4149,12 @@ var (
pattern_Api_GetRecording_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "record", "list"}, ""))
pattern_Api_GetTransformList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "transform", "list"}, ""))
pattern_Api_GetRecordList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 3, 0, 4, 1, 5, 4}, []string{"api", "record", "type", "list", "streamPath"}, ""))
pattern_Api_GetRecordCatalog_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "record", "type", "catalog"}, ""))
pattern_Api_DeleteRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 3, 0, 4, 1, 5, 4}, []string{"api", "record", "type", "delete", "streamPath"}, ""))
)
var (
@@ -3872,4 +4241,10 @@ var (
forward_Api_GetRecording_0 = runtime.ForwardResponseMessage
forward_Api_GetTransformList_0 = runtime.ForwardResponseMessage
forward_Api_GetRecordList_0 = runtime.ForwardResponseMessage
forward_Api_GetRecordCatalog_0 = runtime.ForwardResponseMessage
forward_Api_DeleteRecord_0 = runtime.ForwardResponseMessage
)

View File

@@ -229,6 +229,22 @@ service api {
get: "/api/transform/list"
};
}
rpc GetRecordList (ReqRecordList) returns (ResponseList) {
option (google.api.http) = {
get: "/api/record/{type}/list/{streamPath=**}"
};
}
rpc GetRecordCatalog (ReqRecordCatalog) returns (ResponseCatalog) {
option (google.api.http) = {
get: "/api/record/{type}/catalog"
};
}
rpc DeleteRecord (ReqRecordDelete) returns (ResponseDelete) {
option (google.api.http) = {
post: "/api/record/{type}/delete/{streamPath=**}"
body: "*"
};
}
}
message DisabledPluginsResponse {
@@ -644,4 +660,64 @@ message TransformListResponse {
int32 code = 1;
string message = 2;
repeated Transform data = 3;
}
message ReqRecordList {
string streamPath = 1;
string range = 2;
string start = 3;
string end = 4;
uint32 pageNum = 5;
uint32 pageSize = 6;
string mode = 7;
string type = 8;
}
message RecordFile {
uint32 id = 1;
string filePath = 2;
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
}
message ResponseList {
int32 code = 1;
string message = 2;
uint32 totalCount = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated RecordFile data = 6;
}
message Catalog {
string streamPath = 1;
uint32 count = 2;
google.protobuf.Timestamp startTime = 3;
google.protobuf.Timestamp endTime = 4;
}
message ResponseCatalog {
int32 code = 1;
string message = 2;
repeated Catalog data = 3;
}
message ReqRecordDelete {
string streamPath = 1;
repeated uint32 ids = 2;
string startTime = 3;
string endTime = 4;
string range = 5;
string type = 6;
}
message ResponseDelete {
int32 code = 1;
string message = 2;
repeated RecordFile data = 3;
}
message ReqRecordCatalog {
string type = 1;
}

View File

@@ -61,6 +61,9 @@ type ApiClient interface {
UpdatePushProxy(ctx context.Context, in *PushProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error)
GetRecording(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*RecordingListResponse, error)
GetTransformList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TransformListResponse, error)
GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*ResponseList, error)
GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error)
DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error)
}
type apiClient struct {
@@ -413,6 +416,33 @@ func (c *apiClient) GetTransformList(ctx context.Context, in *emptypb.Empty, opt
return out, nil
}
func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*ResponseList, error) {
out := new(ResponseList)
err := c.cc.Invoke(ctx, "/global.api/GetRecordList", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error) {
out := new(ResponseCatalog)
err := c.cc.Invoke(ctx, "/global.api/GetRecordCatalog", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error) {
out := new(ResponseDelete)
err := c.cc.Invoke(ctx, "/global.api/DeleteRecord", in, out, opts...)
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
@@ -455,6 +485,9 @@ type ApiServer interface {
UpdatePushProxy(context.Context, *PushProxyInfo) (*SuccessResponse, error)
GetRecording(context.Context, *emptypb.Empty) (*RecordingListResponse, error)
GetTransformList(context.Context, *emptypb.Empty) (*TransformListResponse, error)
GetRecordList(context.Context, *ReqRecordList) (*ResponseList, error)
GetRecordCatalog(context.Context, *ReqRecordCatalog) (*ResponseCatalog, error)
DeleteRecord(context.Context, *ReqRecordDelete) (*ResponseDelete, error)
mustEmbedUnimplementedApiServer()
}
@@ -576,6 +609,15 @@ func (UnimplementedApiServer) GetRecording(context.Context, *emptypb.Empty) (*Re
func (UnimplementedApiServer) GetTransformList(context.Context, *emptypb.Empty) (*TransformListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetTransformList not implemented")
}
func (UnimplementedApiServer) GetRecordList(context.Context, *ReqRecordList) (*ResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRecordList not implemented")
}
func (UnimplementedApiServer) GetRecordCatalog(context.Context, *ReqRecordCatalog) (*ResponseCatalog, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRecordCatalog not implemented")
}
func (UnimplementedApiServer) DeleteRecord(context.Context, *ReqRecordDelete) (*ResponseDelete, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteRecord not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
@@ -1273,6 +1315,60 @@ func _Api_GetTransformList_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Api_GetRecordList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqRecordList)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).GetRecordList(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetRecordList",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetRecordList(ctx, req.(*ReqRecordList))
}
return interceptor(ctx, in, info, handler)
}
func _Api_GetRecordCatalog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqRecordCatalog)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).GetRecordCatalog(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetRecordCatalog",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetRecordCatalog(ctx, req.(*ReqRecordCatalog))
}
return interceptor(ctx, in, info, handler)
}
func _Api_DeleteRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqRecordDelete)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).DeleteRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/DeleteRecord",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).DeleteRecord(ctx, req.(*ReqRecordDelete))
}
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)
@@ -1432,6 +1528,18 @@ var Api_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetTransformList",
Handler: _Api_GetTransformList_Handler,
},
{
MethodName: "GetRecordList",
Handler: _Api_GetRecordList_Handler,
},
{
MethodName: "GetRecordCatalog",
Handler: _Api_GetRecordCatalog_Handler,
},
{
MethodName: "DeleteRecord",
Handler: _Api_DeleteRecord_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "global.proto",

View File

@@ -135,7 +135,7 @@ func (a *AnnexB) Demux(codecCtx codec.ICodecCtx) (ret any, err error) {
} else if [3]byte(lastFourBytes[1:]) == codec.NALU_Delimiter1 {
startCode = 3
}
if startCode > 0 {
if startCode > 0 && reader.Offset() >= 3 {
if reader.Offset() == 3 {
startCode = 3
}

View File

@@ -38,6 +38,7 @@ type (
BufferTime time.Duration `desc:"缓冲时长0代表取最近关键帧"` // 缓冲长度(单位:秒)0代表取最近关键帧
Speed float64 `default:"0" desc:"发送速率"` // 发送速率0 为不限速
Scale float64 `default:"1" desc:"缩放倍数"` // 缩放倍数
MaxFPS int `default:"30" 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:混合转发"` // 转发模式
@@ -57,21 +58,21 @@ type (
Key string `desc:"订阅鉴权key"` // 订阅鉴权key
SubType string `desc:"订阅类型"` // 订阅类型
}
HTTPValus map[string][]string
Pull struct {
HTTPValues map[string][]string
Pull struct {
URL string `desc:"拉流地址"`
MaxRetry int `default:"-1" desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉高于0 的数代表最大重拉次数
RetryInterval time.Duration `default:"5s" desc:"重试间隔"` // 重试间隔
Proxy string `desc:"代理地址"` // 代理地址
Header HTTPValus
Args HTTPValus `gorm:"-:all"` // 拉流参数
Header HTTPValues
Args HTTPValues `gorm:"-:all"` // 拉流参数
}
Push struct {
URL string `desc:"推送地址"` // 推送地址
MaxRetry int `desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重推,0 表示不自动重推,-1 表示无限重推高于0 的数代表最大重推次数
RetryInterval time.Duration `default:"5s" desc:"重试间隔"` // 重试间隔
Proxy string `desc:"代理地址"` // 代理地址
Header HTTPValus
Header HTTPValues
}
Record struct {
FilePath string `desc:"录制文件路径"` // 录制文件路径
@@ -139,7 +140,7 @@ func (p *Record) GetRecordConfig() *Record {
return p
}
func (v *HTTPValus) Scan(value any) error {
func (v *HTTPValues) Scan(value any) error {
bytes, ok := value.([]byte)
if !ok {
return fmt.Errorf("failed to unmarshal yaml value: %v", value)
@@ -147,16 +148,16 @@ func (v *HTTPValus) Scan(value any) error {
return yaml.Unmarshal(bytes, v)
}
func (v HTTPValus) Value() (driver.Value, error) {
func (v HTTPValues) Value() (driver.Value, error) {
return yaml.Marshal(v)
}
func (v HTTPValus) Get(key string) string {
func (v HTTPValues) Get(key string) string {
return url.Values(v).Get(key)
}
func (v HTTPValus) DeepClone() (ret HTTPValus) {
ret = make(HTTPValus)
func (v HTTPValues) DeepClone() (ret HTTPValues) {
ret = make(HTTPValues)
for k, v := range v {
ret[k] = append([]string(nil), v...)
}

View File

@@ -1,4 +0,0 @@
package db
// AutoMigrations is a slice of models that need to be auto-migrated
var AutoMigrations []interface{}

View File

@@ -380,7 +380,7 @@ 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 {
if task.Logger != nil && taskType != TASK_TYPE_CALL {
task.Debug("task dispose canceled", "taskId", task.ID, "taskType", taskType, "ownerType", ownerType, "state", task.state)
}
return

View File

@@ -126,7 +126,7 @@ func (t *Track) Trace(msg string, fields ...any) {
t.Log(context.TODO(), task.TraceLevel, msg, fields...)
}
func (t *TsTamer) Tame(ts time.Duration, fps int) (result time.Duration) {
func (t *TsTamer) Tame(ts time.Duration, fps int, scale float64) (result time.Duration) {
if t.LastTs == 0 {
t.BaseTs -= ts
}
@@ -140,5 +140,6 @@ func (t *TsTamer) Tame(ts time.Duration, fps int) (result time.Duration) {
}
}
t.LastTs = result
result = time.Duration(float64(result) / scale)
return
}

View File

@@ -15,7 +15,7 @@ func TestTsTamer_Tame(t *testing.T) {
tr := &TsTamer{}
for i, tt := range tss {
if gotResult := tr.Tame(tt*time.Millisecond, 100); gotResult != wants[i]*time.Millisecond {
if gotResult := tr.Tame(tt*time.Millisecond, 100, 1.0); gotResult != wants[i]*time.Millisecond {
t.Errorf("TsTamer.Tame() = %v, want %v", gotResult, wants[i]*time.Millisecond)
}
}

View File

@@ -13,6 +13,7 @@ const defaultBufSize = 1 << 14
type BufReader struct {
Allocator *ScalableMemoryAllocator
buf MemoryReader
totalRead int
BufLen int
feedData func() error
Dump *os.File
@@ -28,6 +29,7 @@ func NewBufReaderWithBufLen(reader io.Reader, bufLen int) (r *BufReader) {
return err
}
n := len(buf)
r.totalRead += n
r.buf.Buffers = append(r.buf.Buffers, buf)
r.buf.Size += n
r.buf.Length += n
@@ -48,6 +50,7 @@ func NewBufReaderBuffersChan(feedChan chan net.Buffers) (r *BufReader) {
}
for _, buf := range data {
n := len(buf)
r.totalRead += n
r.buf.Size += n
r.buf.Length += n
}
@@ -67,6 +70,7 @@ func NewBufReaderChan(feedChan chan []byte) (r *BufReader) {
return io.EOF
}
n := len(data)
r.totalRead += n
r.buf.Buffers = append(r.buf.Buffers, data)
r.buf.Size += n
r.buf.Length += n

View File

@@ -130,9 +130,9 @@ func (c *Collection[K, T]) Get(key K) (item T, ok bool) {
}
if c.m != nil {
item, ok = c.m[key]
return item, ok
return
}
for _, item = range c.Items {
for _, item := range c.Items {
if item.GetKey() == key {
return item, true
}
@@ -145,7 +145,7 @@ func (c *Collection[K, T]) Find(f func(T) bool) (item T, ok bool) {
c.L.RLock()
defer c.L.RUnlock()
}
for _, item = range c.Items {
for _, item := range c.Items {
if f(item) {
return item, true
}

179
pkg/util/collection_test.go Normal file
View File

@@ -0,0 +1,179 @@
package util
import (
"fmt"
"sync"
"testing"
)
type Class struct {
name string
Id int
}
func (n *Class) GetKey() string {
return n.name
}
func TestCollection(t *testing.T) {
var cc Collection[string, *Class]
for i := 0; i < 10; i++ {
cc.Add(&Class{name: fmt.Sprintf("%d", i), Id: i})
}
cc.RemoveByKey("1")
if item, ok := cc.Get("1"); ok {
fmt.Println(item)
} else {
fmt.Println("not found", item)
}
}
func TestCollection_Range(t *testing.T) {
var cc Collection[string, *Class]
for i := 0; i < 10; i++ {
cc.Add(&Class{name: fmt.Sprintf("%d", i), Id: i})
}
for item := range cc.Range {
fmt.Println(item)
cc.Remove(item)
}
}
// TestItem 是用于测试的结构体
type TestItem struct {
ID string
Data string
}
func (t TestItem) GetKey() string {
return t.ID
}
func TestCollection_BasicOperations(t *testing.T) {
c := &Collection[string, TestItem]{
L: &sync.RWMutex{},
}
// 测试 Add
item1 := TestItem{ID: "1", Data: "test1"}
c.Add(item1)
if c.Length != 1 {
t.Errorf("Expected length 1, got %d", c.Length)
}
// 测试 Get
got, ok := c.Get("1")
if !ok || got != item1 {
t.Errorf("Expected to get item1, got %v", got)
}
// 测试 AddUnique
ok = c.AddUnique(item1)
if ok || c.Length != 1 {
t.Error("AddUnique should not add duplicate item")
}
// 测试 Set
item1Modified := TestItem{ID: "1", Data: "test1-modified"}
added := c.Set(item1Modified)
if added {
t.Error("Set should not return true for existing item")
}
got, _ = c.Get("1")
if got.Data != "test1-modified" {
t.Errorf("Expected modified data, got %s", got.Data)
}
// 测试 Remove
if !c.Remove(item1) {
t.Error("Remove should return true for existing item")
}
if c.Length != 0 {
t.Errorf("Expected length 0 after remove, got %d", c.Length)
}
}
func TestCollection_Events(t *testing.T) {
c := &Collection[string, TestItem]{}
var addCalled, removeCalled bool
c.OnAdd(func(item TestItem) {
addCalled = true
if item.ID != "1" {
t.Errorf("Expected item ID 1, got %s", item.ID)
}
})
c.OnRemove(func(item TestItem) {
removeCalled = true
if item.ID != "1" {
t.Errorf("Expected item ID 1, got %s", item.ID)
}
})
item := TestItem{ID: "1", Data: "test"}
c.Add(item)
if !addCalled {
t.Error("Add listener was not called")
}
c.Remove(item)
if !removeCalled {
t.Error("Remove listener was not called")
}
}
func TestCollection_Search(t *testing.T) {
c := &Collection[string, TestItem]{}
items := []TestItem{
{ID: "1", Data: "test1"},
{ID: "2", Data: "test2"},
{ID: "3", Data: "test1"},
}
for _, item := range items {
c.Add(item)
}
// 测试 Find
found, ok := c.Find(func(item TestItem) bool {
return item.Data == "test1"
})
if !ok || found.ID != "1" {
t.Error("Find should return first matching item")
}
// 测试 Search
count := 0
search := c.Search(func(item TestItem) bool {
return item.Data == "test1"
})
search(func(item TestItem) bool {
count++
return true
})
if count != 2 {
t.Errorf("Search should find 2 items, found %d", count)
}
}
func TestCollection_ConcurrentAccess(t *testing.T) {
c := &Collection[string, TestItem]{
L: &sync.RWMutex{},
}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
item := TestItem{ID: string(rune(id)), Data: "test"}
c.Add(item)
}(i)
}
wg.Wait()
if c.Length != 100 {
t.Errorf("Expected 100 items, got %d", c.Length)
}
}

View File

@@ -15,6 +15,7 @@ const (
StartKey = "start"
EndKey = "end"
RangeKey = "range"
LoopKey = "loop"
LocalTimeFormat = "2006-01-02T15:04:05"
)

View File

@@ -16,6 +16,8 @@ type ReadWriteSeekCloser interface {
io.Closer
}
type Object = map[string]any
func Conditional[T any](cond bool, t, f T) T {
if cond {
return t

110
pkg/util/rune_conv.go Executable file
View File

@@ -0,0 +1,110 @@
package util
import (
"encoding/base64"
"strings"
"unicode"
"github.com/mozillazg/go-pinyin"
)
// StringSegment 表示字符串片段及其类型
type StringSegment struct {
Text string
StrType int // 0: 英文/数字, 1: 中文, 2: 其他语言
}
// SplitRuneString 将字符串切割成不同类型的片段
func SplitRuneString(s string) []StringSegment {
var segments []StringSegment
var builder strings.Builder
var currentType int // 当前正在构建的片段类型
// 获取字符类型
getCharType := func(r rune) int {
switch {
case unicode.Is(unicode.Han, r):
return 1 // 中文
case unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r):
return 2 // 日语假名
case unicode.Is(unicode.Hangul, r):
return 2 // 韩语
case unicode.Is(unicode.Thai, r):
return 2 // 泰语
case unicode.IsLetter(r) || unicode.IsDigit(r):
return 0 // 英文或数字
default:
// 其他 Unicode 字符,如果不是空格或标点,也认为是其他语言
if !unicode.IsSpace(r) && !unicode.IsPunct(r) {
return 2
}
// 对于空格和标点,跟随当前类型
return currentType
}
}
// 添加当前片段到结果中
addSegment := func() {
if builder.Len() > 0 {
segments = append(segments, StringSegment{
Text: builder.String(),
StrType: currentType,
})
builder.Reset()
}
}
runes := []rune(s)
if len(runes) == 0 {
return segments
}
// 初始化第一个字符
firstChar := runes[0]
currentType = getCharType(firstChar)
builder.WriteRune(firstChar)
// 处理剩余字符
for i := 1; i < len(runes); i++ {
currentChar := runes[i]
charType := getCharType(currentChar)
// 如果字符类型发生变化,添加新片段
if charType != currentType {
addSegment()
currentType = charType
}
builder.WriteRune(currentChar)
}
// 添加最后一个片段
addSegment()
return segments
}
// ConvertRuneToEn 将字符串中的中文转换为拼音,其他语言转换为 base64
func ConvertRuneToEn(s string) string {
segments := SplitRuneString(s)
var result strings.Builder
a := pinyin.NewArgs()
for i, seg := range segments {
switch seg.StrType {
case 0: // 英文/数字
result.WriteString(seg.Text)
case 1: // 中文
pinyinSlice := pinyin.LazyPinyin(seg.Text, a)
result.WriteString(strings.Join(pinyinSlice, ""))
case 2: // 其他语言,使用 base64 编码(不带填充)
result.WriteString(base64.RawURLEncoding.EncodeToString([]byte(seg.Text)))
}
// 如果不是最后一个片段,且下一个片段类型不同,添加空格
if i < len(segments)-1 && segments[i+1].StrType != seg.StrType {
result.WriteString("")
}
}
return result.String()
}

View File

@@ -0,0 +1,37 @@
package util
import (
"fmt"
"testing"
)
func TestConvertRuneToEn(t *testing.T) {
// 测试用例
testCases := []string{
"aa哈哈哈",
"hello世界",
"123你好",
"混合字符串abc中文123",
"纯中文测试",
"onlyEnglish",
"Hello世界こんにちは", // 添加包含其他语言的测试用例
}
// 测试 SplitRuneString
t.Log("测试 SplitRuneString 函数:")
for _, str := range testCases {
segments := SplitRuneString(str)
t.Logf("原始字符串: %q\n", str)
for _, seg := range segments {
t.Logf(" 片段: %q, 类型: %d\n", seg.Text, seg.StrType)
}
fmt.Println()
}
// 测试 ConvertRuneToEn
fmt.Println("\n测试 ConvertRuneToEn 函数:")
for _, str := range testCases {
converted := ConvertRuneToEn(str)
t.Logf("原始字符串: %q, 转换后: %q\n", str, converted)
}
}

View File

@@ -166,6 +166,12 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
}
}
}
if p.DB != nil && p.Meta.Recorder != nil {
if err = p.DB.AutoMigrate(&RecordStream{}); err != nil {
p.disable(fmt.Sprintf("auto migrate record stream failed %v", err))
return
}
}
s.AddTask(instance)
return
}
@@ -525,8 +531,8 @@ func (p *Plugin) OnSubscribe(streamPath string, args url.Values) {
// }
// }
for reg, conf := range p.config.OnSub.Pull {
if p.Meta.Puller != nil {
conf.Args = config.HTTPValus(args)
if p.Meta.Puller != nil && reg.MatchString(streamPath) {
conf.Args = config.HTTPValues(args)
conf.URL = reg.Replace(streamPath, conf.URL)
p.handler.Pull(streamPath, conf, nil)
}
@@ -668,7 +674,7 @@ func (p *Plugin) registerHandler(handlers map[string]http.HandlerFunc) {
for patten, handler := range handlers {
p.handle(patten, handler)
}
if p.config.EnableAuth && p.Server.ServerConfig.EnableLogin {
if p.config.EnableAuth && p.Server.ServerConfig.Admin.EnableLogin {
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {

306
plugin/README.md Normal file
View File

@@ -0,0 +1,306 @@
# Plugin Development Guide
## 1. Prerequisites
### Development Tools
- Visual Studio Code
- Goland
- Cursor
### 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
```
### Install gRPC-Gateway
```shell
$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```
### Project Setup
- Create a Go project, e.g., `MyPlugin`
- Create a `pkg` directory for exportable code
- Create a `pb` directory for gRPC proto files
- Create an `example` directory for testing the plugin
> You can also create a directory `xxx` directly in the monibuca project's plugin folder to store your plugin code
## 2. Create a Plugin
```go
package plugin_myplugin
import (
"m7s.live/v5"
)
var _ = m7s.InstallPlugin[MyPlugin]()
type MyPlugin struct {
m7s.Plugin
Foo string
}
```
- `MyPlugin` struct is the plugin definition, `Foo` is a plugin property that can be configured in the configuration file
- Must embed `m7s.Plugin` struct to provide basic plugin functionality
- `m7s.InstallPlugin[MyPlugin](...)` registers the plugin so it can be loaded by monibuca
### Provide Default Configuration
Example:
```go
const defaultConfig = m7s.DefaultYaml(`tcp:
listenaddr: :5554`)
var _ = m7s.InstallPlugin[MyPlugin](defaultConfig)
```
## 3. Implement Event Callbacks (Optional)
### Initialization Callback
```go
func (config *MyPlugin) OnInit() (err error) {
// Initialize things
return
}
```
Used for plugin initialization after configuration is loaded. Return an error if initialization fails, and the plugin will be disabled.
### TCP Request Callback
```go
func (config *MyPlugin) OnTCPConnect(conn *net.TCPConn) task.ITask {
}
```
Called when receiving TCP connection requests if TCP listening port is configured.
### UDP Request Callback
```go
func (config *MyPlugin) OnUDPConnect(conn *net.UDPConn) task.ITask {
}
```
Called when receiving UDP connection requests if UDP listening port is configured.
### QUIC Request Callback
```go
func (config *MyPlugin) OnQUICConnect(quic.Connection) task.ITask {
}
```
Called when receiving QUIC connection requests if QUIC listening port is configured.
## 4. HTTP Interface Callbacks
### Legacy v4 Callback Style
```go
func (config *MyPlugin) API_test1(rw http.ResponseWriter, r *http.Request) {
// do something
}
```
Accessible via `http://ip:port/myplugin/api/test1`
### Route Mapping Configuration
This method supports parameterized routing:
```go
func (config *MyPlugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/test1/{streamPath...}": config.test1,
}
}
func (config *MyPlugin) test1(rw http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
// do something
}
```
## 5. Implement Push/Pull Clients
### Implement Push Client
Push client needs to implement IPusher interface and pass the creation method to InstallPlugin.
```go
type Pusher struct {
pullCtx m7s.PullJob
}
func (c *Pusher) GetPullJob() *m7s.PullJob {
return &c.pullCtx
}
func NewPusher(_ config.Push) m7s.IPusher {
return &Pusher{}
}
var _ = m7s.InstallPlugin[MyPlugin](NewPusher)
```
### Implement Pull Client
Pull client needs to implement IPuller interface and pass the creation method to InstallPlugin.
The following Puller inherits from m7s.HTTPFilePuller for basic file and HTTP pulling:
```go
type Puller struct {
m7s.HTTPFilePuller
}
func NewPuller(_ config.Pull) m7s.IPuller {
return &Puller{}
}
var _ = m7s.InstallPlugin[MyPlugin](NewPuller)
```
## 6. Implement gRPC Service
### Create `myplugin.proto` in `pb` Directory
```proto
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package myplugin;
option go_package="m7s.live/v5/plugin/myplugin/pb";
service api {
rpc MyMethod (MyRequest) returns (MyResponse) {
option (google.api.http) = {
post: "/myplugin/api/bar"
body: "foo"
};
}
}
message MyRequest {
string foo = 1;
}
message MyResponse {
string bar = 1;
}
```
### Generate gRPC Code
Add to VSCode task.json:
```json
{
"type": "shell",
"label": "build pb myplugin",
"command": "protoc",
"args": [
"-I.",
"-I${workspaceRoot}/pb",
"--go_out=.",
"--go_opt=paths=source_relative",
"--go-grpc_out=.",
"--go-grpc_opt=paths=source_relative",
"--grpc-gateway_out=.",
"--grpc-gateway_opt=paths=source_relative",
"myplugin.proto"
],
"options": {
"cwd": "${workspaceRoot}/plugin/myplugin/pb"
}
}
```
Or run command in pb directory:
```shell
protoc -I. -I$ProjectFileDir$/pb --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative myplugin.proto
```
Replace `$ProjectFileDir$` with the directory containing global pb files.
### Implement gRPC Service
Create api.go:
```go
package plugin_myplugin
import (
"context"
"m7s.live/m7s/v5"
"m7s.live/m7s/v5/plugin/myplugin/pb"
)
func (config *MyPlugin) MyMethod(ctx context.Context, req *pb.MyRequest) (*pb.MyResponse, error) {
return &pb.MyResponse{Bar: req.Foo}, nil
}
```
### Register gRPC Service
```go
package plugin_myplugin
import (
"m7s.live/v5"
"m7s.live/v5/plugin/myplugin/pb"
)
var _ = m7s.InstallPlugin[MyPlugin](&pb.Api_ServiceDesc, pb.RegisterApiHandler)
type MyPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
Foo string
}
```
### Additional RESTful Endpoints
Same as v4:
```go
func (config *MyPlugin) API_test1(rw http.ResponseWriter, r *http.Request) {
// do something
}
```
Accessible via GET request to `/myplugin/api/test1`
## 7. Publishing Streams
```go
publisher, err = p.Publish(streamPath, connectInfo)
```
The last two parameters are optional.
After obtaining the `publisher`, you can publish audio/video data using `publisher.WriteAudio` and `publisher.WriteVideo`.
### Define Audio/Video Data
If existing audio/video data formats don't meet your needs, you can define custom formats by implementing this interface:
```go
IAVFrame interface {
GetAllocator() *util.ScalableMemoryAllocator
SetAllocator(*util.ScalableMemoryAllocator)
Parse(*AVTrack) error
ConvertCtx(codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error)
Demux(codec.ICodecCtx) (any, error)
Mux(codec.ICodecCtx, *AVFrame)
GetTimestamp() time.Duration
GetCTS() time.Duration
GetSize() int
Recycle()
String() string
Dump(byte, io.Writer)
}
```
> Define separate types for audio and video
- GetAllocator/SetAllocator: Automatically implemented when embedding RecyclableMemory
- Parse: Identifies key frames, sequence frames, and other important information
- ConvertCtx: Called when protocol conversion is needed
- Demux: Called when audio/video data needs to be demuxed
- Mux: Called when audio/video data needs to be muxed
- Recycle: Automatically implemented when embedding RecyclableMemory
- String: Prints audio/video data information
- GetSize: Gets the size of audio/video data
- GetTimestamp: Gets the timestamp in nanoseconds
- GetCTS: Gets the Composition Time Stamp in nanoseconds (PTS = DTS+CTS)
- Dump: Prints binary audio/video data
## 8. Subscribing to Streams
```go
var suber *m7s.Subscriber
suber, err = p.Subscribe(ctx,streamPath)
go m7s.PlayBlock(suber, handleAudio, handleVideo)
```
Note that handleAudio and handleVideo are callback functions you need to implement. They take an audio/video format type as input and return an error. If the error is not nil, the subscription is terminated.
## 9. Prometheus Integration
Just implement the Collector interface, and the system will automatically collect metrics from all plugins:
```go
func (p *MyPlugin) Describe(ch chan<- *prometheus.Desc) {
}
func (p *MyPlugin) Collect(ch chan<- prometheus.Metric) {
}
```

View File

@@ -2,20 +2,55 @@ package plugin_flv
import (
"bufio"
"context"
"encoding/binary"
"io"
"io/fs"
"m7s.live/v5/pkg/util"
flv "m7s.live/v5/plugin/flv/pkg"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
"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) {
globalReq := &pb.ReqRecordList{
StreamPath: req.StreamPath,
Range: req.Range,
Start: req.Start,
End: req.End,
PageNum: req.PageNum,
PageSize: req.PageSize,
Mode: req.Mode,
Type: "flv",
}
return p.Server.GetRecordList(ctx, globalReq)
}
func (p *FLVPlugin) Catalog(ctx context.Context, req *emptypb.Empty) (resp *pb.ResponseCatalog, err error) {
return p.Server.GetRecordCatalog(ctx, &pb.ReqRecordCatalog{Type: "flv"})
}
func (p *FLVPlugin) Delete(ctx context.Context, req *flvpb.ReqRecordDelete) (resp *pb.ResponseDelete, err error) {
globalReq := &pb.ReqRecordDelete{
StreamPath: req.StreamPath,
Ids: req.Ids,
StartTime: req.StartTime,
EndTime: req.EndTime,
Range: req.Range,
Type: "flv",
}
return p.Server.DeleteRecord(ctx, globalReq)
}
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")

View File

@@ -10,12 +10,13 @@ import (
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/flv/pb"
. "m7s.live/v5/plugin/flv/pkg"
)
type FLVPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
Path string
}
@@ -23,7 +24,7 @@ type FLVPlugin struct {
const defaultConfig m7s.DefaultYaml = `publish:
speed: 1`
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPuller, NewRecorder)
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPuller, NewRecorder, pb.RegisterApiServer, &pb.Api_ServiceDesc)
func (plugin *FLVPlugin) OnInit() (err error) {
_, port, _ := strings.Cut(plugin.GetCommonConf().HTTP.ListenAddr, ":")

342
plugin/flv/pb/flv.pb.go Normal file
View File

@@ -0,0 +1,342 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.1
// source: flv.proto
package pb
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "google.golang.org/protobuf/types/known/durationpb"
emptypb "google.golang.org/protobuf/types/known/emptypb"
_ "google.golang.org/protobuf/types/known/timestamppb"
pb "m7s.live/v5/pb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ReqRecordList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
}
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)
}
}
func (x *ReqRecordList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReqRecordList) ProtoMessage() {}
func (x *ReqRecordList) ProtoReflect() protoreflect.Message {
mi := &file_flv_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReqRecordList.ProtoReflect.Descriptor instead.
func (*ReqRecordList) Descriptor() ([]byte, []int) {
return file_flv_proto_rawDescGZIP(), []int{0}
}
func (x *ReqRecordList) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
func (x *ReqRecordList) GetRange() string {
if x != nil {
return x.Range
}
return ""
}
func (x *ReqRecordList) GetStart() string {
if x != nil {
return x.Start
}
return ""
}
func (x *ReqRecordList) GetEnd() string {
if x != nil {
return x.End
}
return ""
}
func (x *ReqRecordList) GetPageNum() uint32 {
if x != nil {
return x.PageNum
}
return 0
}
func (x *ReqRecordList) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *ReqRecordList) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
type ReqRecordDelete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
}
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)
}
}
func (x *ReqRecordDelete) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_flv_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReqRecordDelete.ProtoReflect.Descriptor instead.
func (*ReqRecordDelete) Descriptor() ([]byte, []int) {
return file_flv_proto_rawDescGZIP(), []int{1}
}
func (x *ReqRecordDelete) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
func (x *ReqRecordDelete) GetIds() []uint32 {
if x != nil {
return x.Ids
}
return nil
}
func (x *ReqRecordDelete) GetStartTime() string {
if x != nil {
return x.StartTime
}
return ""
}
func (x *ReqRecordDelete) GetEndTime() string {
if x != nil {
return x.EndTime
}
return ""
}
func (x *ReqRecordDelete) GetRange() string {
if x != nil {
return x.Range
}
return ""
}
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,
}
var (
file_flv_proto_rawDescOnce sync.Once
file_flv_proto_rawDescData = file_flv_proto_rawDesc
)
func file_flv_proto_rawDescGZIP() []byte {
file_flv_proto_rawDescOnce.Do(func() {
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(file_flv_proto_rawDescData)
})
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_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
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
0, // [0:3] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_flv_proto_init() }
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,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_flv_proto_goTypes,
DependencyIndexes: file_flv_proto_depIdxs,
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
}

388
plugin/flv/pb/flv.pb.gw.go Normal file
View File

@@ -0,0 +1,388 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: flv.proto
/*
Package pb is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package pb
import (
"context"
"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"
"google.golang.org/protobuf/types/known/emptypb"
)
// 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 (
filter_Api_List_0 = &utilities.DoubleArray{Encoding: map[string]int{"streamPath": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
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 ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
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 ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
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_Catalog_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.Catalog(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_Catalog_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
var metadata runtime.ServerMetadata
msg, err := server.Catalog(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordDelete
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordDelete
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := server.Delete(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.
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
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, "/flv.Api/List", runtime.WithHTTPPathPattern("/flv/api/list/{streamPath=**}"))
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("GET", pattern_Api_Catalog_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, "/flv.Api/Catalog", runtime.WithHTTPPathPattern("/flv/api/catalog"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_Catalog_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_Catalog_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Api_Delete_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, "/flv.Api/Delete", runtime.WithHTTPPathPattern("/flv/api/delete/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_Delete_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_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
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)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("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.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
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, "/flv.Api/List", runtime.WithHTTPPathPattern("/flv/api/list/{streamPath=**}"))
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("GET", pattern_Api_Catalog_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, "/flv.Api/Catalog", runtime.WithHTTPPathPattern("/flv/api/catalog"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_Catalog_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_Catalog_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Api_Delete_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, "/flv.Api/Delete", runtime.WithHTTPPathPattern("/flv/api/delete/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_Delete_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_Delete_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, 3, 0, 4, 1, 5, 3}, []string{"flv", "api", "list", "streamPath"}, ""))
pattern_Api_Catalog_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"flv", "api", "catalog"}, ""))
pattern_Api_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"flv", "api", "delete", "streamPath"}, ""))
)
var (
forward_Api_List_0 = runtime.ForwardResponseMessage
forward_Api_Catalog_0 = runtime.ForwardResponseMessage
forward_Api_Delete_0 = runtime.ForwardResponseMessage
)

45
plugin/flv/pb/flv.proto Normal file
View File

@@ -0,0 +1,45 @@
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "global.proto";
package flv;
option go_package="m7s.live/v5/plugin/flv/pb";
service api {
rpc List (ReqRecordList) returns (global.ResponseList) {
option (google.api.http) = {
get: "/flv/api/list/{streamPath=**}"
};
}
rpc Catalog (google.protobuf.Empty) returns (global.ResponseCatalog) {
option (google.api.http) = {
get: "/flv/api/catalog"
};
}
rpc Delete (ReqRecordDelete) returns (global.ResponseDelete) {
option (google.api.http) = {
post: "/flv/api/delete/{streamPath=**}"
body: "*"
};
}
}
message ReqRecordList {
string streamPath = 1;
string range = 2;
string start = 3;
string end = 4;
uint32 pageNum = 5;
uint32 pageSize = 6;
string mode = 7;
}
message ReqRecordDelete {
string streamPath = 1;
repeated uint32 ids = 2;
string startTime = 3;
string endTime = 4;
string range = 5;
}

View File

@@ -0,0 +1,179 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// source: flv.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
pb "m7s.live/v5/pb"
)
// 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
// 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)
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)
}
type apiClient struct {
cc grpc.ClientConnInterface
}
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...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
out := new(pb.ResponseCatalog)
err := c.cc.Invoke(ctx, "/flv.api/Catalog", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
out := new(pb.ResponseDelete)
err := c.cc.Invoke(ctx, "/flv.api/Delete", in, out, opts...)
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, *ReqRecordList) (*pb.ResponseList, 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 {
}
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
return nil, status.Errorf(codes.Unimplemented, "method Catalog not implemented")
}
func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
// 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) {
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(ReqRecordList)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
}
return interceptor(ctx, in, info, handler)
}
func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).Catalog(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Catalog",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqRecordDelete)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).Delete(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Delete",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))
}
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: "flv.api",
HandlerType: (*ApiServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "List",
Handler: _Api_List_Handler,
},
{
MethodName: "Catalog",
Handler: _Api_Catalog_Handler,
},
{
MethodName: "Delete",
Handler: _Api_Delete_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "flv.proto",
}

View File

@@ -24,44 +24,6 @@ type Tag struct {
Timestamp uint32
}
type FlvReader struct {
reader io.Reader
buf [11]byte
}
func NewFlvReader(r io.Reader) *FlvReader {
return &FlvReader{reader: r}
}
func (r *FlvReader) ReadHeader() (err error) {
var header [13]byte
if _, err = io.ReadFull(r.reader, header[:]); err != nil {
return
}
if header[0] != 'F' || header[1] != 'L' || header[2] != 'V' {
return io.ErrUnexpectedEOF
}
return
}
func (r *FlvReader) ReadTag() (tag *Tag, err error) {
tag = &Tag{}
if _, err = io.ReadFull(r.reader, r.buf[:]); err != nil {
return
}
tmp := util.Buffer(r.buf[:])
tag.Type = tmp.ReadByte()
dataSize := tmp.ReadUint24()
tag.Timestamp = tmp.ReadUint24() | (uint32(tmp.ReadByte()) << 24)
tmp.ReadUint24() // streamID always 0
tag.Data = make([]byte, dataSize+4) // +4 for previous tag size
if _, err = io.ReadFull(r.reader, tag.Data); err != nil {
return
}
return
}
type FlvWriter struct {
io.Writer
buf [15]byte

View File

@@ -2,10 +2,11 @@ package flv
import (
"encoding/binary"
"net"
"m7s.live/v5"
. "m7s.live/v5/pkg"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
"net"
)
type Live struct {
@@ -53,18 +54,18 @@ func (task *Live) WriteFlvHeader() (err error) {
return task.WriteFlvTag(net.Buffers{[]byte{'F', 'L', 'V', 0x01, flags, 0, 0, 0, 9, 0, 0, 0, 0}, task.b[4:], data})
}
func (task *Live) rtmpData2FlvTag(t byte, data *rtmp.RTMPData) error {
WriteFLVTagHead(t, data.Timestamp, uint32(data.Size), task.b[4:])
func (task *Live) rtmpData2FlvTag(t byte, data *rtmp.RTMPData, ts uint32) error {
WriteFLVTagHead(t, ts, uint32(data.Size), task.b[4:])
defer binary.BigEndian.PutUint32(task.b[:4], uint32(data.Size)+11)
return task.WriteFlvTag(append(net.Buffers{task.b[:]}, data.Memory.Buffers...))
}
func (task *Live) WriteAudioTag(data *rtmp.RTMPAudio) error {
return task.rtmpData2FlvTag(FLV_TAG_TYPE_AUDIO, &data.RTMPData)
func (task *Live) WriteAudioTag(data *rtmp.RTMPAudio, ts uint32) error {
return task.rtmpData2FlvTag(FLV_TAG_TYPE_AUDIO, &data.RTMPData, ts)
}
func (task *Live) WriteVideoTag(data *rtmp.RTMPVideo) error {
return task.rtmpData2FlvTag(FLV_TAG_TYPE_VIDEO, &data.RTMPData)
func (task *Live) WriteVideoTag(data *rtmp.RTMPVideo, ts uint32) error {
return task.rtmpData2FlvTag(FLV_TAG_TYPE_VIDEO, &data.RTMPData, ts)
}
func (task *Live) Run() (err error) {
@@ -73,9 +74,9 @@ func (task *Live) Run() (err error) {
return
}
err = m7s.PlayBlock(task.Subscriber, func(audio *rtmp.RTMPAudio) error {
return task.WriteAudioTag(audio)
return task.WriteAudioTag(audio, task.Subscriber.AudioReader.AbsTime)
}, func(video *rtmp.RTMPVideo) error {
return task.WriteVideoTag(video)
return task.WriteVideoTag(video, task.Subscriber.VideoReader.AbsTime)
})
if err != nil {
return

View File

@@ -1,14 +1,14 @@
package flv
import (
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
m7s "m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
@@ -18,7 +18,7 @@ import (
type (
RecordReader struct {
m7s.RecordFilePuller
reader *FlvReader
reader *util.BufReader
}
)
@@ -30,110 +30,182 @@ func NewPuller(conf config.Pull) m7s.IPuller {
}
if conf.Args.Get(util.StartKey) != "" {
p := &RecordReader{}
p.Type = "flv"
p.SetDescription(task.OwnerTypeKey, "FlvRecordReader")
return p
}
return nil
}
func (p *RecordReader) Dispose() {
if p.reader != nil {
p.reader.Recycle()
}
p.RecordFilePuller.Dispose()
}
func (p *RecordReader) Run() (err error) {
pullJob := &p.PullJob
pullStartTime := p.PullStartTime
publisher := pullJob.Publisher
allocator := util.NewScalableMemoryAllocator(1 << 10)
var ts, tsOffset int64
var tagHeader [11]byte
var ts int64
var realTime time.Time
defer allocator.Recycle()
publisher.OnSeek = func(seekTime time.Time) {
pullStartTime = seekTime
p.SetRetry(1, 0)
if util.UnixTimeReg.MatchString(pullJob.Args.Get(util.EndKey)) {
pullJob.Args.Set(util.StartKey, strconv.FormatInt(pullStartTime.Unix(), 10))
} else {
pullJob.Args.Set(util.StartKey, pullStartTime.Local().Format(util.LocalTimeFormat))
}
publisher.Stop(pkg.ErrSeek)
}
var seekPosition int64
var seekTsOffset int64
// defer allocator.Recycle()
defer func() {
allocator.Recycle()
}()
publisher.OnGetPosition = func() time.Time {
return realTime
}
for i, stream := range p.Streams {
tsOffset = ts
if p.File != nil {
p.File.Close()
}
p.File, err = os.Open(stream.FilePath)
if err != nil {
continue
}
p.reader = NewFlvReader(p.File)
if err = p.reader.ReadHeader(); err != nil {
return
}
if i == 0 {
startTimestamp := pullStartTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
for loop := 0; loop < p.Loop; loop++ {
nextStream:
for i, stream := range p.Streams {
seekTsOffset = ts
if p.File != nil {
p.File.Close()
}
var metaData rtmp.EcmaArray
if metaData, err = ReadMetaData(p.File); err != nil {
p.File, err = os.Open(stream.FilePath)
if err != nil {
continue
}
if p.reader != nil {
p.reader.Recycle()
}
p.reader = util.NewBufReader(p.File)
var head util.Memory
head, err = p.reader.ReadBytes(9)
if err != nil {
return
}
if keyframes, ok := metaData["keyframes"].(map[string]any); ok {
filepositions := keyframes["filepositions"].([]float64)
times := keyframes["times"].([]float64)
for i, t := range times {
if int64(t*1000) >= startTimestamp {
if _, err = p.File.Seek(int64(filepositions[i]), io.SeekStart); err != nil {
return
}
tsOffset = -int64(t * 1000)
break
}
}
var flvHead [3]byte
var version, flag byte
err = head.NewReader().ReadByteTo(&flvHead[0], &flvHead[1], &flvHead[2], &version, &flag)
hasAudio := (flag & 0x04) != 0
hasVideo := (flag & 0x01) != 0
if !hasAudio {
publisher.NoAudio()
}
}
for {
if p.IsStopped() {
return p.StopReason()
}
if publisher.Paused != nil {
publisher.Paused.Await()
}
var tag *Tag
if tag, err = p.reader.ReadTag(); err != nil {
if err == io.EOF {
err = nil
break
}
return
}
ts = int64(tag.Timestamp) + tsOffset
realTime = stream.StartTime.Add(time.Duration(tag.Timestamp) * time.Millisecond)
if p.MaxTS > 0 && ts > p.MaxTS {
return
}
switch tag.Type {
case FLV_TAG_TYPE_AUDIO:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AddRecycleBytes(tag.Data[:len(tag.Data)-4]) // remove previous tag size
err = publisher.WriteAudio(&audioFrame)
case FLV_TAG_TYPE_VIDEO:
var videoFrame rtmp.RTMPVideo
videoFrame.SetAllocator(allocator)
videoFrame.Timestamp = uint32(ts)
videoFrame.AddRecycleBytes(tag.Data[:len(tag.Data)-4]) // remove previous tag size
err = publisher.WriteVideo(&videoFrame)
if !hasVideo {
publisher.NoVideo()
}
if err != nil {
return
}
if flvHead != [3]byte{'F', 'L', 'V'} {
return errors.New("not flv file")
}
startTimestamp := int64(0)
if i == 0 {
startTimestamp = p.PullStartTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
}
for {
if p.IsStopped() {
return p.StopReason()
}
if publisher.Paused != nil {
publisher.Paused.Await()
}
if needSeek, err := p.CheckSeek(); err != nil {
continue
} else if needSeek {
goto nextStream
}
if _, err = p.reader.ReadBE(4); err != nil { // previous tag size
break
}
// Read tag header (11 bytes total)
if err = p.reader.ReadNto(11, tagHeader[:]); err != nil {
break
}
t := tagHeader[0] // tag type (1 byte)
dataSize := int(tagHeader[1])<<16 | int(tagHeader[2])<<8 | int(tagHeader[3]) // data size (3 bytes)
timestamp := uint32(tagHeader[4])<<16 | uint32(tagHeader[5])<<8 | uint32(tagHeader[6]) | uint32(tagHeader[7])<<24
// stream id is tagHeader[8:11] (3 bytes), always 0
var frame rtmp.RTMPData
frame.SetAllocator(allocator)
if err = p.reader.ReadNto(dataSize, frame.NextN(dataSize)); err != nil {
break
}
ts = int64(timestamp)
if i != 0 || seekPosition == 0 {
ts += seekTsOffset
}
realTime = stream.StartTime.Add(time.Duration(timestamp) * time.Millisecond)
frame.Timestamp = uint32(ts)
switch t {
case FLV_TAG_TYPE_AUDIO:
if publisher.PubAudio {
err = publisher.WriteAudio(frame.WrapAudio())
}
case FLV_TAG_TYPE_VIDEO:
if publisher.PubVideo {
err = publisher.WriteVideo(frame.WrapVideo())
// After processing the first video frame, check if we need to seek
if i == 0 && seekPosition > 0 {
_, err = p.File.Seek(seekPosition, io.SeekStart)
if err != nil {
return
}
p.reader.Recycle()
p.reader = util.NewBufReader(p.File)
seekPosition = 0 // Reset to avoid seeking again
}
}
case FLV_TAG_TYPE_SCRIPT:
r := frame.NewReader()
amf := &rtmp.AMF{
Buffer: util.Buffer(r.ToBytes()),
}
frame.Recycle()
var obj any
if obj, err = amf.Unmarshal(); err != nil {
return
}
name := obj
if obj, err = amf.Unmarshal(); err != nil {
return
}
if i == 0 {
if metaData, ok := obj.(rtmp.EcmaArray); ok {
if keyframes, ok := metaData["keyframes"].(map[string]any); ok {
filepositions := keyframes["filepositions"].([]any)
times := keyframes["times"].([]any)
for i, t := range times {
if ts := int64(t.(float64) * 1000); ts > startTimestamp {
if i < 2 {
break
}
seekPosition = int64(filepositions[i-1].(float64) - 4)
seekTsOffset = -int64(times[i-1].(float64) * 1000)
break
}
}
}
}
} else {
publisher.Info("script", name, obj)
}
default:
err = fmt.Errorf("unknown tag type: %d", t)
}
if err != nil {
return
}
if p.MaxTS > 0 && ts > p.MaxTS {
return
}
}
}
}
return

View File

@@ -150,17 +150,7 @@ var CustomFileName = func(job *m7s.RecordJob) string {
if job.Fragment == 0 || job.Append {
return fmt.Sprintf("%s.flv", job.FilePath)
}
return filepath.Join(job.FilePath, time.Now().Local().Format("2006-01-02T15:04:05")+".flv")
}
func (r *Recorder) Start() (err error) {
if r.RecordJob.Plugin.DB != nil {
err = r.RecordJob.Plugin.DB.AutoMigrate(&r.stream)
if err != nil {
return
}
}
return r.DefaultRecorder.Start()
return filepath.Join(job.FilePath, fmt.Sprintf("%d.flv", time.Now().Unix()))
}
func (r *Recorder) createStream(start time.Time) (err error) {
@@ -196,6 +186,9 @@ func (r *Recorder) createStream(start time.Time) (err error) {
}
func (r *Recorder) writeTailer(end time.Time) {
if r.stream.EndTime.After(r.stream.StartTime) {
return
}
r.stream.EndTime = end
if r.RecordJob.Plugin.DB != nil {
r.RecordJob.Plugin.DB.Save(&r.stream)
@@ -206,6 +199,10 @@ func (r *Recorder) writeTailer(end time.Time) {
}
}
func (r *Recorder) Dispose() {
r.writeTailer(time.Now())
}
type eventRecordCheck struct {
task.Task
DB *gorm.DB
@@ -320,6 +317,14 @@ func (r *Recorder) Run() (err error) {
err = 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...)
offset += int64(seq.Size + 15)
}
}
}
}

View File

@@ -5,22 +5,19 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"unsafe"
"github.com/mcuadros/go-defaults"
"m7s.live/v5/pkg/config"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
m7s "m7s.live/v5"
"m7s.live/v5/pb"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pb"
mp4pb "m7s.live/v5/plugin/mp4/pb"
mp4 "m7s.live/v5/plugin/mp4/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
)
@@ -31,130 +28,6 @@ type ContentPart struct {
Size int
}
func (p *MP4Plugin) List(ctx context.Context, req *pb.ReqRecordList) (resp *pb.ResponseList, err error) {
var streams []m7s.RecordStream
if p.DB == nil {
err = pkg.ErrNoDB
return
}
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
var totalCount int64 //总条数
// 查询总记录数
countQuery := p.DB.Model(&m7s.RecordStream{})
// 查询当前页的数据
query := p.DB.Model(&m7s.RecordStream{})
if req.PageSize > 0 {
query = query.Limit(int(req.PageSize)).Offset(int(offset))
}
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"range": []string{req.Range}, "start": []string{req.Start}, "end": []string{req.End}})
if err != nil {
return
}
var condition string = "end_time>? AND start_time<?"
var values []any = []any{startTime, endTime}
if strings.Contains(req.StreamPath, "*") {
condition += " AND stream_path like ?"
values = append(values, strings.ReplaceAll(req.StreamPath, "*", "%"))
} else if req.StreamPath != "" {
condition += " AND stream_path=?"
values = append(values, req.StreamPath)
}
if req.Mode != "" {
condition += " AND mode=?"
values = append(values, req.Mode)
}
values = append([]any{condition}, values...)
err = countQuery.Find(&streams, values...).Count(&totalCount).Error
if err != nil {
return
}
query.Find(&streams, values...)
resp = &pb.ResponseList{
PageSize: req.PageSize,
PageNum: req.PageNum,
TotalCount: uint32(totalCount),
}
for _, stream := range streams {
resp.Data = append(resp.Data, &pb.RecordFile{
Id: uint32(stream.ID),
StartTime: timestamppb.New(stream.StartTime),
EndTime: timestamppb.New(stream.EndTime),
FilePath: stream.FilePath,
StreamPath: stream.StreamPath,
})
}
return
}
func (p *MP4Plugin) Catalog(ctx context.Context, req *emptypb.Empty) (resp *pb.ResponseCatalog, err error) {
if p.DB == nil {
err = pkg.ErrNoDB
return
}
resp = &pb.ResponseCatalog{}
var result []struct {
StreamPath string
Count uint
StartTime time.Time
EndTime time.Time
}
err = p.DB.Model(&m7s.RecordStream{}).Select("stream_path,count(id) as count,min(start_time) as start_time,max(end_time) as end_time").Group("stream_path").Find(&result).Error
if err != nil {
return
}
for _, row := range result {
resp.Data = append(resp.Data, &pb.Catalog{
StreamPath: row.StreamPath,
Count: uint32(row.Count),
StartTime: timestamppb.New(row.StartTime),
EndTime: timestamppb.New(row.EndTime),
})
}
return
}
func (p *MP4Plugin) Delete(ctx context.Context, req *pb.ReqRecordDelete) (resp *pb.ResponseDelete, err error) {
if p.DB == nil {
err = pkg.ErrNoDB
return
}
ids := req.GetIds()
var result []*m7s.RecordStream
if len(ids) > 0 {
p.DB.Find(&result, "stream_path=? AND id IN ?", req.StreamPath, ids)
} else {
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"range": []string{req.Range}, "start": []string{req.StartTime}, "end": []string{req.EndTime}})
if err != nil {
return nil, err
}
p.DB.Find(&result, "stream_path=? AND start_time>=? AND end_time<=?", req.StreamPath, startTime, endTime)
}
err = p.DB.Delete(result).Error
if err != nil {
return
}
var apiResult []*pb.RecordFile
for _, recordFile := range result {
apiResult = append(apiResult, &pb.RecordFile{
Id: uint32(recordFile.ID),
StartTime: timestamppb.New(recordFile.StartTime),
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
})
err = os.Remove(recordFile.FilePath)
if err != nil {
return
}
}
resp = &pb.ResponseDelete{
Data: apiResult,
}
return
}
func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
if p.DB == nil {
http.Error(w, pkg.ErrNoDB.Error(), http.StatusInternalServerError)
@@ -298,9 +171,9 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
}
}
func (p *MP4Plugin) StartRecord(ctx context.Context, req *pb.ReqStartRecord) (res *pb.ResponseStartRecord, err error) {
func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord) (res *mp4pb.ResponseStartRecord, err error) {
var recordExists bool
res = &pb.ResponseStartRecord{}
res = &mp4pb.ResponseStartRecord{}
p.Server.Records.Call(func() error {
_, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
return job.StreamPath == req.StreamPath && job.FilePath == req.FilePath
@@ -326,10 +199,10 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *pb.ReqStartRecord) (re
return
}
func (p *MP4Plugin) EventStart(ctx context.Context, req *pb.ReqEventRecord) (res *pb.ResponseEventRecord, err error) {
func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (res *mp4pb.ResponseEventRecord, err error) {
beforeDuration := p.BeforeDuration
afterDuration := p.AfterDuration
res = &pb.ResponseEventRecord{}
res = &mp4pb.ResponseEventRecord{}
if req.BeforeDuration != "" {
beforeDuration, err = time.ParseDuration(req.BeforeDuration)
if err != nil {
@@ -399,3 +272,33 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *pb.ReqEventRecord) (res
}
return res, err
}
func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *pb.ResponseList, err error) {
globalReq := &pb.ReqRecordList{
StreamPath: req.StreamPath,
Range: req.Range,
Start: req.Start,
End: req.End,
PageNum: req.PageNum,
PageSize: req.PageSize,
Mode: req.Mode,
Type: "mp4",
}
return p.Server.GetRecordList(ctx, globalReq)
}
func (p *MP4Plugin) Catalog(ctx context.Context, req *emptypb.Empty) (resp *pb.ResponseCatalog, err error) {
return p.Server.GetRecordCatalog(ctx, &pb.ReqRecordCatalog{Type: "mp4"})
}
func (p *MP4Plugin) Delete(ctx context.Context, req *mp4pb.ReqRecordDelete) (resp *pb.ResponseDelete, err error) {
globalReq := &pb.ReqRecordDelete{
StreamPath: req.StreamPath,
Ids: req.Ids,
StartTime: req.StartTime,
EndTime: req.EndTime,
Range: req.Range,
Type: "mp4",
}
return p.Server.DeleteRecord(ctx, globalReq)
}

View File

@@ -93,7 +93,6 @@ func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
func (p *MP4Plugin) OnInit() (err error) {
if p.DB != nil {
err = p.DB.AutoMigrate(&Exception{})
p.DB.AutoMigrate(&m7s.RecordStream{})
var deleteRecordTask DeleteRecordTask
deleteRecordTask.DB = p.DB
deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent

View File

@@ -12,7 +12,8 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
durationpb "google.golang.org/protobuf/types/known/durationpb"
emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
_ "google.golang.org/protobuf/types/known/timestamppb"
pb "m7s.live/v5/pb"
reflect "reflect"
sync "sync"
)
@@ -119,306 +120,6 @@ func (x *ReqRecordList) GetMode() string {
return ""
}
type RecordFile struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
StreamPath string `protobuf:"bytes,3,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
StartTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=endTime,proto3" json:"endTime,omitempty"`
}
func (x *RecordFile) Reset() {
*x = RecordFile{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RecordFile) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RecordFile) ProtoMessage() {}
func (x *RecordFile) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RecordFile.ProtoReflect.Descriptor instead.
func (*RecordFile) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{1}
}
func (x *RecordFile) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *RecordFile) GetFilePath() string {
if x != nil {
return x.FilePath
}
return ""
}
func (x *RecordFile) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
func (x *RecordFile) GetStartTime() *timestamppb.Timestamp {
if x != nil {
return x.StartTime
}
return nil
}
func (x *RecordFile) GetEndTime() *timestamppb.Timestamp {
if x != nil {
return x.EndTime
}
return nil
}
type ResponseList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
TotalCount uint32 `protobuf:"varint,3,opt,name=totalCount,proto3" json:"totalCount,omitempty"`
PageNum uint32 `protobuf:"varint,4,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
PageSize uint32 `protobuf:"varint,5,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
Data []*RecordFile `protobuf:"bytes,6,rep,name=data,proto3" json:"data,omitempty"`
}
func (x *ResponseList) Reset() {
*x = ResponseList{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResponseList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResponseList) ProtoMessage() {}
func (x *ResponseList) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResponseList.ProtoReflect.Descriptor instead.
func (*ResponseList) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{2}
}
func (x *ResponseList) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *ResponseList) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ResponseList) GetTotalCount() uint32 {
if x != nil {
return x.TotalCount
}
return 0
}
func (x *ResponseList) GetPageNum() uint32 {
if x != nil {
return x.PageNum
}
return 0
}
func (x *ResponseList) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *ResponseList) GetData() []*RecordFile {
if x != nil {
return x.Data
}
return nil
}
type Catalog struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Count uint32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`
StartTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
}
func (x *Catalog) Reset() {
*x = Catalog{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Catalog) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Catalog) ProtoMessage() {}
func (x *Catalog) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Catalog.ProtoReflect.Descriptor instead.
func (*Catalog) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{3}
}
func (x *Catalog) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
func (x *Catalog) GetCount() uint32 {
if x != nil {
return x.Count
}
return 0
}
func (x *Catalog) GetStartTime() *timestamppb.Timestamp {
if x != nil {
return x.StartTime
}
return nil
}
func (x *Catalog) GetEndTime() *timestamppb.Timestamp {
if x != nil {
return x.EndTime
}
return nil
}
type ResponseCatalog struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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 []*Catalog `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty"`
}
func (x *ResponseCatalog) Reset() {
*x = ResponseCatalog{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResponseCatalog) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResponseCatalog) ProtoMessage() {}
func (x *ResponseCatalog) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResponseCatalog.ProtoReflect.Descriptor instead.
func (*ResponseCatalog) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{4}
}
func (x *ResponseCatalog) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *ResponseCatalog) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ResponseCatalog) GetData() []*Catalog {
if x != nil {
return x.Data
}
return nil
}
type ReqRecordDelete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -434,7 +135,7 @@ type ReqRecordDelete struct {
func (x *ReqRecordDelete) Reset() {
*x = ReqRecordDelete{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[5]
mi := &file_mp4_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -447,7 +148,7 @@ func (x *ReqRecordDelete) String() string {
func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[5]
mi := &file_mp4_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -460,7 +161,7 @@ func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReqRecordDelete.ProtoReflect.Descriptor instead.
func (*ReqRecordDelete) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{5}
return file_mp4_proto_rawDescGZIP(), []int{1}
}
func (x *ReqRecordDelete) GetStreamPath() string {
@@ -498,69 +199,6 @@ func (x *ReqRecordDelete) GetRange() string {
return ""
}
type ResponseDelete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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 []*RecordFile `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty"`
}
func (x *ResponseDelete) Reset() {
*x = ResponseDelete{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResponseDelete) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResponseDelete) ProtoMessage() {}
func (x *ResponseDelete) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResponseDelete.ProtoReflect.Descriptor instead.
func (*ResponseDelete) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{6}
}
func (x *ResponseDelete) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *ResponseDelete) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ResponseDelete) GetData() []*RecordFile {
if x != nil {
return x.Data
}
return nil
}
type ReqEventRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -568,7 +206,7 @@ type ReqEventRecord struct {
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
EventId string `protobuf:"bytes,2,opt,name=eventId,proto3" json:"eventId,omitempty"`
RecordMode string `protobuf:"bytes,3,opt,name=recordMode,proto3" json:"recordMode,omitempty"` //auto=连续录像模式event=事件录像模式
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` //auto=连续录像模式event=事件录像模式
EventName string `protobuf:"bytes,4,opt,name=eventName,proto3" json:"eventName,omitempty"`
BeforeDuration string `protobuf:"bytes,5,opt,name=beforeDuration,proto3" json:"beforeDuration,omitempty"`
AfterDuration string `protobuf:"bytes,6,opt,name=afterDuration,proto3" json:"afterDuration,omitempty"`
@@ -580,7 +218,7 @@ type ReqEventRecord struct {
func (x *ReqEventRecord) Reset() {
*x = ReqEventRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[7]
mi := &file_mp4_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -593,7 +231,7 @@ func (x *ReqEventRecord) String() string {
func (*ReqEventRecord) ProtoMessage() {}
func (x *ReqEventRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[7]
mi := &file_mp4_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -606,7 +244,7 @@ func (x *ReqEventRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReqEventRecord.ProtoReflect.Descriptor instead.
func (*ReqEventRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{7}
return file_mp4_proto_rawDescGZIP(), []int{2}
}
func (x *ReqEventRecord) GetStreamPath() string {
@@ -623,9 +261,9 @@ func (x *ReqEventRecord) GetEventId() string {
return ""
}
func (x *ReqEventRecord) GetRecordMode() string {
func (x *ReqEventRecord) GetMode() string {
if x != nil {
return x.RecordMode
return x.Mode
}
return ""
}
@@ -685,7 +323,7 @@ type ResponseEventRecord struct {
func (x *ResponseEventRecord) Reset() {
*x = ResponseEventRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[8]
mi := &file_mp4_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -698,7 +336,7 @@ func (x *ResponseEventRecord) String() string {
func (*ResponseEventRecord) ProtoMessage() {}
func (x *ResponseEventRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[8]
mi := &file_mp4_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -711,7 +349,7 @@ func (x *ResponseEventRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResponseEventRecord.ProtoReflect.Descriptor instead.
func (*ResponseEventRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{8}
return file_mp4_proto_rawDescGZIP(), []int{3}
}
func (x *ResponseEventRecord) GetCode() int32 {
@@ -748,7 +386,7 @@ type ReqStartRecord struct {
func (x *ReqStartRecord) Reset() {
*x = ReqStartRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[9]
mi := &file_mp4_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -761,7 +399,7 @@ func (x *ReqStartRecord) String() string {
func (*ReqStartRecord) ProtoMessage() {}
func (x *ReqStartRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[9]
mi := &file_mp4_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -774,7 +412,7 @@ func (x *ReqStartRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReqStartRecord.ProtoReflect.Descriptor instead.
func (*ReqStartRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{9}
return file_mp4_proto_rawDescGZIP(), []int{4}
}
func (x *ReqStartRecord) GetStreamPath() string {
@@ -811,7 +449,7 @@ type ResponseStartRecord struct {
func (x *ResponseStartRecord) Reset() {
*x = ResponseStartRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[10]
mi := &file_mp4_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -824,7 +462,7 @@ func (x *ResponseStartRecord) String() string {
func (*ResponseStartRecord) ProtoMessage() {}
func (x *ResponseStartRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[10]
mi := &file_mp4_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -837,7 +475,7 @@ func (x *ResponseStartRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResponseStartRecord.ProtoReflect.Descriptor instead.
func (*ResponseStartRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{10}
return file_mp4_proto_rawDescGZIP(), []int{5}
}
func (x *ResponseStartRecord) GetCode() int32 {
@@ -872,147 +510,99 @@ var file_mp4_proto_rawDesc = []byte{
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, 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, 0xc8, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74,
0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74,
0x68, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74,
0x68, 0x12, 0x38, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x65,
0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
0x65, 0x22, 0xb7, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69,
0x73, 0x74, 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, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74,
0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x04, 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, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61,
0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xaf, 0x01, 0x0a, 0x07,
0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 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, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x38, 0x0a,
0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x61, 0x0a,
0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67,
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, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6d,
0x70, 0x34, 0x2e, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61,
0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54,
0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74,
0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x14,
0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72,
0x61, 0x6e, 0x67, 0x65, 0x22, 0x63, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05,
0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e,
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61,
0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67,
0x65, 0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6d, 0x6f, 0x64, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54,
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0xa4, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72,
0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12,
0x24, 0x0a, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65,
0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44,
0x65, 0x73, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65,
0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22,
0x57, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x46,
0x69, 0x6c, 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb0, 0x02, 0x0a, 0x0e, 0x52, 0x65,
0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x4d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e,
0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x65,
0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d,
0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x18,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63,
0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x08,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c,
0x12, 0x1a, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x57, 0x0a, 0x13,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61,
0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67,
0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x12,
0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x13, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04,
0x64, 0x61, 0x74, 0x61, 0x32, 0xd6, 0x03, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12, 0x54, 0x0a, 0x04,
0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69,
0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a,
0x2a, 0x7d, 0x12, 0x51, 0x0a, 0x07, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e,
0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71,
0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66,
0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61,
0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x5f, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12,
0x14, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44,
0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x24, 0x22, 0x1f, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c,
0x65, 0x74, 0x65, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d,
0x2a, 0x2a, 0x7d, 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53,
0x74, 0x61, 0x72, 0x74, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x6d, 0x70,
0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x72,
0x74, 0x3a, 0x01, 0x2a, 0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61,
0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x6d, 0x70, 0x34,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a,
0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75,
0x67, 0x69, 0x6e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57,
0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28,
0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xdf, 0x03, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12,
0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65,
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73,
0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07, 0x43, 0x61, 0x74, 0x61,
0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74,
0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x6d,
0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x62,
0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a, 0x16,
0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22, 0x1f,
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f,
0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a,
0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x72, 0x74,
0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22,
0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x3a, 0x01, 0x2a,
0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12,
0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x29,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73,
0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1027,47 +617,37 @@ func file_mp4_proto_rawDescGZIP() []byte {
return file_mp4_proto_rawDescData
}
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_mp4_proto_goTypes = []interface{}{
(*ReqRecordList)(nil), // 0: mp4.ReqRecordList
(*RecordFile)(nil), // 1: mp4.RecordFile
(*ResponseList)(nil), // 2: mp4.ResponseList
(*Catalog)(nil), // 3: mp4.Catalog
(*ResponseCatalog)(nil), // 4: mp4.ResponseCatalog
(*ReqRecordDelete)(nil), // 5: mp4.ReqRecordDelete
(*ResponseDelete)(nil), // 6: mp4.ResponseDelete
(*ReqEventRecord)(nil), // 7: mp4.ReqEventRecord
(*ResponseEventRecord)(nil), // 8: mp4.ResponseEventRecord
(*ReqStartRecord)(nil), // 9: mp4.ReqStartRecord
(*ResponseStartRecord)(nil), // 10: mp4.ResponseStartRecord
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
(*durationpb.Duration)(nil), // 12: google.protobuf.Duration
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
(*ReqRecordList)(nil), // 0: mp4.ReqRecordList
(*ReqRecordDelete)(nil), // 1: mp4.ReqRecordDelete
(*ReqEventRecord)(nil), // 2: mp4.ReqEventRecord
(*ResponseEventRecord)(nil), // 3: mp4.ResponseEventRecord
(*ReqStartRecord)(nil), // 4: mp4.ReqStartRecord
(*ResponseStartRecord)(nil), // 5: mp4.ResponseStartRecord
(*durationpb.Duration)(nil), // 6: google.protobuf.Duration
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(*pb.ResponseList)(nil), // 8: global.ResponseList
(*pb.ResponseCatalog)(nil), // 9: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 10: global.ResponseDelete
}
var file_mp4_proto_depIdxs = []int32{
11, // 0: mp4.RecordFile.startTime:type_name -> google.protobuf.Timestamp
11, // 1: mp4.RecordFile.endTime:type_name -> google.protobuf.Timestamp
1, // 2: mp4.ResponseList.data:type_name -> mp4.RecordFile
11, // 3: mp4.Catalog.startTime:type_name -> google.protobuf.Timestamp
11, // 4: mp4.Catalog.endTime:type_name -> google.protobuf.Timestamp
3, // 5: mp4.ResponseCatalog.data:type_name -> mp4.Catalog
1, // 6: mp4.ResponseDelete.data:type_name -> mp4.RecordFile
12, // 7: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
0, // 8: mp4.api.List:input_type -> mp4.ReqRecordList
13, // 9: mp4.api.Catalog:input_type -> google.protobuf.Empty
5, // 10: mp4.api.Delete:input_type -> mp4.ReqRecordDelete
7, // 11: mp4.api.EventStart:input_type -> mp4.ReqEventRecord
9, // 12: mp4.api.StartRecord:input_type -> mp4.ReqStartRecord
2, // 13: mp4.api.List:output_type -> mp4.ResponseList
4, // 14: mp4.api.Catalog:output_type -> mp4.ResponseCatalog
6, // 15: mp4.api.Delete:output_type -> mp4.ResponseDelete
8, // 16: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
10, // 17: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
13, // [13:18] is the sub-list for method output_type
8, // [8:13] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
6, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
0, // 1: mp4.api.List:input_type -> mp4.ReqRecordList
7, // 2: mp4.api.Catalog:input_type -> google.protobuf.Empty
1, // 3: mp4.api.Delete:input_type -> mp4.ReqRecordDelete
2, // 4: mp4.api.EventStart:input_type -> mp4.ReqEventRecord
4, // 5: mp4.api.StartRecord:input_type -> mp4.ReqStartRecord
8, // 6: mp4.api.List:output_type -> global.ResponseList
9, // 7: mp4.api.Catalog:output_type -> global.ResponseCatalog
10, // 8: mp4.api.Delete:output_type -> global.ResponseDelete
3, // 9: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
5, // 10: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
6, // [6:11] is the sub-list for method output_type
1, // [1:6] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_mp4_proto_init() }
@@ -1089,54 +669,6 @@ func file_mp4_proto_init() {
}
}
file_mp4_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RecordFile); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Catalog); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseCatalog); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqRecordDelete); i {
case 0:
return &v.state
@@ -1148,19 +680,7 @@ func file_mp4_proto_init() {
return nil
}
}
file_mp4_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseDelete); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
file_mp4_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqEventRecord); i {
case 0:
return &v.state
@@ -1172,7 +692,7 @@ func file_mp4_proto_init() {
return nil
}
}
file_mp4_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
file_mp4_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseEventRecord); i {
case 0:
return &v.state
@@ -1184,7 +704,7 @@ func file_mp4_proto_init() {
return nil
}
}
file_mp4_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
file_mp4_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqStartRecord); i {
case 0:
return &v.state
@@ -1196,7 +716,7 @@ func file_mp4_proto_init() {
return nil
}
}
file_mp4_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
file_mp4_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseStartRecord); i {
case 0:
return &v.state
@@ -1215,7 +735,7 @@ func file_mp4_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_mp4_proto_rawDesc,
NumEnums: 0,
NumMessages: 11,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -3,21 +3,22 @@ import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "global.proto";
package mp4;
option go_package="m7s.live/v5/plugin/mp4/pb";
service api {
rpc List (ReqRecordList) returns (ResponseList) {
rpc List (ReqRecordList) returns (global.ResponseList) {
option (google.api.http) = {
get: "/mp4/api/list/{streamPath=**}"
};
}
rpc Catalog (google.protobuf.Empty) returns (ResponseCatalog) {
rpc Catalog (google.protobuf.Empty) returns (global.ResponseCatalog) {
option (google.api.http) = {
get: "/mp4/api/catalog"
};
}
rpc Delete (ReqRecordDelete) returns (ResponseDelete) {
rpc Delete (ReqRecordDelete) returns (global.ResponseDelete) {
option (google.api.http) = {
post: "/mp4/api/delete/{streamPath=**}"
body: "*"
@@ -47,36 +48,6 @@ message ReqRecordList {
string mode = 7;
}
message RecordFile {
uint32 id = 1;
string filePath = 2;
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
}
message ResponseList {
int32 code = 1;
string message = 2;
uint32 totalCount = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated RecordFile data = 6;
}
message Catalog {
string streamPath = 1;
uint32 count = 2;
google.protobuf.Timestamp startTime = 3;
google.protobuf.Timestamp endTime = 4;
}
message ResponseCatalog {
int32 code = 1;
string message = 2;
repeated Catalog data = 3;
}
message ReqRecordDelete {
string streamPath = 1;
repeated uint32 ids = 2;
@@ -85,16 +56,10 @@ message ReqRecordDelete {
string range = 5;
}
message ResponseDelete {
int32 code = 1;
string message = 2;
repeated RecordFile data = 3;
}
message ReqEventRecord {
string streamPath = 1;
string eventId = 2;
string recordMode = 3;//auto=连续录像模式event=事件录像模式
string mode = 3;//auto=连续录像模式event=事件录像模式
string eventName = 4;
string beforeDuration = 5;
string afterDuration = 6;

View File

@@ -12,6 +12,7 @@ import (
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
pb "m7s.live/v5/pb"
)
// This is a compile-time assertion to ensure that this generated file
@@ -23,9 +24,9 @@ const _ = grpc.SupportPackageIsVersion7
//
// 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) (*ResponseList, error)
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ResponseCatalog, error)
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error)
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, 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)
EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error)
StartRecord(ctx context.Context, in *ReqStartRecord, opts ...grpc.CallOption) (*ResponseStartRecord, error)
}
@@ -38,8 +39,8 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*ResponseList, error) {
out := new(ResponseList)
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
out := new(pb.ResponseList)
err := c.cc.Invoke(ctx, "/mp4.api/List", in, out, opts...)
if err != nil {
return nil, err
@@ -47,8 +48,8 @@ func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.Ca
return out, nil
}
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ResponseCatalog, error) {
out := new(ResponseCatalog)
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
out := new(pb.ResponseCatalog)
err := c.cc.Invoke(ctx, "/mp4.api/Catalog", in, out, opts...)
if err != nil {
return nil, err
@@ -56,8 +57,8 @@ func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc
return out, nil
}
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error) {
out := new(ResponseDelete)
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
out := new(pb.ResponseDelete)
err := c.cc.Invoke(ctx, "/mp4.api/Delete", in, out, opts...)
if err != nil {
return nil, err
@@ -87,9 +88,9 @@ func (c *apiClient) StartRecord(ctx context.Context, in *ReqStartRecord, opts ..
// All implementations must embed UnimplementedApiServer
// for forward compatibility
type ApiServer interface {
List(context.Context, *ReqRecordList) (*ResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*ResponseDelete, error)
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
EventStart(context.Context, *ReqEventRecord) (*ResponseEventRecord, error)
StartRecord(context.Context, *ReqStartRecord) (*ResponseStartRecord, error)
mustEmbedUnimplementedApiServer()
@@ -99,13 +100,13 @@ type ApiServer interface {
type UnimplementedApiServer struct {
}
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*ResponseList, error) {
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*ResponseCatalog, error) {
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
return nil, status.Errorf(codes.Unimplemented, "method Catalog not implemented")
}
func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*ResponseDelete, error) {
func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedApiServer) EventStart(context.Context, *ReqEventRecord) (*ResponseEventRecord, error) {

View File

@@ -3,15 +3,13 @@ package mp4
import (
"io"
"os"
"strconv"
"strings"
"time"
"m7s.live/v5/pkg"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pkg/box"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
@@ -26,152 +24,156 @@ type (
func NewPuller(conf config.Pull) m7s.IPuller {
if strings.HasPrefix(conf.URL, "http") || strings.HasSuffix(conf.URL, ".mp4") {
return &HTTPReader{}
p := &HTTPReader{}
p.SetDescription(task.OwnerTypeKey, "Mp4Reader")
return p
}
if conf.Args.Get(util.StartKey) != "" {
return &RecordReader{}
p := &RecordReader{}
p.Type = "mp4"
p.SetDescription(task.OwnerTypeKey, "Mp4RecordReader")
return p
}
return nil
}
func (p *RecordReader) Run() (err error) {
pullJob := &p.PullJob
pullStartTime := p.PullStartTime
publisher := pullJob.Publisher
allocator := util.NewScalableMemoryAllocator(1 << 10)
var ts, tsOffset int64
var realTime time.Time
defer allocator.Recycle()
publisher.OnSeek = func(seekTime time.Time) {
pullStartTime = seekTime
p.SetRetry(1, 0)
if util.UnixTimeReg.MatchString(pullJob.Args.Get(util.EndKey)) {
pullJob.Args.Set(util.StartKey, strconv.FormatInt(pullStartTime.Unix(), 10))
} else {
pullJob.Args.Set(util.StartKey, pullStartTime.Local().Format(util.LocalTimeFormat))
}
publisher.Stop(pkg.ErrSeek)
}
publisher.OnGetPosition = func() time.Time {
return realTime
}
for i, stream := range p.Streams {
tsOffset = ts
if p.File != nil {
p.File.Close()
}
p.File, err = os.Open(stream.FilePath)
if err != nil {
continue
}
p.demuxer = NewDemuxer(p.File)
if err = p.demuxer.Demux(); err != nil {
return
}
if i == 0 {
for _, track := range p.demuxer.Tracks {
switch track.Cid {
case box.MP4_CODEC_H264:
var sequence rtmp.RTMPVideo
sequence.SetAllocator(allocator)
sequence.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
err = publisher.WriteVideo(&sequence)
case box.MP4_CODEC_H265:
var sequence rtmp.RTMPVideo
sequence.SetAllocator(allocator)
sequence.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
err = publisher.WriteVideo(&sequence)
case box.MP4_CODEC_AAC:
var sequence rtmp.RTMPAudio
sequence.SetAllocator(allocator)
sequence.Append([]byte{0xaf, 0x00}, track.ExtraData)
err = publisher.WriteAudio(&sequence)
}
for loop := 0; loop < p.Loop; loop++ {
nextStream:
for i, stream := range p.Streams {
tsOffset = ts
if p.File != nil {
p.File.Close()
}
startTimestamp := pullStartTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
var startSample *box.Sample
if startSample, err = p.demuxer.SeekTime(uint64(startTimestamp)); err != nil {
tsOffset = 0
p.File, err = os.Open(stream.FilePath)
if err != nil {
continue
}
tsOffset = -int64(startSample.DTS)
}
for track, sample := range p.demuxer.ReadSample {
if p.IsStopped() {
return p.StopReason()
p.demuxer = NewDemuxer(p.File)
if err = p.demuxer.Demux(); err != nil {
return
}
if publisher.Paused != nil {
publisher.Paused.Await()
if i == 0 {
for _, track := range p.demuxer.Tracks {
switch track.Cid {
case box.MP4_CODEC_H264:
var sequence rtmp.RTMPVideo
sequence.SetAllocator(allocator)
sequence.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
err = publisher.WriteVideo(&sequence)
case box.MP4_CODEC_H265:
var sequence rtmp.RTMPVideo
sequence.SetAllocator(allocator)
sequence.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
err = publisher.WriteVideo(&sequence)
case box.MP4_CODEC_AAC:
var sequence rtmp.RTMPAudio
sequence.SetAllocator(allocator)
sequence.Append([]byte{0xaf, 0x00}, track.ExtraData)
err = publisher.WriteAudio(&sequence)
}
}
startTimestamp := p.PullStartTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
var startSample *box.Sample
if startSample, err = p.demuxer.SeekTime(uint64(startTimestamp)); err != nil {
tsOffset = 0
continue
}
tsOffset = -int64(startSample.DTS)
}
if _, err = p.demuxer.reader.Seek(sample.Offset, io.SeekStart); err != nil {
return
}
sample.Data = allocator.Malloc(sample.Size)
if _, err = io.ReadFull(p.demuxer.reader, sample.Data); err != nil {
allocator.Free(sample.Data)
return
}
ts = int64(sample.DTS + uint64(tsOffset))
realTime = stream.StartTime.Add(time.Duration(sample.DTS) * time.Millisecond)
if p.MaxTS > 0 && ts > p.MaxTS {
return
}
switch track.Cid {
case box.MP4_CODEC_H264:
var videoFrame rtmp.RTMPVideo
videoFrame.SetAllocator(allocator)
videoFrame.CTS = uint32(sample.PTS - sample.DTS)
videoFrame.Timestamp = uint32(ts)
videoFrame.AppendOne([]byte{util.Conditional[byte](sample.KeyFrame, 0x17, 0x27), 0x01, byte(videoFrame.CTS >> 24), byte(videoFrame.CTS >> 8), byte(videoFrame.CTS)})
videoFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_H265:
var videoFrame rtmp.RTMPVideo
videoFrame.SetAllocator(allocator)
videoFrame.CTS = uint32(sample.PTS - sample.DTS)
videoFrame.Timestamp = uint32(ts)
var head []byte
var b0 byte = 0b1010_0000
if sample.KeyFrame {
b0 = 0b1001_0000
for track, sample := range p.demuxer.ReadSample {
if p.IsStopped() {
return p.StopReason()
}
if videoFrame.CTS == 0 {
head = videoFrame.NextN(5)
head[0] = b0 | rtmp.PacketTypeCodedFramesX
} else {
head = videoFrame.NextN(8)
head[0] = b0 | rtmp.PacketTypeCodedFrames
util.PutBE(head[5:8], videoFrame.CTS) // cts
if publisher.Paused != nil {
publisher.Paused.Await()
}
if needSeek, err := p.CheckSeek(); err != nil {
continue
} else if needSeek {
goto nextStream
}
if _, err = p.demuxer.reader.Seek(sample.Offset, io.SeekStart); err != nil {
return
}
sample.Data = allocator.Malloc(sample.Size)
if _, err = io.ReadFull(p.demuxer.reader, sample.Data); err != nil {
allocator.Free(sample.Data)
return
}
ts = int64(sample.DTS + uint64(tsOffset))
realTime = stream.StartTime.Add(time.Duration(sample.DTS) * time.Millisecond)
if p.MaxTS > 0 && ts > p.MaxTS {
return
}
switch track.Cid {
case box.MP4_CODEC_H264:
var videoFrame rtmp.RTMPVideo
videoFrame.SetAllocator(allocator)
videoFrame.CTS = uint32(sample.PTS - sample.DTS)
videoFrame.Timestamp = uint32(ts)
videoFrame.AppendOne([]byte{util.Conditional[byte](sample.KeyFrame, 0x17, 0x27), 0x01, byte(videoFrame.CTS >> 24), byte(videoFrame.CTS >> 8), byte(videoFrame.CTS)})
videoFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_H265:
var videoFrame rtmp.RTMPVideo
videoFrame.SetAllocator(allocator)
videoFrame.CTS = uint32(sample.PTS - sample.DTS)
videoFrame.Timestamp = uint32(ts)
var head []byte
var b0 byte = 0b1010_0000
if sample.KeyFrame {
b0 = 0b1001_0000
}
if videoFrame.CTS == 0 {
head = videoFrame.NextN(5)
head[0] = b0 | rtmp.PacketTypeCodedFramesX
} else {
head = videoFrame.NextN(8)
head[0] = b0 | rtmp.PacketTypeCodedFrames
util.PutBE(head[5:8], videoFrame.CTS) // cts
}
copy(head[1:], codec.FourCC_H265[:])
videoFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_AAC:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0xaf, 0x01})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711A:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0x72})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711U:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0x82})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
}
copy(head[1:], codec.FourCC_H265[:])
videoFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_AAC:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0xaf, 0x01})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711A:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0x72})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711U:
var audioFrame rtmp.RTMPAudio
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(ts)
audioFrame.AppendOne([]byte{0x82})
audioFrame.AddRecycleBytes(sample.Data)
err = publisher.WriteAudio(&audioFrame)
}
}
}

View File

@@ -180,16 +180,6 @@ func (r *Recorder) createStream(start time.Time) (err error) {
return
}
func (r *Recorder) Start() (err error) {
if r.RecordJob.Plugin.DB != nil {
err = r.RecordJob.Plugin.DB.AutoMigrate(&r.stream)
if err != nil {
return
}
}
return r.DefaultRecorder.Start()
}
func (r *Recorder) Dispose() {
if r.muxer != nil {
r.writeTailer(time.Now())
@@ -319,6 +309,27 @@ func (r *Recorder) Run() (err error) {
switch ctx := vt.ICodecCtx.(type) {
case *codec.H264Ctx:
if bytes[1] == 0 {
// Check if video resolution has changed
if uint32(ctx.Width()) != videoTrack.Width || uint32(ctx.Height()) != videoTrack.Height {
r.Info("Video resolution changed, restarting recording",
"old", fmt.Sprintf("%dx%d", videoTrack.Width, videoTrack.Height),
"new", fmt.Sprintf("%dx%d", ctx.Width(), ctx.Height()))
now := time.Now()
r.writeTailer(now)
err = r.createStream(now)
if err != nil {
return nil
}
at, vt = nil, nil
if vr := sub.VideoReader; vr != nil {
vr.ResetAbsTime()
//seq := vt.SequenceFrame.(*rtmp.RTMPVideo)
//offset = int64(seq.Size + 15)
}
if ar := sub.AudioReader; ar != nil {
ar.ResetAbsTime()
}
}
return nil
}
case *rtmp.H265Ctx:

175
plugin/onvif/README.md Normal file
View File

@@ -0,0 +1,175 @@
# ONVIF Plugin for Monibuca v5
ONVIF 插件用于发现和管理 ONVIF 设备,支持自动发现、添加和拉流等功能。
## 配置说明
```yaml
onvif:
discoverInterval: 30 # 设备发现间隔(秒)
autoPull: true # 是否自动拉流
autoAdd: true # 是否自动添加发现的设备
interfaces: # 网卡配置
- interfaceName: eth0
username: admin
password: admin
devices: # 设备配置
- ip: 192.168.1.100
username: admin
password: admin
```
## API 接口
### 1. 设备管理
#### 1.1 设备列表
- 路径:`/onvif/list`
- 方法GET
- 描述:获取所有已发现和添加的设备列表
#### 1.2 添加设备
- 路径:`/onvif/add`
- 方法POST
- 描述:手动添加 ONVIF 设备
- 参数:
```json
{
"ip": "192.168.1.100",
"port": "80",
"user": "admin",
"passwd": "admin",
"path": "",
"channel": 0
}
```
#### 1.3 移除设备
- 路径:`/onvif/remove`
- 方法POST
- 描述:移除已添加的设备
- 参数:
```json
{
"ip": "192.168.1.100"
}
```
#### 1.4 设备发现
- 路径:`/onvif/discovery`
- 方法GET
- 描述:手动触发设备发现
### 2. PTZ 控制
#### 2.1 云台移动
- 路径:`/onvif/ptz/move`
- 方法POST
- 描述:控制设备云台移动
- 参数:
```json
{
"ip": "192.168.1.100",
"mode": 0, // 0:绝对移动 1:相对移动 2:连续移动
"pan": 0.0, // 水平移动 -1.0 到 1.0
"tilt": 0.0, // 垂直移动 -1.0 到 1.0
"zoom": 0.0, // 缩放 -1.0 到 1.0
"speed": 1.0 // 移动速度 0.0 到 1.0
}
```
#### 2.2 获取预置点
- 路径:`/onvif/ptz/preset/get`
- 方法POST
- 描述:获取设备预置点列表
- 参数:
```json
{
"ip": "192.168.1.100"
}
```
#### 2.3 设置预置点
- 路径:`/onvif/ptz/preset/set`
- 方法POST
- 描述:设置预置点
- 参数:
```json
{
"ip": "192.168.1.100",
"preset_token": "1",
"preset_name": "position1"
}
```
#### 2.4 调用预置点
- 路径:`/onvif/ptz/preset/goto`
- 方法POST
- 描述:移动到预置点位置
- 参数:
```json
{
"ip": "192.168.1.100",
"preset_token": "1"
}
```
### 3. 图像设置
#### 3.1 获取图像参数
- 路径:`/onvif/imaging/get`
- 方法POST
- 描述:获取设备图像参数
- 参数:
```json
{
"ip": "192.168.1.100"
}
```
#### 3.2 设置图像参数
- 路径:`/onvif/imaging/set`
- 方法POST
- 描述:设置设备图像参数
- 参数:
```json
{
"ip": "192.168.1.100",
"brightness": 50.0,
"color_saturation": 50.0,
"contrast": 50.0,
"sharpness": 50.0,
"force": false
}
```
## 功能特性
1. 自动发现 ONVIF 设备
2. 支持手动添加设备
3. 自动获取设备视频流地址
4. 支持多通道选择
5. 支持自动拉流
6. 支持设备认证管理
7. PTZ 云台控制
- 绝对移动
- 相对移动
- 连续移动
- 预置点管理
8. 图像参数设置
- 亮度
- 对比度
- 饱和度
- 锐度
## 依赖要求
- Monibuca v5.0.0 或更高版本
- Go 1.18 或更高版本
## 安装方法
该插件已经包含在 Monibuca v5 的主仓库中,无需单独安装。只需在配置文件中启用该插件即可使用。
## VIRTUAL_IFACE 说明
VIRTUAL_IFACE 在 plugin-onvifpro 插件中被用作一个特殊的接口名称,主要用于以下目的:
虚拟接口标识: 它作为一个常量字符串,用于标识一种特殊的设备管理模式,可能用于手动添加或配置的设备,而不是通过网络接口自动发现的设备。
流路径解析: 在解析流路径时VIRTUAL_IFACE 被用作接口名称,以便正确提取设备地址。
设备列表组织: 设备列表使用 VIRTUAL_IFACE 作为 key 来组织设备,方便管理。

639
plugin/onvif/api.go Normal file
View File

@@ -0,0 +1,639 @@
package plugin_onvif
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"github.com/IOTechSystems/onvif/xsd/onvif"
"github.com/jinzhu/copier"
)
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type DeviceAddReq struct {
IP string `json:"ip"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"passwd"`
Path string `json:"path"`
Channel int `json:"channel"`
}
type PtzMoveReq struct {
IP string `json:"ip"`
Mode int `json:"mode"`
Pan float64 `json:"pan"`
Tilt float64 `json:"tilt"`
Zoom float64 `json:"zoom"`
Speed float64 `json:"speed"`
}
type PtzPresetReq struct {
IP string `json:"ip"`
PresetToken string `json:"preset_token"`
PresetName string `json:"preset_name"`
}
type ImagingReq struct {
IP string `json:"ip"`
Brightness float64 `json:"brightness"`
ColorSaturation float64 `json:"color_saturation"`
Contrast float64 `json:"contrast"`
Sharpness float64 `json:"sharpness"`
Force bool `json:"force"`
}
type DeviceParam struct {
Ip string `json:"ip"`
Port string `json:"port"`
User string `json:"user"`
Passwd string `json:"passwd"`
Path string `json:"path"`
IFace string `json:"iface"`
Channel int `json:"channel"`
}
/*
ip, user, passwd, port, path string, channel int
*/
func parseDeviceParam(r *http.Request) (*DeviceParam, error) {
ip := r.URL.Query().Get("ip")
port := r.URL.Query().Get("port")
user := r.URL.Query().Get("user")
passwd := r.URL.Query().Get("passwd")
path := r.URL.Query().Get("path")
channel := r.URL.Query().Get("channel")
iface := r.URL.Query().Get("iface")
if ip == "" {
return nil, fmt.Errorf("param ip error")
}
if channel == "" {
channel = "0"
}
channelInt, err := strconv.Atoi(channel)
if err != nil {
return nil, fmt.Errorf("param channel error")
}
if port == "" {
port = "80"
}
if path == "" {
path = "/onvif/device_service"
}
if iface == "" {
iface = VIRTUAL_IFACE
}
return &DeviceParam{
Ip: ip,
Port: port,
User: user,
Passwd: passwd,
Path: path,
IFace: iface,
Channel: channelInt,
}, nil
}
func getDevice(autoAdd bool, method string, resp *Response, w http.ResponseWriter, r *http.Request) (*DeviceStatus, error) {
w.Header().Set("Content-Type", "application/json")
if r.Method != method {
w.WriteHeader(http.StatusMethodNotAllowed)
resp.Code = http.StatusMethodNotAllowed
resp.Msg = "method not allowed"
json.NewEncoder(w).Encode(resp)
return nil, fmt.Errorf("method not allowed")
}
devParam, err := parseDeviceParam(r)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return nil, err
}
devs, ok := deviceList.Data.Get(devParam.IFace)
if !ok {
resp.Code = http.StatusBadRequest
resp.Msg = "iface not found"
json.NewEncoder(w).Encode(resp)
return nil, fmt.Errorf("iface not found")
}
dev, ok := devs.Get(devParam.Ip + ":" + devParam.Port)
if !ok {
if !autoAdd {
resp.Code = http.StatusBadRequest
resp.Msg = "device not found"
json.NewEncoder(w).Encode(resp)
return nil, fmt.Errorf("device not found")
}
//add device
ds, code, err := deviceList.AddDevice(devParam)
if err != nil {
resp.Msg = err.Error()
resp.Code = code
json.NewEncoder(w).Encode(resp)
return nil, err
}
dev = ds
}
if devParam.Channel > len(dev.Profiles) {
resp.Code = http.StatusBadRequest
resp.Msg = "channel out of range"
json.NewEncoder(w).Encode(resp)
return nil, fmt.Errorf("channel out of range")
}
dev.Channel = devParam.Channel
return dev, nil
}
func (o *OnvifPlugin) closeStream(resp *Response, w http.ResponseWriter, r *http.Request) error {
d, err := getDevice(o.AutoAdd, http.MethodPost, resp, w, r)
if err != nil {
return err
}
devParam, _ := parseDeviceParam(r)
streamPath := GenStreamPath(d.Device, devParam.IFace)
stream, _ := o.Server.Streams.Get(streamPath)
if stream == nil {
return nil
}
stream.Stop(errors.New("close manual"))
return nil
}
func (o *OnvifPlugin) API_list(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := Response{
Code: 0,
Msg: "ok",
Data: nil,
}
byList := r.URL.Query().Get("bylist")
if byList == "1" {
list := make([]*DeviceStatus, 0)
deviceList.Data.Range(func(ic *InterfaceCollection) bool {
ic.Range(func(ds *DeviceStatus) bool {
list = append(list, ds)
return true
})
return true
})
resp.Data = list
} else {
data := make(map[string]map[string]*DeviceStatus)
deviceList.Data.Range(func(ic *InterfaceCollection) bool {
if _, ok := data[ic.iface]; !ok {
data[ic.iface] = make(map[string]*DeviceStatus)
}
ic.Range(func(ds *DeviceStatus) bool {
data[ic.iface][ds.GetKey()] = ds
return true
})
return true
})
resp.Data = data
}
json.NewEncoder(w).Encode(resp)
return
}
func (o *OnvifPlugin) API_adddevice(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
resp.Code = http.StatusMethodNotAllowed
resp.Msg = "method not allowed"
json.NewEncoder(w).Encode(resp)
return
}
devParam, err := parseDeviceParam(r)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
//add device
_, code, err := deviceList.AddDevice(devParam)
if err != nil {
resp.Msg = err.Error()
resp.Code = code
json.NewEncoder(w).Encode(resp)
return
}
resp.Code = 0
json.NewEncoder(w).Encode(resp)
return
}
func (o *OnvifPlugin) API_deldevice(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
resp.Code = http.StatusMethodNotAllowed
resp.Msg = "method not allowed"
json.NewEncoder(w).Encode(resp)
return
}
devParam, err := parseDeviceParam(r)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
//先关闭流
err = o.closeStream(&resp, w, r)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
//add device
deviceList.DelDevice(devParam)
resp.Code = 0
json.NewEncoder(w).Encode(resp)
return
}
func (o *OnvifPlugin) API_pull(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
d, err := getDevice(o.AutoAdd, http.MethodGet, &resp, w, r)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
devParam, _ := parseDeviceParam(r)
err = d.PullStream(devParam.IFace, devParam.Channel)
if err != nil {
resp.Code = StatusInitError
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
resp.Data = map[string]any{
"stream": d.Stream,
}
json.NewEncoder(w).Encode(resp)
}
func (o *OnvifPlugin) API_close(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
o.closeStream(&resp, w, r)
json.NewEncoder(w).Encode(resp)
return
}
func (o *OnvifPlugin) API_status(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodGet, &resp, w, r)
if err != nil {
return
}
resp.Data = map[string]any{
"status": dev.Status,
}
json.NewEncoder(w).Encode(resp)
}
// 获取设备能力
func (o *OnvifPlugin) API_capability(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodGet, &resp, w, r)
if err != nil {
return
}
caps := dev.Device.GetServices()
resp.Data = caps
json.NewEncoder(w).Encode(resp)
}
// 获取图片能力信息
func (o *OnvifPlugin) API_imageProfile(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodGet, &resp, w, r)
if err != nil {
return
}
settings, err := dev.GetImaging()
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
resp.Data = settings
json.NewEncoder(w).Encode(resp)
}
func (o *OnvifPlugin) API_setImageProfile(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodPost, &resp, w, r)
if err != nil {
return
}
var settingReq ImageSettingReq
content, err := io.ReadAll(r.Body)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
err = json.Unmarshal(content, &settingReq)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
var settings onvif.ImagingSettings20
copier.Copy(&settings, settingReq.ImageSettings)
err = dev.SetImaging(&settings, settingReq.ForcePersistence)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
json.NewEncoder(w).Encode(resp)
}
// 获取预置位
func (o *OnvifPlugin) API_ptzPreset(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodGet, &resp, w, r)
if err != nil {
return
}
settings, err := dev.GetPtzPreset()
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
resp.Data = settings
json.NewEncoder(w).Encode(resp)
}
// 设置预置位 name 必填 preset_token 可选,如果提供则更新预置位
func (o *OnvifPlugin) API_setPtzPreset(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodPost, &resp, w, r)
if err != nil {
return
}
name := r.URL.Query().Get("name")
if name == "" {
resp.Code = http.StatusBadRequest
resp.Msg = "name is empty"
json.NewEncoder(w).Encode(resp)
return
}
presetToken := r.URL.Query().Get("preset_token")
token, err := dev.SetPtzPreset(name, presetToken)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
resp.Data = map[string]any{"Token": token}
json.NewEncoder(w).Encode(resp)
}
// 预置位移动
func (o *OnvifPlugin) API_gotoPtzPreset(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodPost, &resp, w, r)
if err != nil {
return
}
presetToken := r.URL.Query().Get("preset_token")
if presetToken == "" {
resp.Code = http.StatusBadRequest
resp.Msg = "preset_token is empty"
json.NewEncoder(w).Encode(resp)
return
}
content, err := io.ReadAll(r.Body)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
var speed *onvif.PTZSpeed
if string(content) != "" {
json.Unmarshal(content, &speed)
}
err = dev.GotoPtzPreset(presetToken, speed)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
json.NewEncoder(w).Encode(resp)
}
// 预置位删除
func (o *OnvifPlugin) API_removePtzPreset(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodPost, &resp, w, r)
if err != nil {
return
}
presetToken := r.URL.Query().Get("preset_token")
if presetToken == "" {
resp.Code = http.StatusBadRequest
resp.Msg = "preset_token is empty"
json.NewEncoder(w).Encode(resp)
return
}
err = dev.RemovePtzPreset(presetToken)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
json.NewEncoder(w).Encode(resp)
}
// 移动摄像头
func (o *OnvifPlugin) API_ptz(w http.ResponseWriter, r *http.Request) {
resp := Response{
Code: 0,
Msg: "ok",
Data: map[string]any{},
}
dev, err := getDevice(o.AutoAdd, http.MethodPost, &resp, w, r)
if err != nil {
return
}
mode := r.URL.Query().Get("mode")
if mode == "" {
resp.Code = http.StatusBadRequest
resp.Msg = "mode is empty"
json.NewEncoder(w).Encode(resp)
return
}
modeInt, err := strconv.Atoi(mode)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = "mode is error"
json.NewEncoder(w).Encode(resp)
return
}
content, err := io.ReadAll(r.Body)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
switch modeInt {
case PtzMoveAbs, PtzMoveRelative:
var ptzMove ptzMoveReq
err = json.Unmarshal(content, &ptzMove)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
err = dev.PtzMove(modeInt, ptzMove.Move, ptzMove.Speed)
case PtzMoveContinue:
var ptzContinueMove ptzContinueMoveReq
err = json.Unmarshal(content, &ptzContinueMove)
if err != nil {
resp.Code = http.StatusBadRequest
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
err = dev.PtzContinueMove(ptzContinueMove.Velocity, ptzContinueMove.Timeout)
default:
resp.Code = http.StatusBadRequest
resp.Msg = "mode is error"
json.NewEncoder(w).Encode(resp)
return
}
if err != nil {
resp.Code = StatusPtzMove
resp.Msg = err.Error()
json.NewEncoder(w).Encode(resp)
return
}
json.NewEncoder(w).Encode(resp)
}
func (p *OnvifPlugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/list": p.API_list,
"/add": p.API_adddevice,
"/remove": p.API_deldevice,
"/ptz": p.API_ptz,
"/ptz/preset/get": p.API_ptzPreset,
"/ptz/preset/set": p.API_setPtzPreset,
"/ptz/preset/goto": p.API_gotoPtzPreset,
"/imaging/get": p.API_imageProfile,
"/imaging/set": p.API_setImageProfile,
}
}

564
plugin/onvif/device.go Executable file
View File

@@ -0,0 +1,564 @@
package plugin_onvif
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"m7s.live/v5/pkg/util"
"strings"
donvif "github.com/IOTechSystems/onvif"
"github.com/IOTechSystems/onvif/gosoap"
"github.com/IOTechSystems/onvif/imaging"
"github.com/IOTechSystems/onvif/media"
"github.com/IOTechSystems/onvif/ptz"
"github.com/IOTechSystems/onvif/xsd"
"github.com/IOTechSystems/onvif/xsd/onvif"
onvifTypes "github.com/IOTechSystems/onvif/xsd/onvif"
"github.com/beevik/etree"
"m7s.live/v5/pkg/config"
rtsp "m7s.live/v5/plugin/rtsp/pkg"
)
// 设备状态常量
const (
StatusInitOk = iota
StatusInitError
StatusAddError
StatusProfileError
StatusGetStreamUriOk
StatusGetStreamUriError
StatusPullRtspOk
StatusPullRtspError
StatusGetImagingSetting
StatusSetImagingSetting
StatusGetPtzPreset
StatusSetPtzPreset
StatusGotoPtzPreset
StatusPtzMove
)
// PTZ移动模式
const (
PtzMoveAbs = iota
PtzMoveRelative
PtzMoveContinue
)
type DeviceStatus struct {
Device *donvif.Device
Xaddr string `json:"xaddr"` // onvif 设备地址
IP string `json:"ip"` // 设备IP
Port string `json:"port"` // 设备端口
Username string `json:"username"` // 设备用户名
Password string `json:"password"` // 设备密码
Path string `json:"path"` // onvif device_service 路径
MediaUrl string `json:"mediaUrl"` // rtsp 流
Channel int `json:"channel"` // 设备通道
Stream string `json:"stream"` // 设备流
Status int `json:"status"` // 设备状态
Description string `json:"description"` // 状态描述
Profiles []onvif.Profile
}
func (d *DeviceStatus) GetKey() string {
if d.Xaddr == "" {
d.Xaddr = d.IP + ":" + d.Port
}
return d.Xaddr
}
type AuthConfig struct {
Interfaces map[string]deviceAuth
Devices map[string]deviceAuth
}
type deviceAuth struct {
Username string
Password string
}
var authCfg = &AuthConfig{
Interfaces: make(map[string]deviceAuth),
Devices: make(map[string]deviceAuth),
}
func GenStreamPath(device *donvif.Device, ifname string) string {
streamPath := strings.ReplaceAll(device.GetDeviceParams().Xaddr, ".", "_")
streamPath = "onvif/" + util.ConvertRuneToEn(ifname) + "/" + strings.ReplaceAll(streamPath, ":", "_")
return streamPath
}
func NewDeviceStatus(ip, user, passwd, port, path string, channel int) (*DeviceStatus, int, error) {
param := donvif.DeviceParams{Xaddr: ip + ":" + port, Username: user, Password: passwd, EndpointRefAddress: path}
device, err := donvif.NewDevice(param)
if err != nil {
return nil, StatusAddError, err
}
profiles, err := GetProfiles(device)
if err != nil {
return nil, StatusProfileError, err
}
return &DeviceStatus{Device: device,
Channel: channel, Path: path, Profiles: profiles,
IP: ip, Port: port,
Username: user,
Password: passwd,
}, 0, nil
}
// MarshalJSON 实现设备状态的JSON序列化
func (d *DeviceStatus) MarshalJSON() ([]byte, error) {
type Alias DeviceStatus
return json.Marshal(&struct {
*Alias
Status int `json:"status"`
StatusText string `json:"status_text"`
Profiles int `json:"profiles,omitempty"`
StreamToken string `json:"stream_token,omitempty"`
}{
Alias: (*Alias)(d),
Status: d.Status,
StatusText: getStatusText(d.Status),
})
}
// GetImaging 获取图像设置
func (d *DeviceStatus) GetImaging() (*onvifTypes.ImagingSettings20, error) {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return nil, err
}
if len(d.Profiles) == 0 {
return nil, fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(imaging.GetImagingSettings{
VideoSourceToken: token,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var imagingSetting imaging.GetImagingSettingsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&imagingSetting)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return &imagingSetting.ImagingSettings, nil
}
// SetImaging 设置图像参数
func (d *DeviceStatus) SetImaging(settings *onvifTypes.ImagingSettings20, forcePersistence bool) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(imaging.SetImagingSettings{
VideoSourceToken: token,
ImagingSettings: *settings,
ForcePersistence: xsd.Boolean(forcePersistence),
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var imagingSetting imaging.SetImagingSettingsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&imagingSetting)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
// GetPtzPresets 获取预置点列表
func (d *DeviceStatus) GetPtzPresets() ([]onvifTypes.PTZPreset, error) {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return nil, err
}
if len(d.Profiles) == 0 {
return nil, fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(ptz.GetPresets{
ProfileToken: token,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.GetPresetsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return presets.Preset, nil
}
// SetPtzPreset 设置预置点
func (d *DeviceStatus) SetPtzPreset(name string, presetToken string) (*onvif.ReferenceToken, error) {
token := d.Profiles[d.Channel].Token
ptzName := xsd.String(name)
ptzPresetToken := onvif.ReferenceToken(presetToken)
result, err := d.Device.CallMethod(ptz.SetPreset{
ProfileToken: &token,
PresetToken: &ptzPresetToken,
PresetName: &ptzName,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.SetPresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return &presets.PresetToken, nil
}
// PtzMove PTZ移动控制
func (d *DeviceStatus) PtzMove(mode int, move ptz.Vector, speed ptz.Speed) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
switch mode {
case PtzMoveAbs:
result, err := dev.CallMethod(ptz.AbsoluteMove{
ProfileToken: token,
Position: move,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.AbsoluteMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
case PtzMoveRelative:
result, err := dev.CallMethod(ptz.RelativeMove{
ProfileToken: token,
Translation: move,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.RelativeMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
case PtzMoveContinue:
result, err := dev.CallMethod(ptz.ContinuousMove{
ProfileToken: &token,
Velocity: &onvifTypes.PTZSpeed{
PanTilt: move.PanTilt,
Zoom: move.Zoom,
},
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.ContinuousMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
default:
return fmt.Errorf("unknown ptz move mode: %d", mode)
}
}
// GotoPtzPreset 跳转到预置点
func (d *DeviceStatus) GotoPtzPreset(presetToken string, speed *onvifTypes.PTZSpeed) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
ptzPresetToken := onvifTypes.ReferenceToken(presetToken)
result, err := dev.CallMethod(ptz.GotoPreset{
ProfileToken: &token,
PresetToken: &ptzPresetToken,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var presets ptz.GotoPresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func (d *DeviceStatus) PullStream(ifname string, channel int) error {
// 生成流路径
streamPath := GenStreamPath(d.Device, ifname)
var rtspUrl string
var err error
if d.MediaUrl != "" {
rtspUrl = d.MediaUrl
} else {
// 获取 RTSP 流地址
rtspUrl, err = GetStreamUri(d.Device, d.Profiles[channel].Token)
if err != nil {
d.Status = StatusGetStreamUriError
return fmt.Errorf("get stream uri error: %v", err)
}
d.Status = StatusGetStreamUriOk
}
pullConf := config.Pull{
URL: rtspUrl,
}
// pubConf := config.Publish{}
puller := rtsp.NewPuller(pullConf)
puller.GetPullJob().Init(puller, &deviceList.plugin.Plugin, streamPath, pullConf, nil)
d.Status = StatusPullRtspOk
d.Stream = streamPath
return nil
}
func (d *DeviceStatus) GetPtzPreset() ([]onvif.PTZPreset, error) {
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.GetPresets{ProfileToken: token})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.GetPresetsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return presets.Preset, nil
}
func (d *DeviceStatus) RemovePtzPreset(presetToken string) error {
ptzPresetToken := onvif.ReferenceToken(presetToken)
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.RemovePreset{
ProfileToken: token,
PresetToken: ptzPresetToken,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var presets ptz.RemovePresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func (d *DeviceStatus) PtzContinueMove(vel *onvif.PTZSpeed, tmout *xsd.Duration) error {
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.ContinuousMove{
ProfileToken: &token,
Velocity: vel,
Timeout: tmout,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var resp ptz.ContinuousMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&resp)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func getStatusText(status int) string {
switch status {
case StatusInitOk:
return "初始化成功"
case StatusInitError:
return "初始化失败"
case StatusAddError:
return "添加设备失败"
case StatusProfileError:
return "获取配置文件失败"
case StatusGetStreamUriOk:
return "获取流地址成功"
case StatusGetStreamUriError:
return "获取流地址失败"
case StatusPullRtspOk:
return "拉流成功"
case StatusPullRtspError:
return "拉流失败"
case StatusGetImagingSetting:
return "获取图像设置"
case StatusSetImagingSetting:
return "设置图像参数"
case StatusGetPtzPreset:
return "获取预置点"
case StatusSetPtzPreset:
return "设置预置点"
case StatusGotoPtzPreset:
return "跳转预置点"
case StatusPtzMove:
return "云台控制"
default:
return "未知状态"
}
}
func GetStreamUri(dev *donvif.Device, profileToken onvifTypes.ReferenceToken) (string, error) {
response, err := dev.CallMethod(media.GetStreamUri{ProfileToken: &profileToken})
if err != nil {
return "", err
}
resp, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
doc := etree.NewDocument()
if err := doc.ReadFromBytes(resp); err != nil {
return "", fmt.Errorf("error:%s", err.Error())
}
endpoints := doc.Root().FindElements("./Body/GetStreamUriResponse/MediaUri/Uri")
if len(endpoints) == 0 {
return "", fmt.Errorf("error:%s", "no media uri")
}
mediaUri := endpoints[0].Text()
if !strings.Contains(mediaUri, "rtsp") {
fmt.Println("mediaUri:", mediaUri)
return "", fmt.Errorf("error:%s", "media uri is not rtsp")
}
if !strings.Contains(mediaUri, "@") && dev.GetDeviceParams().Username != "" {
//如果返回的rtsp里没有账号密码则自己拼接
mediaUri = strings.Replace(mediaUri, "//", fmt.Sprintf("//%s:%s@", dev.GetDeviceParams().Username, dev.GetDeviceParams().Password), 1)
}
if strings.Contains(mediaUri, "udp") {
mediaUri = strings.Replace(mediaUri, "udp", "rtsp", 1)
}
return mediaUri, nil
}
// 获取设备的账号密码
func getDeviceAuth(interfaceName string, ip string, config *AuthConfig) deviceAuth {
var auth deviceAuth
if a, ok := config.Interfaces[interfaceName]; ok {
auth = a
}
if a, ok := config.Devices[ip]; ok {
auth = a
}
return auth
}
func GetProfiles(dev *donvif.Device) ([]onvifTypes.Profile, error) {
result, err := dev.CallMethod(media.GetProfiles{})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var profiles media.GetProfilesResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&profiles)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return profiles.Profiles, nil
}
func getAuth(iface, ip string) *deviceAuth {
if auth, ok := authCfg.Devices[ip]; ok {
return &auth
}
if auth, ok := authCfg.Interfaces[iface]; ok {
return &auth
}
return nil
}

193
plugin/onvif/device_list.go Executable file
View File

@@ -0,0 +1,193 @@
package plugin_onvif
import (
"encoding/xml"
"fmt"
"net/url"
"strings"
"sync"
"github.com/IOTechSystems/onvif"
wsdiscovery "github.com/IOTechSystems/onvif/ws-discovery"
"m7s.live/v5/pkg/util"
)
type DeviceList struct {
Data *util.Collection[string, *InterfaceCollection]
plugin *OnvifPlugin
}
var deviceList DeviceList
func (d *DeviceList) discoveryDevice() {
for _, iface := range d.plugin.Interfaces {
if iface.InterfaceName == VIRTUAL_IFACE {
continue
}
devices, err := wsdiscovery.SendProbe(iface.InterfaceName, nil, []string{"dn:NetworkVideoTransmitter"}, map[string]string{"dn": "http://www.onvif.org/ver10/network/wsdl"})
if err != nil {
d.plugin.Warn("discover devices failed",
"interface", iface.InterfaceName,
"error", err.Error(),
)
continue
}
var ifaceDevices *InterfaceCollection
var ok bool
if ifaceDevices, ok = d.Data.Get(iface.InterfaceName); !ok {
ifaceDevices = &InterfaceCollection{
Collection: util.Collection[string, *DeviceStatus]{
Items: make([]*DeviceStatus, 0),
L: &sync.RWMutex{},
},
iface: iface.InterfaceName,
}
}
d.Data.Add(ifaceDevices)
for _, dev := range devices {
if strings.Contains(dev, "onvif") {
var envelope struct {
Body struct {
ProbeMatches struct {
ProbeMatch struct {
XAddrs string `xml:"XAddrs"`
} `xml:"ProbeMatch"`
} `xml:"ProbeMatches"`
} `xml:"Body"`
}
if err := xml.Unmarshal([]byte(dev), &envelope); err != nil {
d.plugin.Warn("parse device xml failed", "error", err.Error())
continue
}
xaddr := envelope.Body.ProbeMatches.ProbeMatch.XAddrs
if xaddr == "" {
continue
}
u, err := url.Parse(xaddr)
if err != nil {
d.plugin.Warn("parse xaddr failed", "error", err.Error())
continue
}
ipPort := strings.Split(u.Host, ":")
if _, ok := ifaceDevices.Get(u.Host); !ok {
auth := getAuth(iface.InterfaceName, ipPort[0])
if auth == nil {
continue
}
status, _, err := NewDeviceStatus(ipPort[0], auth.Username, auth.Password, ipPort[1], "/onvif/device_service", 0)
if err != nil {
d.plugin.Warn("create device failed", "error", err.Error())
continue
}
ifaceDevices.Add(status)
if d.plugin.AutoAdd {
status.AutoAdd()
}
}
}
}
}
}
func (d *DeviceList) AutoPullStream() {
d.Data.Range(func(ic *InterfaceCollection) bool {
ic.Range(func(status *DeviceStatus) bool {
if status.Stream == "" && status.MediaUrl != "" {
status.PullStream(ic.iface, status.Channel)
}
return true
})
return true
})
}
func (d *DeviceList) GetDeviceByStreamPath(streamPath string) *DeviceStatus {
if streamPath == "" {
return nil
}
var result *DeviceStatus
d.Data.Range(func(ic *InterfaceCollection) bool {
ic.Range(func(status *DeviceStatus) bool {
if status.Path == streamPath {
result = status
return false
}
return true
})
if result != nil {
return false
}
return true
})
return result
}
func (d *DeviceStatus) AutoAdd() error {
dev, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: fmt.Sprintf("%s:%s", d.IP, d.Port),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
profiles, err := GetProfiles(dev)
if err != nil {
return err
}
for i, p := range profiles {
uri, err := GetStreamUri(dev, p.Token)
if err != nil {
continue
}
d.MediaUrl = string(uri)
d.Channel = i
break
}
return nil
}
func (dl *DeviceList) AddDevice(param *DeviceParam) (*DeviceStatus, int, error) {
if param.Port == "" {
param.Port = "80"
}
if param.Path == "" {
param.Path = "/onvif/device_service"
}
if param.IFace == "" {
param.IFace = VIRTUAL_IFACE
}
xaddr := param.Ip + ":" + param.Port
devs, ok := dl.Data.Get(param.IFace)
if ok {
if dev, exist := devs.Get(xaddr); exist {
return dev, 0, nil
}
} else {
devs = &InterfaceCollection{iface: param.IFace}
}
ds, code, err := NewDeviceStatus(param.Ip, param.User, param.Passwd, param.Port, param.Path, param.Channel)
if err != nil {
return nil, code, err
}
if !devs.Set(ds) {
return nil, 0, fmt.Errorf("failed to add device %s", param.IFace)
}
dl.Data.Set(devs)
return ds, 0, nil
}
func (dl *DeviceList) DelDevice(param *DeviceParam) {
devs, ok := dl.Data.Get(param.IFace)
if !ok {
return
}
devs.RemoveByKey(param.Ip + ":" + param.Port)
}

100
plugin/onvif/dto.go Executable file
View File

@@ -0,0 +1,100 @@
package plugin_onvif
import (
"github.com/IOTechSystems/onvif/ptz"
"github.com/IOTechSystems/onvif/xsd"
"github.com/IOTechSystems/onvif/xsd/onvif"
)
type ImageSettings struct {
BacklightCompensation struct {
Mode string `json:"Mode"`
Level int `json:"Level"`
} `json:"BacklightCompensation"`
Brightness int `json:"Brightness"`
ColorSaturation int `json:"ColorSaturation"`
Contrast int `json:"Contrast"`
Exposure struct {
Mode string `json:"Mode"`
Priority string `json:"Priority"`
Window struct {
Bottom int `json:"Bottom"`
Top int `json:"Top"`
Right int `json:"Right"`
Left int `json:"Left"`
} `json:"Window"`
MinExposureTime int `json:"MinExposureTime"`
MaxExposureTime int `json:"MaxExposureTime"`
MinGain int `json:"MinGain"`
MaxGain int `json:"MaxGain"`
MinIris int `json:"MinIris"`
MaxIris int `json:"MaxIris"`
ExposureTime int `json:"ExposureTime"`
Gain int `json:"Gain"`
Iris int `json:"Iris"`
} `json:"Exposure"`
Focus struct {
AutoFocusMode string `json:"AutoFocusMode"`
DefaultSpeed int `json:"DefaultSpeed"`
NearLimit int `json:"NearLimit"`
FarLimit int `json:"FarLimit"`
Extension string `json:"Extension"`
} `json:"Focus"`
IrCutFilter string `json:"IrCutFilter"`
Sharpness int `json:"Sharpness"`
WideDynamicRange struct {
Mode string `json:"Mode"`
Level int `json:"Level"`
} `json:"WideDynamicRange"`
WhiteBalance struct {
Mode string `json:"Mode"`
CrGain int `json:"CrGain"`
CbGain int `json:"CbGain"`
Extension string `json:"Extension"`
} `json:"WhiteBalance"`
Extension struct {
ImageStabilization struct {
Mode string `json:"Mode"`
Level int `json:"Level"`
Extension string `json:"Extension"`
} `json:"ImageStabilization"`
Extension struct {
IrCutFilterAutoAdjustment struct {
BoundaryType string `json:"BoundaryType"`
BoundaryOffset int `json:"BoundaryOffset"`
ResponseTime string `json:"ResponseTime"`
Extension string `json:"Extension"`
} `json:"IrCutFilterAutoAdjustment"`
Extension struct {
ToneCompensation struct {
Mode string `json:"Mode"`
Level int `json:"Level"`
Extension string `json:"Extension"`
} `json:"ToneCompensation"`
Defogging struct {
Mode string `json:"Mode"`
Level int `json:"Level"`
Extension string `json:"Extension"`
} `json:"Defogging"`
NoiseReduction struct {
Level int `json:"Level"`
} `json:"NoiseReduction"`
Extension string `json:"Extension"`
} `json:"Extension"`
} `json:"Extension"`
} `json:"Extension"`
}
type ImageSettingReq struct {
ForcePersistence bool `json:"ForcePersistence"`
ImageSettings ImageSettings `json:"ImageSettings"`
}
type ptzMoveReq struct {
Move ptz.Vector `json:"Move"`
Speed ptz.Speed `json:"Speed"`
}
type ptzContinueMoveReq struct {
Velocity *onvif.PTZSpeed `json:"Velocity,omitempty"`
Timeout *xsd.Duration `json:"Timeout,omitempty"`
}

46
plugin/onvif/http.go Normal file
View File

@@ -0,0 +1,46 @@
package plugin_onvif
import (
"encoding/json"
"net/http"
)
func Success(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"data": data,
})
}
func BadRequest(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": 400,
"msg": err.Error(),
})
}
func InternalError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": 500,
"msg": err.Error(),
})
}
func NotFound(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": 404,
"msg": "not found",
})
}
func UnmarshalRequest(r *http.Request, v interface{}) error {
return json.NewDecoder(r.Body).Decode(v)
}

140
plugin/onvif/index.go Executable file
View File

@@ -0,0 +1,140 @@
package plugin_onvif
import (
"fmt"
"sync"
"time"
"m7s.live/v5/pkg/util"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/task"
)
const VIRTUAL_IFACE = "virtual"
var (
_ = m7s.InstallPlugin[OnvifPlugin](nil)
)
type OnvifPlugin struct {
m7s.Plugin
DiscoverInterval int `default:"3" desc:"设备发现间隔0表示不自动发现"`
AutoPull bool `default:"false" desc:"是否自动拉流"`
AutoAdd bool `default:"false" desc:"是否自动添加发现的设备"`
Interfaces []struct {
InterfaceName string `desc:"网卡名称"`
Username string `desc:"用户名"`
Password string `desc:"密码"`
} `desc:"网卡配置"`
Devices []struct {
IP string `desc:"设备IP"`
Username string `desc:"用户名"`
Password string `desc:"密码"`
} `desc:"设备配置"`
}
type InterfaceCollection struct {
util.Collection[string, *DeviceStatus]
iface string
}
func (c *InterfaceCollection) GetKey() string {
return c.iface
}
// OnvifTimerTask 定时任务结构体
type OnvifTimerTask struct {
task.TickTask
plugin *OnvifPlugin
}
// GetTickInterval 设置定时间隔
func (t *OnvifTimerTask) GetTickInterval() time.Duration {
return time.Duration(t.plugin.DiscoverInterval) * time.Second
}
// Tick 执行定时任务
func (t *OnvifTimerTask) Tick(any) {
deviceList.discoveryDevice()
if t.plugin.AutoPull {
deviceList.AutoPullStream()
}
}
//func (p *OnvifPlugin) OnEvent(event any) {
// switch e := event.(type) {
// case pkg.IStreamEvent:
// if e.Type() == pkg.SEclose {
// stream := e.Target().Path
// device := deviceList.GetDeviceByStreamPath(stream)
// if device != nil {
// device.Stream = ""
// }
// }
// }
//}
func (p *OnvifPlugin) OnInit() (err error) {
// 检查配置参数
if p.DiscoverInterval < 0 {
p.Error("invalid discover interval",
"interval", p.DiscoverInterval,
"valid_range", ">=0",
)
return fmt.Errorf("invalid discover interval: %d, valid range is >=0", p.DiscoverInterval)
}
// 初始化设备列表
deviceList.Data = &util.Collection[string, *InterfaceCollection]{
Items: make([]*InterfaceCollection, 0),
L: &sync.RWMutex{},
}
deviceList.plugin = p
virtualIface := &InterfaceCollection{
Collection: util.Collection[string, *DeviceStatus]{
Items: make([]*DeviceStatus, 0),
L: &sync.RWMutex{},
},
iface: VIRTUAL_IFACE,
}
deviceList.Data.Add(virtualIface)
preprocessAuth(p, authCfg)
// 如果设置了发现间隔,启动设备发现任务
if p.DiscoverInterval > 0 {
// 立即执行一次发现
deviceList.discoveryDevice()
if p.AutoPull {
deviceList.AutoPullStream()
}
// 添加定时任务
p.AddTask(&OnvifTimerTask{
plugin: p,
})
}
p.Info("onvif plugin initialized",
"discover_interval", p.DiscoverInterval,
"auto_pull", p.AutoPull,
"auto_add", p.AutoAdd,
)
return nil
}
func preprocessAuth(conf *OnvifPlugin, c *AuthConfig) {
for _, i := range conf.Interfaces {
c.Interfaces[i.InterfaceName] = deviceAuth{
Username: i.Username,
Password: i.Password,
}
}
for _, d := range conf.Devices {
c.Devices[d.IP] = deviceAuth{
Username: d.Username,
Password: d.Password,
}
}
}

View File

@@ -58,7 +58,7 @@ func GetVideoFrame(streamPath string, server *m7s.Server) (pkg.AnnexB, *pkg.AVTr
return pkg.AnnexB{}, nil, err
}
if track.ICodecCtx == nil {
return pkg.AnnexB{}, nil, fmt.Errorf("unsupported codec")
return pkg.AnnexB{}, nil, pkg.ErrUnsupportCodec
}
annexb.Mux(track.ICodecCtx, &reader.Value)

View File

@@ -94,7 +94,7 @@ func AddWatermark(imgData []byte, config WatermarkConfig) ([]byte, error) {
Text: config.Text,
Font: config.Font,
FontSize: config.FontSize,
Spacing: 10,
Spacing: config.FontSpacing,
RowSpacing: 10,
ColSpacing: 20,
Rows: 1,

View File

@@ -145,17 +145,8 @@ type Publisher struct {
OnGetPosition func() time.Time
PullProxy *PullProxy
dumpFile *os.File
}
type AliasStream struct {
*Publisher
AutoRemove bool
StreamPath string
Alias string
}
func (a *AliasStream) GetKey() string {
return a.Alias
MaxFPS float64
dropRate float64 // 丢帧率0-1之间
}
func (p *Publisher) SubscriberRange(yield func(sub *Subscriber) bool) {
@@ -189,17 +180,7 @@ func (p *Publisher) Start() (err error) {
}
s.Streams.Set(p)
p.Info("publish")
if pullProxy, ok := s.PullProxies.Find(func(pullProxy *PullProxy) bool {
return pullProxy.GetStreamPath() == p.StreamPath
}); ok {
p.PullProxy = pullProxy
if pullProxy.Status == PullProxyStatusOnline {
pullProxy.ChangeStatus(PullProxyStatusPulling)
if mp4Plugin, ok := s.Plugins.Get("MP4"); ok && pullProxy.FilePath != "" {
mp4Plugin.Record(p, pullProxy.Record, nil)
}
}
}
p.processPullProxyOnStart()
p.audioReady = util.NewPromiseWithTimeout(p, p.PublishTimeout)
if !p.PubAudio {
p.audioReady.Reject(ErrMuted)
@@ -215,20 +196,7 @@ func (p *Publisher) Start() (err error) {
}
s.Waiting.WakeUp(p.StreamPath, p)
for alias := range s.AliasStreams.Range {
if alias.StreamPath != p.StreamPath {
continue
}
if alias.Publisher == nil {
alias.Publisher = p
s.Waiting.WakeUp(alias.Alias, p)
} else if alias.Publisher.StreamPath != alias.StreamPath {
alias.Publisher.TransferSubscribers(p)
alias.Publisher = p
}
}
p.processAliasOnStart()
for plugin := range s.Plugins.Range {
plugin.OnPublish(p)
}
@@ -358,7 +326,7 @@ func (p *Publisher) writeAV(t *AVTrack, data IAVFrame) {
frame.CTS = data.GetCTS()
bytesIn := frame.Wraps[0].GetSize()
t.AddBytesIn(bytesIn)
frame.Timestamp = t.Tame(ts, t.FPS)
frame.Timestamp = t.Tame(ts, t.FPS, p.Scale)
if p.Enabled(p, task.TraceLevel) {
codec := t.FourCC().String()
data := frame.Wraps[0].String()
@@ -375,6 +343,40 @@ func (p *Publisher) trackAdded() error {
return nil
}
func (p *Publisher) dropFrame(t *AVTrack, idr *util.Ring[AVFrame]) (drop bool) {
// Frame dropping logic based on MaxFPS
if p.MaxFPS > 0 && float64(t.FPS) > p.MaxFPS {
dropRatio := float64(t.FPS)/p.MaxFPS - 1 // How many frames to drop for each frame kept
p.dropRate = dropRatio / (dropRatio + 1) // 计算丢帧率
if p.Scale >= 8 {
// Only keep I-frames when Scale >= 8
if !t.Value.IDR {
// Drop all P-frames
return true
}
// Drop I-frames based on FPS ratio
if dropRatio > 1 && t.Value.IDR && t.Value.Sequence%uint32(dropRatio+1) != 0 {
return true
}
} else {
// Normal frame dropping strategy
if !t.Value.IDR {
// Drop P-frames based on position in GOP and FPS ratio
if idr != nil {
posInGOP := int(t.Value.Sequence - idr.Value.Sequence)
gopThreshold := int(float64(p.GOP) / (dropRatio + 1))
if posInGOP > gopThreshold {
return true
}
}
}
}
} else {
p.dropRate = 0
}
return
}
func (p *Publisher) WriteVideo(data IAVFrame) (err error) {
defer func() {
if err != nil {
@@ -407,12 +409,20 @@ func (p *Publisher) WriteVideo(data IAVFrame) (err error) {
if t.ICodecCtx == nil {
return ErrUnsupportCodec
}
if codecCtxChanged {
p.Info("video codec changed", "width", t.ICodecCtx.(IVideoCodecCtx).Width(), "height", t.ICodecCtx.(IVideoCodecCtx).Height())
if codecCtxChanged && oldCodecCtx != nil {
oldWidth, oldHeight := oldCodecCtx.(IVideoCodecCtx).Width(), oldCodecCtx.(IVideoCodecCtx).Height()
newWidth, newHeight := t.ICodecCtx.(IVideoCodecCtx).Width(), t.ICodecCtx.(IVideoCodecCtx).Height()
if oldWidth != newWidth || oldHeight != newHeight {
p.Info("video resolution changed", "oldWidth", oldWidth, "oldHeight", oldHeight, "newWidth", newWidth, "newHeight", newHeight)
}
}
var idr *util.Ring[AVFrame]
if t.IDRingList.Len() > 0 {
idr = t.IDRingList.Back().Value
if p.dropFrame(t, idr) {
data.Recycle()
return nil
}
}
if t.Value.IDR {
if !t.IsReady() {
@@ -469,6 +479,7 @@ func (p *Publisher) WriteVideo(data IAVFrame) (err error) {
}
}
t.Step()
p.VideoTrack.speedControl(p.Speed, t.LastTs)
return
}
@@ -488,6 +499,17 @@ func (p *Publisher) WriteAudio(data IAVFrame) (err error) {
if !p.PubAudio {
return ErrMuted
}
// 根据丢帧率进行音频帧丢弃
if p.dropRate > 0 {
t := p.AudioTrack.AVTrack
if t != nil {
// 使用序列号进行平均丢帧
if t.Value.Sequence%uint32(1/p.dropRate) != 0 {
data.Recycle()
return nil
}
}
}
t := p.AudioTrack.AVTrack
if t == nil {
t = NewAVTrack(data, p.Logger.With("track", "audio"), &p.Publish, p.audioReady)
@@ -600,31 +622,7 @@ func (p *Publisher) Dispose() {
if p.Paused != nil {
p.Paused.Reject(p.StopReason())
}
var relatedAlias []*AliasStream
for alias := range s.AliasStreams.Range {
if alias.StreamPath == p.StreamPath {
if alias.AutoRemove {
defer s.AliasStreams.Remove(alias)
}
relatedAlias = append(relatedAlias, alias)
}
}
if p.Subscribers.Length > 0 {
SUBSCRIBER:
for subscriber := range p.SubscriberRange {
for _, alias := range relatedAlias {
if subscriber.StreamPath == alias.Alias {
if originStream, ok := s.Streams.Get(alias.Alias); ok {
originStream.AddSubscriber(subscriber)
continue SUBSCRIBER
}
}
}
s.Waiting.Wait(subscriber)
}
p.Subscribers.Clear()
}
p.processAliasOnDispose()
p.AudioTrack.Dispose()
p.VideoTrack.Dispose()
p.Info("unpublish", "remain", s.Streams.Length, "reason", p.StopReason())
@@ -632,19 +630,20 @@ func (p *Publisher) Dispose() {
p.dumpFile.Close()
}
p.State = PublisherStateDisposed
if p.PullProxy != nil && p.PullProxy.Status == PullProxyStatusPulling && s.PullProxies.Has(p.PullProxy.GetKey()) {
p.PullProxy.ChangeStatus(PullProxyStatusOnline)
}
p.processPullProxyOnDispose()
}
func (p *Publisher) TransferSubscribers(newPublisher *Publisher) {
p.Info("transfer subscribers", "newPublisher", newPublisher.ID, "newStreamPath", newPublisher.StreamPath)
var remain SubscriberCollection
for subscriber := range p.SubscriberRange {
if subscriber.Type != SubscribeTypeServer {
continue
remain.Add(subscriber)
} else {
newPublisher.AddSubscriber(subscriber)
}
newPublisher.AddSubscriber(subscriber)
p.Subscribers.Remove(subscriber)
}
p.Subscribers = remain
p.BufferTime = p.Plugin.GetCommonConf().Publish.BufferTime
p.AudioTrack.SetMinBuffer(p.BufferTime)
p.VideoTrack.SetMinBuffer(p.BufferTime)
@@ -693,12 +692,32 @@ func (p *Publisher) WaitTrack() (err error) {
return
}
func (p *Publisher) NoVideo() {
p.PubVideo = false
if p.videoReady != nil {
p.videoReady.Reject(ErrMuted)
}
}
func (p *Publisher) NoAudio() {
p.PubAudio = false
if p.audioReady != nil {
p.audioReady.Reject(ErrMuted)
}
}
func (p *Publisher) Pause() {
if p.Paused != nil {
return
}
p.Paused = util.NewPromise(p)
p.pauseTime = time.Now()
}
func (p *Publisher) Resume() {
if p.Paused == nil {
return
}
p.Paused.Resolve()
p.Paused = nil
p.VideoTrack.pausedTime += time.Since(p.pauseTime)

View File

@@ -1,13 +1,21 @@
package m7s
import (
"context"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/mcuadros/go-defaults"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
"m7s.live/v5/pb"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
@@ -35,6 +43,7 @@ type (
PullOnStart, Audio, StopOnIdle bool
config.Pull `gorm:"embedded;embeddedPrefix:pull_"`
config.Record `gorm:"embedded;embeddedPrefix:record_"`
RecordType string
ParentID uint
Type string
Status byte
@@ -129,6 +138,32 @@ func (d *PullProxyTask) Dispose() {
})
}
func (d *PullProxy) InitializeWithServer(s *Server) {
d.server = s
d.Logger = s.Logger.With("pullProxy", d.ID, "type", d.Type, "name", d.Name)
if d.Type == "" {
u, err := url.Parse(d.URL)
if err != nil {
d.Logger.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
d.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
d.Type = "hls"
case ".flv":
d.Type = "flv"
case ".mp4":
d.Type = "mp4"
}
}
}
}
func (d *PullProxyTask) Pull() {
var pubConf = d.Plugin.config.Publish
pubConf.PubAudio = d.PullProxy.Audio
@@ -178,3 +213,218 @@ func (d *TCPPullProxy) Tick(any) {
d.PullProxy.ChangeStatus(PullProxyStatusOnline)
}
}
func (p *Publisher) processPullProxyOnStart() {
s := p.Plugin.Server
if pullProxy, ok := s.PullProxies.Find(func(pullProxy *PullProxy) bool {
return pullProxy.GetStreamPath() == p.StreamPath
}); ok {
p.PullProxy = pullProxy
if pullProxy.Status == PullProxyStatusOnline {
pullProxy.ChangeStatus(PullProxyStatusPulling)
if mp4Plugin, ok := s.Plugins.Get("MP4"); ok && pullProxy.FilePath != "" {
mp4Plugin.Record(p, pullProxy.Record, nil)
}
}
}
}
func (p *Publisher) processPullProxyOnDispose() {
s := p.Plugin.Server
if p.PullProxy != nil && p.PullProxy.Status == PullProxyStatusPulling && s.PullProxies.Has(p.PullProxy.GetKey()) {
p.PullProxy.ChangeStatus(PullProxyStatusOnline)
}
}
func (s *Server) GetPullProxyList(ctx context.Context, req *emptypb.Empty) (res *pb.PullProxyListResponse, err error) {
res = &pb.PullProxyListResponse{}
s.PullProxies.Call(func() error {
for device := range s.PullProxies.Range {
res.Data = append(res.Data, &pb.PullProxyInfo{
Name: device.Name,
CreateTime: timestamppb.New(device.CreatedAt),
UpdateTime: timestamppb.New(device.UpdatedAt),
Type: device.Type,
PullURL: device.URL,
ParentID: uint32(device.ParentID),
Status: uint32(device.Status),
ID: uint32(device.ID),
PullOnStart: device.PullOnStart,
StopOnIdle: device.StopOnIdle,
Audio: device.Audio,
RecordPath: device.Record.FilePath,
RecordFragment: durationpb.New(device.Record.Fragment),
Description: device.Description,
Rtt: uint32(device.RTT.Milliseconds()),
StreamPath: device.GetStreamPath(),
})
}
return nil
})
return
}
func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *pb.SuccessResponse, err error) {
device := &PullProxy{
server: s,
Name: req.Name,
Type: req.Type,
ParentID: uint(req.ParentID),
PullOnStart: req.PullOnStart,
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Pull)
defaults.SetDefaults(&device.Record)
device.URL = req.PullURL
device.Audio = req.Audio
device.StopOnIdle = req.StopOnIdle
device.Record.FilePath = req.RecordPath
device.Record.Fragment = req.RecordFragment.AsDuration()
if s.DB == nil {
err = pkg.ErrNoDB
return
}
s.DB.Create(device)
if req.StreamPath == "" {
device.StreamPath = device.GetStreamPath()
}
s.PullProxies.Add(device)
res = &pb.SuccessResponse{}
return
}
func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
target := &PullProxy{
server: s,
}
err = s.DB.First(target, req.ID).Error
if err != nil {
return
}
target.Name = req.Name
target.URL = req.PullURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
}
target.PullOnStart = req.PullOnStart
target.StopOnIdle = req.StopOnIdle
target.Audio = req.Audio
target.Description = req.Description
target.Record.FilePath = req.RecordPath
target.Record.Fragment = req.RecordFragment.AsDuration()
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
target.StreamPath = req.StreamPath
s.DB.Save(target)
var needStopOld *PullProxy
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(req.ID)); ok {
if target.URL != device.URL || device.Audio != target.Audio || device.StreamPath != target.StreamPath || device.Record.FilePath != target.Record.FilePath || device.Record.Fragment != target.Record.Fragment {
device.Stop(task.ErrStopByUser)
needStopOld = device
return nil
}
if device.PullOnStart != target.PullOnStart && target.PullOnStart && device.Handler != nil && device.Status == PullProxyStatusOnline {
device.Handler.Pull()
}
device.Name = target.Name
device.PullOnStart = target.PullOnStart
device.StopOnIdle = target.StopOnIdle
device.Description = target.Description
}
return nil
})
if needStopOld != nil {
needStopOld.WaitStopped()
s.PullProxies.Add(target)
}
res = &pb.SuccessResponse{}
return
}
func (s *Server) RemovePullProxy(ctx context.Context, req *pb.RequestWithId) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
res = &pb.SuccessResponse{}
if req.Id > 0 {
tx := s.DB.Delete(&PullProxy{
ID: uint(req.Id),
})
err = tx.Error
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(req.Id)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
return
} else if req.StreamPath != "" {
var deviceList []PullProxy
s.DB.Find(&deviceList, "stream_path=?", req.StreamPath)
if len(deviceList) > 0 {
for _, device := range deviceList {
tx := s.DB.Delete(&PullProxy{}, device.ID)
err = tx.Error
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(device.ID)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
}
}
return
} else {
res.Message = "parameter wrong"
return
}
}

View File

@@ -2,9 +2,11 @@ package m7s
import (
"io"
"math"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -54,6 +56,9 @@ type (
Streams []RecordStream
File *os.File
MaxTS int64
seekChan chan time.Time
Type string
Loop int
}
wsReadCloser struct {
@@ -196,6 +201,28 @@ func (p *RecordFilePuller) GetPullJob() *PullJob {
return &p.PullJob
}
func (p *RecordFilePuller) queryRecordStreams(startTime, endTime time.Time) (err error) {
if p.PullJob.Plugin.DB == nil {
return pkg.ErrNoDB
}
queryRecord := RecordStream{
Mode: RecordModeAuto,
Type: p.Type,
}
tx := p.PullJob.Plugin.DB.Where(&queryRecord).Find(&p.Streams, "end_time>=? AND start_time<=? AND stream_path=?", startTime, endTime, p.PullJob.RemoteURL)
if tx.Error != nil {
return tx.Error
}
if len(p.Streams) == 0 {
return pkg.ErrNotFound
}
for _, stream := range p.Streams {
p.Debug("queryRecordStreams", "filePath", stream.FilePath)
}
p.MaxTS = endTime.Sub(startTime).Milliseconds()
return nil
}
func (p *RecordFilePuller) Start() (err error) {
p.SetRetry(0, 0)
if p.PullJob.Plugin.DB == nil {
@@ -208,26 +235,38 @@ func (p *RecordFilePuller) Start() (err error) {
if p.PullStartTime, p.PullEndTime, err = util.TimeRangeQueryParse(p.PullJob.Args); err != nil {
return
}
queryRecord := RecordStream{
Mode: RecordModeAuto,
p.seekChan = make(chan time.Time, 1)
loop := p.PullJob.Args.Get(util.LoopKey)
p.Loop, err = strconv.Atoi(loop)
if err != nil || p.Loop < 0 {
p.Loop = math.MaxInt32
}
tx := p.PullJob.Plugin.DB.Where(&queryRecord).Find(&p.Streams, "end_time>=? AND start_time<=? AND stream_path=?", p.PullStartTime, p.PullEndTime, p.PullJob.RemoteURL)
if tx.Error != nil {
return tx.Error
publisher := p.PullJob.Publisher
publisher.OnSeek = func(seekTime time.Time) {
// p.PullStartTime = seekTime
// p.SetRetry(1, 0)
// if util.UnixTimeReg.MatchString(p.PullJob.Args.Get(util.EndKey)) {
// p.PullJob.Args.Set(util.StartKey, strconv.FormatInt(seekTime.Unix(), 10))
// } else {
// p.PullJob.Args.Set(util.StartKey, seekTime.Local().Format(util.LocalTimeFormat))
// }
select {
case p.seekChan <- seekTime:
default:
}
}
p.MaxTS = p.PullEndTime.Sub(p.PullStartTime).Milliseconds()
return p.queryRecordStreams(p.PullStartTime, p.PullEndTime)
}
if len(p.Streams) == 0 {
return pkg.ErrNotFound
}
p.Info("vod", "streams", p.Streams)
return
func (p *RecordFilePuller) GetSeekChan() chan time.Time {
return p.seekChan
}
func (p *RecordFilePuller) Dispose() {
if p.File != nil {
p.File.Close()
}
close(p.seekChan)
}
func (w *wsReadCloser) Read(p []byte) (n int, err error) {
@@ -241,3 +280,19 @@ func (w *wsReadCloser) Read(p []byte) (n int, err error) {
func (w *wsReadCloser) Close() error {
return w.ws.Close()
}
func (p *RecordFilePuller) CheckSeek() (needSeek bool, err error) {
select {
case p.PullStartTime = <-p.seekChan:
if err = p.queryRecordStreams(p.PullStartTime, p.PullEndTime); err != nil {
return
}
if p.File != nil {
p.File.Close()
p.File = nil
}
needSeek = true
default:
}
return
}

View File

@@ -1,13 +1,20 @@
package m7s
import (
"context"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/mcuadros/go-defaults"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
"m7s.live/v5/pb"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
)
@@ -141,3 +148,210 @@ func (d *TCPPushProxy) Tick(any) {
d.PushProxy.ChangeStatus(PushProxyStatusOnline)
}
}
func (d *PushProxy) InitializeWithServer(s *Server) {
d.server = s
d.Logger = s.Logger.With("pushProxy", d.ID, "type", d.Type, "name", d.Name)
if d.Type == "" {
u, err := url.Parse(d.URL)
if err != nil {
d.Logger.Error("parse push url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
d.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
d.Type = "hls"
case ".flv":
d.Type = "flv"
case ".mp4":
d.Type = "mp4"
}
}
}
}
func (s *Server) GetPushProxyList(ctx context.Context, req *emptypb.Empty) (res *pb.PushProxyListResponse, err error) {
res = &pb.PushProxyListResponse{}
s.PushProxies.Call(func() error {
for device := range s.PushProxies.Range {
res.Data = append(res.Data, &pb.PushProxyInfo{
Name: device.Name,
CreateTime: timestamppb.New(device.CreatedAt),
UpdateTime: timestamppb.New(device.UpdatedAt),
Type: device.Type,
PushURL: device.URL,
ParentID: uint32(device.ParentID),
Status: uint32(device.Status),
ID: uint32(device.ID),
PushOnStart: device.PushOnStart,
Audio: device.Audio,
Description: device.Description,
Rtt: uint32(device.RTT.Milliseconds()),
StreamPath: device.GetStreamPath(),
})
}
return nil
})
return
}
func (s *Server) AddPushProxy(ctx context.Context, req *pb.PushProxyInfo) (res *pb.SuccessResponse, err error) {
device := &PushProxy{
server: s,
Name: req.Name,
Type: req.Type,
ParentID: uint(req.ParentID),
PushOnStart: req.PushOnStart,
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Push)
device.URL = req.PushURL
device.Audio = req.Audio
if s.DB == nil {
err = pkg.ErrNoDB
return
}
s.DB.Create(device)
s.PushProxies.Add(device)
res = &pb.SuccessResponse{}
return
}
func (s *Server) UpdatePushProxy(ctx context.Context, req *pb.PushProxyInfo) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
target := &PushProxy{
server: s,
}
err = s.DB.First(target, req.ID).Error
if err != nil {
return
}
target.Name = req.Name
target.URL = req.PushURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
}
target.PushOnStart = req.PushOnStart
target.Audio = req.Audio
target.Description = req.Description
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
target.StreamPath = req.StreamPath
s.DB.Save(target)
var needStopOld *PushProxy
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(req.ID)); ok {
if target.URL != device.URL || device.Audio != target.Audio || device.StreamPath != target.StreamPath {
device.Stop(task.ErrStopByUser)
needStopOld = device
return nil
}
if device.PushOnStart != target.PushOnStart && target.PushOnStart && device.Handler != nil && device.Status == PushProxyStatusOnline {
device.Handler.Push()
}
device.Name = target.Name
device.PushOnStart = target.PushOnStart
device.Description = target.Description
}
return nil
})
if needStopOld != nil {
needStopOld.WaitStopped()
s.PushProxies.Add(target)
}
res = &pb.SuccessResponse{}
return
}
func (s *Server) RemovePushProxy(ctx context.Context, req *pb.RequestWithId) (res *pb.SuccessResponse, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
res = &pb.SuccessResponse{}
if req.Id > 0 {
tx := s.DB.Delete(&PushProxy{
ID: uint(req.Id),
})
err = tx.Error
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(req.Id)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
return
} else if req.StreamPath != "" {
var deviceList []*PushProxy
s.DB.Find(&deviceList, "stream_path=?", req.StreamPath)
if len(deviceList) > 0 {
for _, device := range deviceList {
tx := s.DB.Delete(device)
err = tx.Error
s.PushProxies.Call(func() error {
if device, ok := s.PushProxies.Get(uint(device.ID)); ok {
device.Stop(task.ErrStopByUser)
}
return nil
})
}
}
return
} else {
res.Message = "parameter wrong"
return
}
}

View File

@@ -51,7 +51,7 @@ type (
}
RecordStream struct {
ID uint `gorm:"primarykey"`
StartTime, EndTime time.Time `gorm:"default:'1970-01-01 00:00:00'"`
StartTime, EndTime time.Time `gorm:"default:'1970-01-01T00:00:00Z'"`
EventId string `json:"eventId" desc:"事件编号" gorm:"type:varchar(255);comment:事件编号"`
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式event=事件录像模式;default:'auto'"`
EventName string `json:"eventName" desc:"事件名称" gorm:"type:varchar(255);comment:事件名称"`

248
server.go
View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os"
@@ -14,6 +15,8 @@ import (
"strings"
"time"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/shirou/gopsutil/v4/cpu"
"google.golang.org/protobuf/proto"
@@ -60,13 +63,19 @@ type (
PulseInterval time.Duration `default:"5s" desc:"心跳事件间隔"` //心跳事件间隔
DisableAll bool `default:"false" desc:"禁用所有插件"` //禁用所有插件
StreamAlias map[config.Regexp]string `desc:"流别名"`
Location map[config.Regexp]string `desc:"HTTP路由转发规则,key为正则表达式,value为目标地址"`
PullProxy []*PullProxy
EnableLogin bool `default:"false" desc:"启用登录机制"` //启用登录机制
Users []struct {
Username string `desc:"用户名"`
Password string `desc:"密码"`
Role string `default:"user" desc:"角色,可选值:admin,user"`
} `desc:"用户列表,仅在启用登录机制时生效"`
PushProxy []*PushProxy
Admin struct {
EnableLogin bool `default:"false" desc:"启用登录机制"` //启用登录机制
FilePath string `default:"admin.zip" desc:"管理员界面文件路径"`
HomePage string `default:"home" desc:"管理员界面首页"`
Users []struct {
Username string `desc:"用户名"`
Password string `desc:"密码"`
Role string `default:"user" desc:"角色,可选值:admin,user"`
} `desc:"用户列表,仅在启用登录机制时生效"`
} `desc:"管理员界面配置"`
}
WaitStream struct {
StreamPath string
@@ -262,6 +271,7 @@ func (s *Server) Start() (err error) {
"/api/stream/annexb/{streamPath...}": s.api_Stream_AnnexB_,
"/api/videotrack/sse/{streamPath...}": s.api_VideoTrack_SSE,
"/api/audiotrack/sse/{streamPath...}": s.api_AudioTrack_SSE,
"/annexb/{streamPath...}": s.annexB,
})
if s.config.DSN != "" {
@@ -271,14 +281,14 @@ func (s *Server) Start() (err error) {
s.Error("failed to connect database", "error", err, "dsn", s.config.DSN, "type", s.config.DBType)
return
}
// Auto-migrate the User model
if err = s.DB.AutoMigrate(&db.User{}); err != nil {
s.Error("failed to auto-migrate User model", "error", err)
// Auto-migrate models
if err = s.DB.AutoMigrate(&db.User{}, &PullProxy{}, &PushProxy{}, &StreamAliasDB{}); err != nil {
s.Error("failed to auto-migrate models", "error", err)
return
}
// Create users from configuration if EnableLogin is true
if s.ServerConfig.EnableLogin {
for _, userConfig := range s.ServerConfig.Users {
if s.ServerConfig.Admin.EnableLogin {
for _, userConfig := range s.ServerConfig.Admin.Users {
var user db.User
// Check if user exists
if err = s.DB.Where("username = ?", userConfig.Username).First(&user).Error; err != nil {
@@ -406,56 +416,123 @@ func (s *Server) Start() (err error) {
}
}
}
if s.DB != nil {
s.DB.AutoMigrate(&PullProxy{})
s.DB.AutoMigrate(&PushProxy{})
}
for _, d := range s.PullProxy {
if d.ID != 0 {
d.server = s
if d.Type == "" {
u, err := url.Parse(d.URL)
if err != nil {
s.Error("parse pull url failed", "error", err)
continue
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
d.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
d.Type = "hls"
case ".flv":
d.Type = "flv"
case ".mp4":
d.Type = "mp4"
}
}
}
if s.DB != nil {
s.DB.Save(d)
} else {
s.PullProxies.Add(d, s.Logger.With("pullProxy", d.ID, "type", d.Type, "name", d.Name))
}
}
}
if s.DB != nil {
var pullProxies []*PullProxy
s.DB.Find(&pullProxies)
for _, d := range pullProxies {
d.server = s
d.Logger = s.Logger.With("pullProxy", d.ID, "type", d.Type, "name", d.Name)
d.ChangeStatus(PullProxyStatusOffline)
s.PullProxies.Add(d)
}
}
s.initDB()
return nil
}, "serverStart")
return
}
func (s *Server) initPullProxies() {
// 1. First read all pull proxies from database
var pullProxies []*PullProxy
s.DB.Find(&pullProxies)
// Create a map for quick lookup of existing proxies
existingPullProxies := make(map[uint]*PullProxy)
for _, proxy := range pullProxies {
existingPullProxies[proxy.ID] = proxy
proxy.InitializeWithServer(s)
proxy.ChangeStatus(PullProxyStatusOffline)
}
// 2. Process and override with config data
for _, configProxy := range s.PullProxy {
if configProxy.ID != 0 {
configProxy.InitializeWithServer(s)
// Update or insert into database
s.DB.Save(configProxy)
// Override existing proxy or add to list
if existing, ok := existingPullProxies[configProxy.ID]; ok {
// Update existing proxy with config values
existing.URL = configProxy.URL
existing.Type = configProxy.Type
existing.Name = configProxy.Name
existing.PullOnStart = configProxy.PullOnStart
} else {
pullProxies = append(pullProxies, configProxy)
}
}
}
// 3. Finally add all proxies to collections
for _, proxy := range pullProxies {
s.PullProxies.Add(proxy)
}
}
func (s *Server) initPushProxies() {
// 1. Read all push proxies from database
var pushProxies []*PushProxy
s.DB.Find(&pushProxies)
// Create a map for quick lookup of existing proxies
existingPushProxies := make(map[uint]*PushProxy)
for _, proxy := range pushProxies {
existingPushProxies[proxy.ID] = proxy
proxy.InitializeWithServer(s)
proxy.ChangeStatus(PushProxyStatusOffline)
}
// 2. Process and override with config data
for _, configProxy := range s.PushProxy {
if configProxy.ID != 0 {
configProxy.InitializeWithServer(s)
// Update or insert into database
s.DB.Save(configProxy)
// Override existing proxy or add to list
if existing, ok := existingPushProxies[configProxy.ID]; ok {
// Update existing proxy with config values
existing.URL = configProxy.URL
existing.Type = configProxy.Type
existing.Name = configProxy.Name
existing.PushOnStart = configProxy.PushOnStart
existing.Audio = configProxy.Audio
} else {
pushProxies = append(pushProxies, configProxy)
}
}
}
// 3. Finally add all proxies to collections
for _, proxy := range pushProxies {
s.PushProxies.Add(proxy)
}
}
func (s *Server) initPullProxiesWithoutDB() {
// Process config proxies without database
for _, proxy := range s.PullProxy {
if proxy.ID != 0 {
proxy.InitializeWithServer(s)
s.PullProxies.Add(proxy, proxy.Logger)
}
}
}
func (s *Server) initPushProxiesWithoutDB() {
// Process config proxies without database
for _, proxy := range s.PushProxy {
if proxy.ID != 0 {
proxy.InitializeWithServer(s)
s.PushProxies.Add(proxy, proxy.Logger)
}
}
}
func (s *Server) initDB() {
if s.DB != nil {
s.initPullProxies()
s.initPushProxies()
s.initStreamAlias()
} else {
s.initPullProxiesWithoutDB()
s.initPushProxiesWithoutDB()
}
}
func (c *CheckSubWaitTimeout) GetTickInterval() time.Duration {
return c.s.PulseInterval
}
@@ -504,6 +581,19 @@ func (s *Server) OnSubscribe(streamPath string, args url.Values) {
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check for location-based forwarding first
if s.Location != nil {
for pattern, target := range s.Location {
if pattern.MatchString(r.URL.Path) {
// Rewrite the URL path and handle locally
r.URL.Path = pattern.ReplaceAllString(r.URL.Path, target)
// Forward to local handler
s.config.HTTP.GetHandler().ServeHTTP(w, r)
return
}
}
}
// 检查 admin.zip 是否需要重新加载
now := time.Now()
if now.Sub(lastCheckTime) > checkInterval {
@@ -533,7 +623,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ValidateToken implements auth.TokenValidator
func (s *Server) ValidateToken(tokenString string) (*auth.JWTClaims, error) {
if !s.ServerConfig.EnableLogin {
if !s.ServerConfig.Admin.EnableLogin {
return &auth.JWTClaims{Username: "anonymous"}, nil
}
return auth.ValidateJWT(tokenString)
@@ -542,7 +632,7 @@ func (s *Server) ValidateToken(tokenString string) (*auth.JWTClaims, error) {
// Login implements the Login RPC method
func (s *Server) Login(ctx context.Context, req *pb.LoginRequest) (res *pb.LoginResponse, err error) {
res = &pb.LoginResponse{}
if !s.ServerConfig.EnableLogin {
if !s.ServerConfig.Admin.EnableLogin {
res.Data = &pb.LoginSuccess{
Token: "monibuca",
UserInfo: &pb.UserInfo{
@@ -595,7 +685,7 @@ func (s *Server) Logout(ctx context.Context, req *pb.LogoutRequest) (res *pb.Log
// GetUserInfo implements the GetUserInfo RPC method
func (s *Server) GetUserInfo(ctx context.Context, req *pb.UserInfoRequest) (res *pb.UserInfoResponse, err error) {
if !s.ServerConfig.EnableLogin {
if !s.ServerConfig.Admin.EnableLogin {
res = &pb.UserInfoResponse{
Code: 0,
Message: "success",
@@ -634,7 +724,7 @@ func (s *Server) GetUserInfo(ctx context.Context, req *pb.UserInfoRequest) (res
// AuthInterceptor creates a new unary interceptor for authentication
func (s *Server) AuthInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !s.ServerConfig.EnableLogin {
if !s.ServerConfig.Admin.EnableLogin {
return handler(ctx, req)
}
@@ -677,3 +767,41 @@ func (s *Server) AuthInterceptor() grpc.UnaryServerInterceptor {
return handler(newCtx, req)
}
}
func (s *Server) annexB(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
if r.URL.RawQuery != "" {
streamPath += "?" + r.URL.RawQuery
}
var conf = s.config.Subscribe
conf.SubType = SubscribeTypeServer
conf.SubAudio = false
suber, err := s.SubscribeWithConfig(r.Context(), streamPath, conf)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var conn net.Conn
conn, err = suber.CheckWebSocket(w, r)
if err != nil {
return
}
if conn == nil {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Transfer-Encoding", "identity")
w.WriteHeader(http.StatusOK)
}
PlayBlock(suber, func(frame *pkg.AVFrame) (err error) {
return nil
}, func(frame *pkg.AnnexB) (err error) {
if conn != nil {
return wsutil.WriteServerMessage(conn, ws.OpBinary, util.ConcatBuffers(frame.Memory.Buffers))
}
var buf net.Buffers
buf = append(buf, frame.Memory.Buffers...)
buf.WriteTo(w)
return nil
})
}

View File

@@ -105,37 +105,18 @@ func (s *Subscriber) Start() (err error) {
server := s.Plugin.Server
server.Subscribers.Add(s)
s.Info("subscribe")
if alias, ok := server.AliasStreams.Get(s.StreamPath); ok {
if alias.Publisher != nil {
alias.Publisher.AddSubscriber(s)
return
} else {
server.OnSubscribe(alias.StreamPath, s.Args)
}
} else {
for reg, alias := range server.StreamAlias {
if streamPath := reg.Replace(s.StreamPath, alias); streamPath != "" {
server.AliasStreams.Set(&AliasStream{
StreamPath: streamPath,
Alias: s.StreamPath,
})
if publisher, ok := server.Streams.Get(streamPath); ok {
publisher.AddSubscriber(s)
return
} else {
server.OnSubscribe(streamPath, s.Args)
}
break
}
}
hasInvited, done := s.processAliasOnStart()
if done {
return
}
if publisher, ok := server.Streams.Get(s.StreamPath); ok {
publisher.AddSubscriber(s)
return
} else {
server.Waiting.Wait(s)
server.OnSubscribe(s.StreamPath, s.Args)
if !hasInvited {
server.OnSubscribe(s.StreamPath, s.Args)
}
}
return
}

175
website/index.html Normal file
View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monibuca - 高性能流媒体服务器框架</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<div class="container">
<div class="logo">
<img src="logo.svg" alt="Monibuca Logo">
<span>Monibuca</span>
</div>
<div class="nav-links">
<a href="#features">特性</a>
<a href="#quickstart">快速开始</a>
<a href="#plugins">插件</a>
<a href="#docs">文档</a>
<a href="https://github.com/langhuihui/monibuca" class="github-link" target="_blank" rel="noopener">GitHub</a>
</div>
</div>
</nav>
</header>
<main>
<section class="hero">
<div class="container">
<h1>下一代流媒体服务器框架</h1>
<p class="subtitle">高性能、可扩展、插件化的纯 Go 流媒体服务器开发框架</p>
<div class="cta-buttons">
<a href="#quickstart" class="primary-button">快速开始</a>
<a href="https://docs.m7s.live" class="secondary-button" target="_blank" rel="noopener">查看文档</a>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="icon">🚀</div>
<h3>高性能</h3>
<p>采用纯 Go 开发,充分利用 Go 的并发特性</p>
</div>
<div class="feature-card">
<div class="icon">🔌</div>
<h3>插件化</h3>
<p>核心功能都以插件形式提供,可按需加载</p>
</div>
<div class="feature-card">
<div class="icon">🛠</div>
<h3>可扩展</h3>
<p>支持自定义插件开发,灵活扩展功能</p>
</div>
<div class="feature-card">
<div class="icon">📽</div>
<h3>多协议</h3>
<p>支持 RTMP、HTTP-FLV、HLS、WebRTC 等</p>
</div>
</div>
</div>
</section>
<section id="features" class="features">
<div class="container">
<h2>核心特性</h2>
<div class="features-list">
<div class="feature">
<h3>🎯 低延迟</h3>
<p>针对实时性场景优化,提供极低的传输延迟</p>
</div>
<div class="feature">
<h3>📊 实时监控</h3>
<p>支持 Prometheus 监控集成,实时掌握系统状态</p>
</div>
<div class="feature">
<h3>🔄 集群支持</h3>
<p>支持分布式部署,轻松扩展系统规模</p>
</div>
</div>
</div>
</section>
<section id="quickstart" class="quickstart">
<div class="container">
<h2>快速开始</h2>
<div class="code-block">
<div class="code-header">
<span>安装</span>
<button class="copy-button" data-target="install-code">复制</button>
</div>
<pre><code id="install-code">mkdir my-m7s-server && cd my-m7s-server
go mod init my-m7s-server</code></pre>
</div>
<div class="code-block">
<div class="code-header">
<span>创建主程序</span>
<button class="copy-button" data-target="main-code">复制</button>
</div>
<pre><code id="main-code">package main
import (
"context"
"m7s.live/v5"
_ "m7s.live/v5/plugin/rtmp"
_ "m7s.live/v5/plugin/flv"
_ "m7s.live/v5/plugin/hls"
)
func main() {
m7s.Run(context.Background(), "config.yaml")
}</code></pre>
</div>
</div>
</section>
<section id="plugins" class="plugins">
<div class="container">
<h2>官方插件</h2>
<div class="plugins-grid">
<div class="plugin-card">
<h3>RTMP</h3>
<p>支持 RTMP 协议推拉流</p>
</div>
<div class="plugin-card">
<h3>HTTP-FLV</h3>
<p>支持 HTTP-FLV 协议直播</p>
</div>
<div class="plugin-card">
<h3>HLS</h3>
<p>支持 HLS 协议直播点播</p>
</div>
<div class="plugin-card">
<h3>WebRTC</h3>
<p>支持 WebRTC 协议互动直播</p>
</div>
<div class="plugin-card">
<h3>GB28181</h3>
<p>支持国标协议设备接入</p>
</div>
<div class="plugin-card">
<h3>SRT</h3>
<p>支持 SRT 协议传输</p>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h4>资源</h4>
<a href="https://docs.m7s.live">文档</a>
<a href="https://pkg.go.dev/m7s.live/v5">API 参考</a>
<a href="https://github.com/langhuihui/monibuca/tree/main/example">示例代码</a>
</div>
<div class="footer-section">
<h4>社区</h4>
<a href="https://github.com/langhuihui/monibuca">GitHub</a>
<a href="https://github.com/langhuihui/monibuca/issues">问题反馈</a>
</div>
<div class="footer-section">
<h4>关于</h4>
<p>© 2024 Monibuca. 采用 AGPL 许可证.</p>
</div>
</div>
</div>
</footer>
<script src="main.js"></script>
</body>
</html>

93
website/main.js Normal file
View File

@@ -0,0 +1,93 @@
// Copy button functionality
document.querySelectorAll('.copy-button').forEach(button => {
button.addEventListener('click', () => {
const codeId = button.getAttribute('data-target');
const codeElement = document.getElementById(codeId);
const text = codeElement.textContent;
navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;
button.textContent = '已复制!';
button.style.background = 'rgba(0,255,0,0.2)';
setTimeout(() => {
button.textContent = originalText;
button.style.background = 'transparent';
}, 2000);
});
});
});
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Header scroll effect
const header = document.querySelector('header');
let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => {
if (window.scrollY > lastScrollY) {
header.style.transform = 'translateY(-100%)';
} else {
header.style.transform = 'translateY(0)';
}
lastScrollY = window.scrollY;
});
// Add transition to header
header.style.transition = 'transform 0.3s ease-in-out';
// Mobile menu functionality (if needed in the future)
// const mobileMenuButton = document.querySelector('.mobile-menu-button');
// const navLinks = document.querySelector('.nav-links');
// if (mobileMenuButton) {
// mobileMenuButton.addEventListener('click', () => {
// navLinks.classList.toggle('active');
// });
// }
// Intersection Observer for fade-in animations
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe all sections
document.querySelectorAll('section').forEach(section => {
section.style.opacity = '0';
section.style.transform = 'translateY(20px)';
section.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out';
observer.observe(section);
});
// Add fade-in class for animation
const style = document.createElement('style');
style.textContent = `
.fade-in {
opacity: 1 !important;
transform: translateY(0) !important;
}
`;
document.head.appendChild(style);

419
website/style.css Normal file
View File

@@ -0,0 +1,419 @@
:root {
--primary-color: #646cff;
--primary-color-dark: #535bf2;
--text-color: #213547;
--text-color-light: #666;
--background-color: #242424;
--text-color-dark: rgba(255, 255, 255, 0.87);
--text-color-dark-2: rgba(255, 255, 255, 0.6);
--border-color: #eee;
--code-background: #1a1a1a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: var(--text-color-dark);
line-height: 1.6;
background: var(--background-color);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
/* Header & Navigation */
header {
background: rgba(36, 36, 36, 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
}
nav {
height: 64px;
display: flex;
align-items: center;
}
nav .container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
}
.logo img {
height: 32px;
}
.logo span {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-color);
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-links a {
text-decoration: none;
color: var(--text-color-dark-2);
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--text-color-dark);
}
.github-link {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Hero Section */
.hero {
padding: 120px 0 80px;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 50% 0%, rgba(100, 108, 255, 0.15), rgba(36, 36, 36, 0) 50%),
radial-gradient(circle at 0% 100%, rgba(255, 182, 255, 0.1), rgba(36, 36, 36, 0) 50%),
radial-gradient(circle at 100% 100%, rgba(100, 108, 255, 0.1), rgba(36, 36, 36, 0) 50%);
z-index: -1;
}
.hero h1 {
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 1rem;
background: linear-gradient(120deg, #bd34fe 30%, #47caff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero .subtitle {
font-size: 1.5rem;
color: var(--text-color-dark-2);
margin-bottom: 2rem;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 4rem;
}
.primary-button, .secondary-button {
padding: 0.75rem 2rem;
border-radius: 20px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
border: 1px solid transparent;
}
.primary-button {
background: linear-gradient(to right, #bd34fe 30%, #47caff);
color: white;
box-shadow: 0 2px 12px rgba(189, 52, 254, 0.3);
}
.primary-button:hover {
background: linear-gradient(to right, #a925e5 30%, #38b8eb);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(189, 52, 254, 0.4);
}
.secondary-button {
background: rgba(255, 255, 255, 0.1);
color: var(--text-color-dark);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.secondary-button:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
/* Features Grid */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
.feature-card {
background: var(--code-background);
padding: 2rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.feature-card:hover {
transform: translateY(-5px);
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
.feature-card .icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.feature-card h3 {
margin-bottom: 0.5rem;
color: #bd34fe;
}
.feature-card p {
color: var(--text-color-dark-2);
}
/* Features Section */
.features {
padding: 80px 0;
background: var(--background-color);
position: relative;
}
.features::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 0% 0%, rgba(189, 52, 254, 0.15), rgba(36, 36, 36, 0) 50%),
radial-gradient(circle at 100% 0%, rgba(71, 202, 255, 0.15), rgba(36, 36, 36, 0) 50%);
z-index: -1;
}
.features h2 {
color: var(--text-color-dark);
}
.features-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.feature {
background: var(--code-background);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.feature h3 {
color: #47caff;
}
.feature p {
color: var(--text-color-dark-2);
}
/* Quickstart Section */
.quickstart {
padding: 80px 0;
}
.quickstart h2 {
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
}
.code-block {
background: var(--code-background);
border-radius: 8px;
margin-bottom: 2rem;
overflow: hidden;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(255,255,255,0.1);
}
.code-header span {
color: #fff;
}
.copy-button {
background: transparent;
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
padding: 0.25rem 0.75rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.copy-button:hover {
background: rgba(255,255,255,0.1);
}
pre {
margin: 0;
padding: 1.5rem;
}
code {
color: #fff;
font-family: 'Fira Code', monospace;
font-size: 0.9rem;
}
/* Plugins Section */
.plugins {
padding: 80px 0;
background: var(--background-color);
position: relative;
}
.plugins::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 100% 50%, rgba(71, 202, 255, 0.15), rgba(36, 36, 36, 0) 50%),
radial-gradient(circle at 0% 50%, rgba(189, 52, 254, 0.15), rgba(36, 36, 36, 0) 50%);
z-index: -1;
}
.plugins h2 {
color: var(--text-color-dark);
}
.plugins-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.plugin-card {
background: var(--code-background);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.plugin-card:hover {
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
.plugin-card h3 {
background: linear-gradient(120deg, #bd34fe 30%, #47caff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.plugin-card p {
color: var(--text-color-dark-2);
}
/* Footer */
footer {
background: var(--code-background);
color: var(--text-color-dark);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
footer::before {
background:
radial-gradient(circle at 0% 0%, rgba(189, 52, 254, 0.15), rgba(26, 26, 26, 0) 50%),
radial-gradient(circle at 100% 100%, rgba(71, 202, 255, 0.15), rgba(26, 26, 26, 0) 50%);
}
.footer-content {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 3rem;
}
.footer-section h4 {
color: #bd34fe;
}
.footer-section a {
color: var(--text-color-dark-2);
}
.footer-section a:hover {
color: var(--text-color-dark);
}
/* Responsive Design */
@media (max-width: 768px) {
.nav-links {
display: none;
}
.hero h1 {
font-size: 2.5rem;
}
.hero .subtitle {
font-size: 1.2rem;
}
.features-grid,
.plugins-grid {
grid-template-columns: 1fr;
}
.cta-buttons {
flex-direction: column;
}
.footer-content {
grid-template-columns: 1fr;
text-align: center;
}
}