Compare commits

..

11 Commits

Author SHA1 Message Date
langhuihui
01fa1f3ed8 gifix: replay cap script 2025-06-17 23:51:19 +08:00
langhuihui
830da3aaab fix: mp4 demuxer 2025-06-17 20:22:51 +08:00
langhuihui
5a04dc814d fix: event record check 2025-06-17 19:32:53 +08:00
langhuihui
af5d2bc1f2 fix: set record type 2025-06-17 18:34:10 +08:00
langhuihui
a3e0c1864e feat: add ping pong to batchv2 2025-06-17 14:03:37 +08:00
langhuihui
33d385d2bf fix: record bug 2025-06-17 11:36:32 +08:00
langhuihui
29c47a8d08 fix: hls demo page 2025-06-17 11:26:11 +08:00
langhuihui
5bf5e7bb20 feat: mp4 conert to ts format 2025-06-17 11:09:35 +08:00
langhuihui
4b74ea5841 doc: auth 2025-06-17 09:41:36 +08:00
langhuihui
43710fb017 fix: record 2025-06-16 22:41:55 +08:00
langhuihui
962dda8d08 refactor: mp4 and record system 2025-06-16 20:28:49 +08:00
46 changed files with 2826 additions and 2632 deletions

90
api.go
View File

@@ -96,22 +96,14 @@ func (s *Server) api_Stream_AnnexB_(rw http.ResponseWriter, r *http.Request) {
return
}
defer reader.StopRead()
if reader.Value.Raw == nil {
if err = reader.Value.Demux(publisher.VideoTrack.ICodecCtx); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
var annexb pkg.AnnexB
var t pkg.AVTrack
t.ICodecCtx, t.SequenceFrame, err = annexb.ConvertCtx(publisher.VideoTrack.ICodecCtx)
if t.ICodecCtx == nil {
http.Error(rw, "unsupported codec", http.StatusInternalServerError)
var annexb *pkg.AnnexB
var converter = pkg.NewAVFrameConvert[*pkg.AnnexB](publisher.VideoTrack.AVTrack, nil)
annexb, err = converter.ConvertFromAVFrame(&reader.Value)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
annexb.Mux(t.ICodecCtx, &reader.Value)
_, err = annexb.WriteTo(rw)
annexb.WriteTo(rw)
}
func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err error) {
@@ -736,7 +728,63 @@ func (s *Server) GetConfig(_ context.Context, req *pb.GetConfigRequest) (res *pb
return
}
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.ResponseList, err error) {
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
if req.PageSize == 0 {
req.PageSize = 10
}
if req.PageNum == 0 {
req.PageNum = 1
}
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
var totalCount int64 //总条数
var result []*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)
}
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
}
resp = &pb.RecordResponseList{
Total: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
}
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,
})
}
return
}
func (s *Server) GetEventRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.EventRecordResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
@@ -751,15 +799,12 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
var totalCount int64 //总条数
var result []*EventRecordStream
query := s.DB.Model(&RecordStream{})
query := s.DB.Model(&EventRecordStream{})
if strings.Contains(req.StreamPath, "*") {
query = query.Where("stream_path like ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
} else if req.StreamPath != "" {
query = query.Where("stream_path = ?", req.StreamPath)
}
if req.Mode != "" {
query = query.Where("mode = ?", req.Mode)
}
if req.Type != "" {
query = query.Where("type = ?", req.Type)
}
@@ -781,21 +826,22 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
if err != nil {
return
}
resp = &pb.ResponseList{
resp = &pb.EventRecordResponseList{
Total: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
}
for _, recordFile := range result {
resp.Data = append(resp.Data, &pb.RecordFile{
resp.Data = append(resp.Data, &pb.EventRecordFile{
Id: uint32(recordFile.ID),
StartTime: timestamppb.New(recordFile.StartTime),
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
EventLevel: recordFile.EventLevel,
EventDesc: recordFile.EventDesc,
EventId: recordFile.EventId,
EventName: recordFile.EventName,
EventDesc: recordFile.EventDesc,
})
}
return

279
doc/arch/auth.md Normal file
View File

@@ -0,0 +1,279 @@
# Stream Authentication Mechanism
Monibuca V5 provides a comprehensive stream authentication mechanism to control access permissions for publishing and subscribing to streams. The authentication mechanism supports multiple methods, including key-based signature authentication and custom authentication handlers.
## Authentication Principles
### 1. Authentication Flow Sequence Diagrams
#### Publishing Authentication Sequence Diagram
```mermaid
sequenceDiagram
participant Client as Publishing Client
participant Plugin as Plugin
participant AuthHandler as Auth Handler
participant Server as Server
Client->>Plugin: Publishing Request (streamPath, args)
Plugin->>Plugin: Check EnableAuth && Type == PublishTypeServer
alt Authentication Enabled
Plugin->>Plugin: Look for custom auth handler
alt Custom Handler Exists
Plugin->>AuthHandler: onAuthPub(publisher)
AuthHandler->>AuthHandler: Execute custom auth logic
AuthHandler-->>Plugin: Auth result
else Use Key-based Auth
Plugin->>Plugin: Check if conf.Key exists
alt Key Configured
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: Validate timestamp
Plugin->>Plugin: Validate secret length
Plugin->>Plugin: Calculate MD5 signature
Plugin->>Plugin: Compare signatures
Plugin-->>Plugin: Auth result
end
end
alt Auth Failed
Plugin-->>Client: Auth failed, reject publishing
else Auth Success
Plugin->>Server: Create Publisher and add to stream management
Server-->>Plugin: Publishing successful
Plugin-->>Client: Publishing established successfully
end
else Auth Disabled
Plugin->>Server: Create Publisher directly
Server-->>Plugin: Publishing successful
Plugin-->>Client: Publishing established successfully
end
```
#### Subscribing Authentication Sequence Diagram
```mermaid
sequenceDiagram
participant Client as Subscribing Client
participant Plugin as Plugin
participant AuthHandler as Auth Handler
participant Server as Server
Client->>Plugin: Subscribing Request (streamPath, args)
Plugin->>Plugin: Check EnableAuth && Type == SubscribeTypeServer
alt Authentication Enabled
Plugin->>Plugin: Look for custom auth handler
alt Custom Handler Exists
Plugin->>AuthHandler: onAuthSub(subscriber)
AuthHandler->>AuthHandler: Execute custom auth logic
AuthHandler-->>Plugin: Auth result
else Use Key-based Auth
Plugin->>Plugin: Check if conf.Key exists
alt Key Configured
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: Validate timestamp
Plugin->>Plugin: Validate secret length
Plugin->>Plugin: Calculate MD5 signature
Plugin->>Plugin: Compare signatures
Plugin-->>Plugin: Auth result
end
end
alt Auth Failed
Plugin-->>Client: Auth failed, reject subscribing
else Auth Success
Plugin->>Server: Create Subscriber and wait for Publisher
Server->>Server: Wait for stream publishing and track ready
Server-->>Plugin: Subscribing ready
Plugin-->>Client: Start streaming data transmission
end
else Auth Disabled
Plugin->>Server: Create Subscriber directly
Server-->>Plugin: Subscribing successful
Plugin-->>Client: Start streaming data transmission
end
```
### 2. Authentication Trigger Points
Authentication is triggered in the following two scenarios:
- **Publishing Authentication**: Triggered when there's a publishing request in the `PublishWithConfig` method
- **Subscribing Authentication**: Triggered when there's a subscribing request in the `SubscribeWithConfig` method
### 3. Authentication Condition Checks
Authentication is only executed when the following conditions are met simultaneously:
```go
if p.config.EnableAuth && publisher.Type == PublishTypeServer
```
- `EnableAuth`: Authentication is enabled in the plugin configuration
- `Type == PublishTypeServer/SubscribeTypeServer`: Only authenticate server-type publishing/subscribing
### 4. Authentication Method Priority
The system executes authentication in the following priority order:
1. **Custom Authentication Handler** (Highest priority)
2. **Key-based Signature Authentication**
3. **No Authentication** (Default pass)
## Custom Authentication Handlers
### Publishing Authentication Handler
```go
onAuthPub := p.Meta.OnAuthPub
if onAuthPub == nil {
onAuthPub = p.Server.Meta.OnAuthPub
}
if onAuthPub != nil {
if err = onAuthPub(publisher).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
Authentication handler lookup order:
1. Plugin-level authentication handler `p.Meta.OnAuthPub`
2. Server-level authentication handler `p.Server.Meta.OnAuthPub`
### Subscribing Authentication Handler
```go
onAuthSub := p.Meta.OnAuthSub
if onAuthSub == nil {
onAuthSub = p.Server.Meta.OnAuthSub
}
if onAuthSub != nil {
if err = onAuthSub(subscriber).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
## Key-based Signature Authentication
When there's no custom authentication handler, if a Key is configured, the system will use MD5-based signature authentication mechanism.
### Authentication Algorithm
```go
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
// 1. Validate expiration time
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
return fmt.Errorf("auth failed expired")
}
// 2. Validate secret length
if len(secret) != 32 {
return fmt.Errorf("auth failed secret length must be 32")
}
// 3. Calculate the true secret
trueSecret := md5.Sum([]byte(key + streamPath + expire))
// 4. Compare secrets
if secret == hex.EncodeToString(trueSecret[:]) {
return nil
}
return fmt.Errorf("auth failed invalid secret")
}
```
### Signature Calculation Steps
1. **Construct signature string**: `key + streamPath + expire`
2. **MD5 encryption**: Perform MD5 hash on the signature string
3. **Hexadecimal encoding**: Convert MD5 result to 32-character hexadecimal string
4. **Verify signature**: Compare calculation result with client-provided secret
### Parameter Description
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| key | string | Secret key set in configuration file | "mySecretKey" |
| streamPath | string | Stream path | "live/test" |
| expire | string | Expiration timestamp (hexadecimal) | "64a1b2c3" |
| secret | string | Client-calculated signature (32-char hex) | "5d41402abc4b2a76b9719d911017c592" |
### Timestamp Handling
- Expiration time uses hexadecimal Unix timestamp
- System validates if current time exceeds expiration time
- Timestamp parsing failure or expiration will cause authentication failure
## API Key Generation
The system also provides API interfaces for key generation, supporting authentication needs for admin dashboard:
```go
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// JWT Token validation
authHeader := r.Header.Get("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
_, err := p.Server.ValidateToken(tokenString)
// Generate publishing or subscribing key
streamPath := r.PathValue("streamPath")
t := r.PathValue("type")
expire := r.URL.Query().Get("expire")
if t == "publish" {
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
} else if t == "subscribe" {
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
}
}))
```
## Configuration Examples
### Enable Authentication
```yaml
# Plugin configuration
rtmp:
enableAuth: true
publish:
key: "your-publish-key"
subscribe:
key: "your-subscribe-key"
```
### Publishing URL Example
```
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
```
### Subscribing URL Example
```
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
```
## Security Considerations
1. **Key Protection**: Keys in configuration files should be properly secured to prevent leakage
2. **Time Window**: Set reasonable expiration times to balance security and usability
3. **HTTPS Transport**: Use HTTPS for transmitting authentication parameters in production
4. **Logging**: Authentication failures are logged as warnings for security auditing
## Error Handling
Common causes of authentication failure:
- `auth failed expired`: Timestamp expired or format error
- `auth failed secret length must be 32`: Incorrect secret length
- `auth failed invalid secret`: Signature verification failed
- `invalid token`: JWT verification failed during API key generation

View File

@@ -26,7 +26,7 @@
### Plugin Development
[plugin/README.md](../plugin/README.md)
[plugin/README.md](../../plugin/README.md)
## Task System

View File

@@ -0,0 +1,279 @@
# 流鉴权机制
Monibuca V5 提供了完善的流鉴权机制,用于控制推流和拉流的访问权限。鉴权机制支持多种方式,包括基于密钥的签名鉴权和自定义鉴权处理器。
## 鉴权原理
### 1. 鉴权流程时序图
#### 推流鉴权时序图
```mermaid
sequenceDiagram
participant Client as 推流客户端
participant Plugin as 插件
participant AuthHandler as 鉴权处理器
participant Server as 服务器
Client->>Plugin: 推流请求 (streamPath, args)
Plugin->>Plugin: 检查 EnableAuth && Type == PublishTypeServer
alt 启用鉴权
Plugin->>Plugin: 查找自定义鉴权处理器
alt 存在自定义处理器
Plugin->>AuthHandler: onAuthPub(publisher)
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
AuthHandler-->>Plugin: 鉴权结果
else 使用密钥鉴权
Plugin->>Plugin: 检查 conf.Key 是否存在
alt 配置了Key
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: 验证时间戳
Plugin->>Plugin: 验证secret长度
Plugin->>Plugin: 计算MD5签名
Plugin->>Plugin: 比较签名
Plugin-->>Plugin: 鉴权结果
end
end
alt 鉴权失败
Plugin-->>Client: 鉴权失败,拒绝推流
else 鉴权成功
Plugin->>Server: 创建Publisher并添加到流管理
Server-->>Plugin: 推流成功
Plugin-->>Client: 推流建立成功
end
else 未启用鉴权
Plugin->>Server: 直接创建Publisher
Server-->>Plugin: 推流成功
Plugin-->>Client: 推流建立成功
end
```
#### 拉流鉴权时序图
```mermaid
sequenceDiagram
participant Client as 拉流客户端
participant Plugin as 插件
participant AuthHandler as 鉴权处理器
participant Server as 服务器
Client->>Plugin: 拉流请求 (streamPath, args)
Plugin->>Plugin: 检查 EnableAuth && Type == SubscribeTypeServer
alt 启用鉴权
Plugin->>Plugin: 查找自定义鉴权处理器
alt 存在自定义处理器
Plugin->>AuthHandler: onAuthSub(subscriber)
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
AuthHandler-->>Plugin: 鉴权结果
else 使用密钥鉴权
Plugin->>Plugin: 检查 conf.Key 是否存在
alt 配置了Key
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: 验证时间戳
Plugin->>Plugin: 验证secret长度
Plugin->>Plugin: 计算MD5签名
Plugin->>Plugin: 比较签名
Plugin-->>Plugin: 鉴权结果
end
end
alt 鉴权失败
Plugin-->>Client: 鉴权失败,拒绝拉流
else 鉴权成功
Plugin->>Server: 创建Subscriber并等待Publisher
Server->>Server: 等待流发布和轨道就绪
Server-->>Plugin: 拉流准备就绪
Plugin-->>Client: 开始传输流数据
end
else 未启用鉴权
Plugin->>Server: 直接创建Subscriber
Server-->>Plugin: 拉流成功
Plugin-->>Client: 开始传输流数据
end
```
### 2. 鉴权触发时机
鉴权在以下两种情况下触发:
- **推流鉴权**:当有推流请求时,在`PublishWithConfig`方法中触发
- **拉流鉴权**:当有拉流请求时,在`SubscribeWithConfig`方法中触发
### 3. 鉴权条件判断
鉴权只在以下条件同时满足时才会执行:
```go
if p.config.EnableAuth && publisher.Type == PublishTypeServer
```
- `EnableAuth`:插件配置中启用了鉴权
- `Type == PublishTypeServer/SubscribeTypeServer`:只对服务端类型的推流/拉流进行鉴权
### 4. 鉴权方式优先级
系统按以下优先级执行鉴权:
1. **自定义鉴权处理器**(最高优先级)
2. **基于密钥的签名鉴权**
3. **无鉴权**(默认通过)
## 自定义鉴权处理器
### 推流鉴权处理器
```go
onAuthPub := p.Meta.OnAuthPub
if onAuthPub == nil {
onAuthPub = p.Server.Meta.OnAuthPub
}
if onAuthPub != nil {
if err = onAuthPub(publisher).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
鉴权处理器查找顺序:
1. 插件级别的鉴权处理器 `p.Meta.OnAuthPub`
2. 服务器级别的鉴权处理器 `p.Server.Meta.OnAuthPub`
### 拉流鉴权处理器
```go
onAuthSub := p.Meta.OnAuthSub
if onAuthSub == nil {
onAuthSub = p.Server.Meta.OnAuthSub
}
if onAuthSub != nil {
if err = onAuthSub(subscriber).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
## 基于密钥的签名鉴权
当没有自定义鉴权处理器时如果配置了Key系统将使用基于MD5的签名鉴权机制。
### 鉴权算法
```go
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
// 1. 验证过期时间
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
return fmt.Errorf("auth failed expired")
}
// 2. 验证secret长度
if len(secret) != 32 {
return fmt.Errorf("auth failed secret length must be 32")
}
// 3. 计算真实的secret
trueSecret := md5.Sum([]byte(key + streamPath + expire))
// 4. 比较secret
if secret == hex.EncodeToString(trueSecret[:]) {
return nil
}
return fmt.Errorf("auth failed invalid secret")
}
```
### 签名计算步骤
1. **构造签名字符串**`key + streamPath + expire`
2. **MD5加密**对签名字符串进行MD5哈希
3. **十六进制编码**将MD5结果转换为32位十六进制字符串
4. **验证签名**比较计算结果与客户端提供的secret
### 参数说明
| 参数 | 类型 | 说明 | 示例 |
|------|------|------|------|
| key | string | 密钥,在配置文件中设置 | "mySecretKey" |
| streamPath | string | 流路径 | "live/test" |
| expire | string | 过期时间戳16进制 | "64a1b2c3" |
| secret | string | 客户端计算的签名32位十六进制 | "5d41402abc4b2a76b9719d911017c592" |
### 时间戳处理
- 过期时间使用16进制Unix时间戳
- 系统会验证当前时间是否超过过期时间
- 时间戳解析失败或已过期都会导致鉴权失败
## API密钥生成
系统还提供了API接口用于生成密钥支持管理后台的鉴权需求
```go
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// JWT Token验证
authHeader := r.Header.Get("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
_, err := p.Server.ValidateToken(tokenString)
// 生成推流或拉流密钥
streamPath := r.PathValue("streamPath")
t := r.PathValue("type")
expire := r.URL.Query().Get("expire")
if t == "publish" {
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
} else if t == "subscribe" {
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
}
}))
```
## 配置示例
### 启用鉴权
```yaml
# 插件配置
rtmp:
enableAuth: true
publish:
key: "your-publish-key"
subscribe:
key: "your-subscribe-key"
```
### 推流URL示例
```
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
```
### 拉流URL示例
```
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
```
## 安全考虑
1. **密钥保护**配置文件中的key应当妥善保管避免泄露
2. **时间窗口**:合理设置过期时间,平衡安全性和可用性
3. **HTTPS传输**生产环境建议使用HTTPS传输鉴权参数
4. **日志记录**:鉴权失败会记录警告日志,便于安全审计
## 错误处理
鉴权失败的常见原因:
- `auth failed expired`:时间戳已过期或格式错误
- `auth failed secret length must be 32`secret长度不正确
- `auth failed invalid secret`:签名验证失败
- `invalid token`API密钥生成时JWT验证失败

View File

@@ -4010,9 +4010,8 @@ type ReqRecordList struct {
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"`
Type string `protobuf:"bytes,8,opt,name=type,proto3" json:"type,omitempty"`
EventLevel string `protobuf:"bytes,9,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
EventLevel string `protobuf:"bytes,8,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4089,13 +4088,6 @@ func (x *ReqRecordList) GetPageSize() uint32 {
return 0
}
func (x *ReqRecordList) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *ReqRecordList) GetType() string {
if x != nil {
return x.Type
@@ -4117,9 +4109,6 @@ type RecordFile struct {
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"`
EventLevel string `protobuf:"bytes,6,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
EventName string `protobuf:"bytes,7,opt,name=eventName,proto3" json:"eventName,omitempty"`
EventDesc string `protobuf:"bytes,8,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4189,53 +4178,35 @@ func (x *RecordFile) GetEndTime() *timestamppb.Timestamp {
return nil
}
func (x *RecordFile) GetEventLevel() string {
if x != nil {
return x.EventLevel
}
return ""
}
func (x *RecordFile) GetEventName() string {
if x != nil {
return x.EventName
}
return ""
}
func (x *RecordFile) GetEventDesc() string {
if x != nil {
return x.EventDesc
}
return ""
}
type ResponseList struct {
type EventRecordFile struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Total uint32 `protobuf:"varint,3,opt,name=total,proto3" json:"total,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"`
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"`
EventId string `protobuf:"bytes,6,opt,name=eventId,proto3" json:"eventId,omitempty"`
EventLevel string `protobuf:"bytes,7,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
EventName string `protobuf:"bytes,8,opt,name=eventName,proto3" json:"eventName,omitempty"`
EventDesc string `protobuf:"bytes,9,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ResponseList) Reset() {
*x = ResponseList{}
func (x *EventRecordFile) Reset() {
*x = EventRecordFile{}
mi := &file_global_proto_msgTypes[58]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ResponseList) String() string {
func (x *EventRecordFile) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResponseList) ProtoMessage() {}
func (*EventRecordFile) ProtoMessage() {}
func (x *ResponseList) ProtoReflect() protoreflect.Message {
func (x *EventRecordFile) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[58]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -4247,47 +4218,236 @@ func (x *ResponseList) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use ResponseList.ProtoReflect.Descriptor instead.
func (*ResponseList) Descriptor() ([]byte, []int) {
// Deprecated: Use EventRecordFile.ProtoReflect.Descriptor instead.
func (*EventRecordFile) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{58}
}
func (x *ResponseList) GetCode() int32 {
func (x *EventRecordFile) GetId() uint32 {
if x != nil {
return x.Id
}
return 0
}
func (x *EventRecordFile) GetFilePath() string {
if x != nil {
return x.FilePath
}
return ""
}
func (x *EventRecordFile) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
func (x *EventRecordFile) GetStartTime() *timestamppb.Timestamp {
if x != nil {
return x.StartTime
}
return nil
}
func (x *EventRecordFile) GetEndTime() *timestamppb.Timestamp {
if x != nil {
return x.EndTime
}
return nil
}
func (x *EventRecordFile) GetEventId() string {
if x != nil {
return x.EventId
}
return ""
}
func (x *EventRecordFile) GetEventLevel() string {
if x != nil {
return x.EventLevel
}
return ""
}
func (x *EventRecordFile) GetEventName() string {
if x != nil {
return x.EventName
}
return ""
}
func (x *EventRecordFile) GetEventDesc() string {
if x != nil {
return x.EventDesc
}
return ""
}
type RecordResponseList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Total uint32 `protobuf:"varint,3,opt,name=total,proto3" json:"total,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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RecordResponseList) Reset() {
*x = RecordResponseList{}
mi := &file_global_proto_msgTypes[59]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RecordResponseList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RecordResponseList) ProtoMessage() {}
func (x *RecordResponseList) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[59]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RecordResponseList.ProtoReflect.Descriptor instead.
func (*RecordResponseList) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{59}
}
func (x *RecordResponseList) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *ResponseList) GetMessage() string {
func (x *RecordResponseList) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ResponseList) GetTotal() uint32 {
func (x *RecordResponseList) GetTotal() uint32 {
if x != nil {
return x.Total
}
return 0
}
func (x *ResponseList) GetPageNum() uint32 {
func (x *RecordResponseList) GetPageNum() uint32 {
if x != nil {
return x.PageNum
}
return 0
}
func (x *ResponseList) GetPageSize() uint32 {
func (x *RecordResponseList) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *ResponseList) GetData() []*RecordFile {
func (x *RecordResponseList) GetData() []*RecordFile {
if x != nil {
return x.Data
}
return nil
}
type EventRecordResponseList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Total uint32 `protobuf:"varint,3,opt,name=total,proto3" json:"total,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 []*EventRecordFile `protobuf:"bytes,6,rep,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EventRecordResponseList) Reset() {
*x = EventRecordResponseList{}
mi := &file_global_proto_msgTypes[60]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EventRecordResponseList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EventRecordResponseList) ProtoMessage() {}
func (x *EventRecordResponseList) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[60]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EventRecordResponseList.ProtoReflect.Descriptor instead.
func (*EventRecordResponseList) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{60}
}
func (x *EventRecordResponseList) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *EventRecordResponseList) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *EventRecordResponseList) GetTotal() uint32 {
if x != nil {
return x.Total
}
return 0
}
func (x *EventRecordResponseList) GetPageNum() uint32 {
if x != nil {
return x.PageNum
}
return 0
}
func (x *EventRecordResponseList) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *EventRecordResponseList) GetData() []*EventRecordFile {
if x != nil {
return x.Data
}
@@ -4306,7 +4466,7 @@ type Catalog struct {
func (x *Catalog) Reset() {
*x = Catalog{}
mi := &file_global_proto_msgTypes[59]
mi := &file_global_proto_msgTypes[61]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4318,7 +4478,7 @@ func (x *Catalog) String() string {
func (*Catalog) ProtoMessage() {}
func (x *Catalog) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[59]
mi := &file_global_proto_msgTypes[61]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4331,7 +4491,7 @@ func (x *Catalog) ProtoReflect() protoreflect.Message {
// Deprecated: Use Catalog.ProtoReflect.Descriptor instead.
func (*Catalog) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{59}
return file_global_proto_rawDescGZIP(), []int{61}
}
func (x *Catalog) GetStreamPath() string {
@@ -4373,7 +4533,7 @@ type ResponseCatalog struct {
func (x *ResponseCatalog) Reset() {
*x = ResponseCatalog{}
mi := &file_global_proto_msgTypes[60]
mi := &file_global_proto_msgTypes[62]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4385,7 +4545,7 @@ func (x *ResponseCatalog) String() string {
func (*ResponseCatalog) ProtoMessage() {}
func (x *ResponseCatalog) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[60]
mi := &file_global_proto_msgTypes[62]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4398,7 +4558,7 @@ func (x *ResponseCatalog) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResponseCatalog.ProtoReflect.Descriptor instead.
func (*ResponseCatalog) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{60}
return file_global_proto_rawDescGZIP(), []int{62}
}
func (x *ResponseCatalog) GetCode() int32 {
@@ -4436,7 +4596,7 @@ type ReqRecordDelete struct {
func (x *ReqRecordDelete) Reset() {
*x = ReqRecordDelete{}
mi := &file_global_proto_msgTypes[61]
mi := &file_global_proto_msgTypes[63]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4448,7 +4608,7 @@ func (x *ReqRecordDelete) String() string {
func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[61]
mi := &file_global_proto_msgTypes[63]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4461,7 +4621,7 @@ func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReqRecordDelete.ProtoReflect.Descriptor instead.
func (*ReqRecordDelete) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{61}
return file_global_proto_rawDescGZIP(), []int{63}
}
func (x *ReqRecordDelete) GetStreamPath() string {
@@ -4517,7 +4677,7 @@ type ResponseDelete struct {
func (x *ResponseDelete) Reset() {
*x = ResponseDelete{}
mi := &file_global_proto_msgTypes[62]
mi := &file_global_proto_msgTypes[64]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4529,7 +4689,7 @@ func (x *ResponseDelete) String() string {
func (*ResponseDelete) ProtoMessage() {}
func (x *ResponseDelete) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[62]
mi := &file_global_proto_msgTypes[64]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4542,7 +4702,7 @@ func (x *ResponseDelete) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResponseDelete.ProtoReflect.Descriptor instead.
func (*ResponseDelete) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{62}
return file_global_proto_rawDescGZIP(), []int{64}
}
func (x *ResponseDelete) GetCode() int32 {
@@ -4575,7 +4735,7 @@ type ReqRecordCatalog struct {
func (x *ReqRecordCatalog) Reset() {
*x = ReqRecordCatalog{}
mi := &file_global_proto_msgTypes[63]
mi := &file_global_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4587,7 +4747,7 @@ func (x *ReqRecordCatalog) String() string {
func (*ReqRecordCatalog) ProtoMessage() {}
func (x *ReqRecordCatalog) ProtoReflect() protoreflect.Message {
mi := &file_global_proto_msgTypes[63]
mi := &file_global_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4600,7 +4760,7 @@ func (x *ReqRecordCatalog) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReqRecordCatalog.ProtoReflect.Descriptor instead.
func (*ReqRecordCatalog) Descriptor() ([]byte, []int) {
return file_global_proto_rawDescGZIP(), []int{63}
return file_global_proto_rawDescGZIP(), []int{65}
}
func (x *ReqRecordCatalog) GetType() string {
@@ -5015,7 +5175,7 @@ const file_global_proto_rawDesc = "" +
"\x15TransformListResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12%\n" +
"\x04data\x18\x03 \x03(\v2\x11.global.TransformR\x04data\"\xeb\x01\n" +
"\x04data\x18\x03 \x03(\v2\x11.global.TransformR\x04data\"\xd7\x01\n" +
"\rReqRecordList\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
@@ -5025,11 +5185,10 @@ const file_global_proto_rawDesc = "" +
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
"\x04mode\x18\a \x01(\tR\x04mode\x12\x12\n" +
"\x04type\x18\b \x01(\tR\x04type\x12\x1e\n" +
"\x04type\x18\a \x01(\tR\x04type\x12\x1e\n" +
"\n" +
"eventLevel\x18\t \x01(\tR\n" +
"eventLevel\"\xa4\x02\n" +
"eventLevel\x18\b \x01(\tR\n" +
"eventLevel\"\xc8\x01\n" +
"\n" +
"RecordFile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1a\n" +
@@ -5038,19 +5197,35 @@ const file_global_proto_rawDesc = "" +
"streamPath\x18\x03 \x01(\tR\n" +
"streamPath\x128\n" +
"\tstartTime\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x124\n" +
"\aendTime\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\x12\x1e\n" +
"\aendTime\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\"\xc3\x02\n" +
"\x0fEventRecordFile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1a\n" +
"\bfilePath\x18\x02 \x01(\tR\bfilePath\x12\x1e\n" +
"\n" +
"eventLevel\x18\x06 \x01(\tR\n" +
"streamPath\x18\x03 \x01(\tR\n" +
"streamPath\x128\n" +
"\tstartTime\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x124\n" +
"\aendTime\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\x12\x18\n" +
"\aeventId\x18\x06 \x01(\tR\aeventId\x12\x1e\n" +
"\n" +
"eventLevel\x18\a \x01(\tR\n" +
"eventLevel\x12\x1c\n" +
"\teventName\x18\a \x01(\tR\teventName\x12\x1c\n" +
"\teventDesc\x18\b \x01(\tR\teventDesc\"\xb0\x01\n" +
"\fResponseList\x12\x12\n" +
"\teventName\x18\b \x01(\tR\teventName\x12\x1c\n" +
"\teventDesc\x18\t \x01(\tR\teventDesc\"\xb6\x01\n" +
"\x12RecordResponseList\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x14\n" +
"\x05total\x18\x03 \x01(\rR\x05total\x12\x18\n" +
"\apageNum\x18\x04 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x05 \x01(\rR\bpageSize\x12&\n" +
"\x04data\x18\x06 \x03(\v2\x12.global.RecordFileR\x04data\"\xaf\x01\n" +
"\x04data\x18\x06 \x03(\v2\x12.global.RecordFileR\x04data\"\xc0\x01\n" +
"\x17EventRecordResponseList\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x14\n" +
"\x05total\x18\x03 \x01(\rR\x05total\x12\x18\n" +
"\apageNum\x18\x04 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x05 \x01(\rR\bpageSize\x12+\n" +
"\x04data\x18\x06 \x03(\v2\x17.global.EventRecordFileR\x04data\"\xaf\x01\n" +
"\aCatalog\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
@@ -5076,7 +5251,7 @@ const file_global_proto_rawDesc = "" +
"\amessage\x18\x02 \x01(\tR\amessage\x12&\n" +
"\x04data\x18\x03 \x03(\v2\x12.global.RecordFileR\x04data\"&\n" +
"\x10ReqRecordCatalog\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type2\xae!\n" +
"\x04type\x18\x01 \x01(\tR\x04type2\xba\"\n" +
"\x03api\x12P\n" +
"\aSysInfo\x12\x16.google.protobuf.Empty\x1a\x17.global.SysInfoResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/sysinfo\x12i\n" +
"\x0fDisabledPlugins\x12\x16.google.protobuf.Empty\x1a\x1f.global.DisabledPluginsResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/api/plugins/disabled\x12P\n" +
@@ -5118,8 +5293,9 @@ const file_global_proto_rawDesc = "" +
"\x0fRemovePushProxy\x12\x15.global.RequestWithId\x1a\x17.global.SuccessResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/proxy/push/remove/{id}\x12d\n" +
"\x0fUpdatePushProxy\x12\x15.global.PushProxyInfo\x1a\x17.global.SuccessResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/proxy/push/update\x12_\n" +
"\fGetRecording\x12\x16.google.protobuf.Empty\x1a\x1d.global.RecordingListResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/record/list\x12f\n" +
"\x10GetTransformList\x12\x16.google.protobuf.Empty\x1a\x1d.global.TransformListResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/transform/list\x12m\n" +
"\rGetRecordList\x12\x15.global.ReqRecordList\x1a\x14.global.ResponseList\"/\x82\xd3\xe4\x93\x02)\x12'/api/record/{type}/list/{streamPath=**}\x12i\n" +
"\x10GetTransformList\x12\x16.google.protobuf.Empty\x1a\x1d.global.TransformListResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/transform/list\x12s\n" +
"\rGetRecordList\x12\x15.global.ReqRecordList\x1a\x1a.global.RecordResponseList\"/\x82\xd3\xe4\x93\x02)\x12'/api/record/{type}/list/{streamPath=**}\x12\x83\x01\n" +
"\x12GetEventRecordList\x12\x15.global.ReqRecordList\x1a\x1f.global.EventRecordResponseList\"5\x82\xd3\xe4\x93\x02/\x12-/api/record/{type}/event/list/{streamPath=**}\x12i\n" +
"\x10GetRecordCatalog\x12\x18.global.ReqRecordCatalog\x1a\x17.global.ResponseCatalog\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/api/record/{type}/catalog\x12u\n" +
"\fDeleteRecord\x12\x17.global.ReqRecordDelete\x1a\x16.global.ResponseDelete\"4\x82\xd3\xe4\x93\x02.:\x01*\")/api/record/{type}/delete/{streamPath=**}B\x10Z\x0em7s.live/v5/pbb\x06proto3"
@@ -5135,7 +5311,7 @@ func file_global_proto_rawDescGZIP() []byte {
return file_global_proto_rawDescData
}
var file_global_proto_msgTypes = make([]protoimpl.MessageInfo, 71)
var file_global_proto_msgTypes = make([]protoimpl.MessageInfo, 73)
var file_global_proto_goTypes = []any{
(*DisabledPluginsResponse)(nil), // 0: global.DisabledPluginsResponse
(*GetConfigRequest)(nil), // 1: global.GetConfigRequest
@@ -5195,171 +5371,178 @@ var file_global_proto_goTypes = []any{
(*TransformListResponse)(nil), // 55: global.TransformListResponse
(*ReqRecordList)(nil), // 56: global.ReqRecordList
(*RecordFile)(nil), // 57: global.RecordFile
(*ResponseList)(nil), // 58: global.ResponseList
(*Catalog)(nil), // 59: global.Catalog
(*ResponseCatalog)(nil), // 60: global.ResponseCatalog
(*ReqRecordDelete)(nil), // 61: global.ReqRecordDelete
(*ResponseDelete)(nil), // 62: global.ResponseDelete
(*ReqRecordCatalog)(nil), // 63: global.ReqRecordCatalog
nil, // 64: global.Formily.PropertiesEntry
nil, // 65: global.Formily.ComponentPropsEntry
nil, // 66: global.FormilyResponse.PropertiesEntry
nil, // 67: global.PluginInfo.DescriptionEntry
nil, // 68: global.TaskTreeData.DescriptionEntry
nil, // 69: global.StreamWaitListResponse.ListEntry
nil, // 70: global.TrackSnapShotData.ReaderEntry
(*timestamppb.Timestamp)(nil), // 71: google.protobuf.Timestamp
(*durationpb.Duration)(nil), // 72: google.protobuf.Duration
(*anypb.Any)(nil), // 73: google.protobuf.Any
(*emptypb.Empty)(nil), // 74: google.protobuf.Empty
(*EventRecordFile)(nil), // 58: global.EventRecordFile
(*RecordResponseList)(nil), // 59: global.RecordResponseList
(*EventRecordResponseList)(nil), // 60: global.EventRecordResponseList
(*Catalog)(nil), // 61: global.Catalog
(*ResponseCatalog)(nil), // 62: global.ResponseCatalog
(*ReqRecordDelete)(nil), // 63: global.ReqRecordDelete
(*ResponseDelete)(nil), // 64: global.ResponseDelete
(*ReqRecordCatalog)(nil), // 65: global.ReqRecordCatalog
nil, // 66: global.Formily.PropertiesEntry
nil, // 67: global.Formily.ComponentPropsEntry
nil, // 68: global.FormilyResponse.PropertiesEntry
nil, // 69: global.PluginInfo.DescriptionEntry
nil, // 70: global.TaskTreeData.DescriptionEntry
nil, // 71: global.StreamWaitListResponse.ListEntry
nil, // 72: global.TrackSnapShotData.ReaderEntry
(*timestamppb.Timestamp)(nil), // 73: google.protobuf.Timestamp
(*durationpb.Duration)(nil), // 74: google.protobuf.Duration
(*anypb.Any)(nil), // 75: google.protobuf.Any
(*emptypb.Empty)(nil), // 76: google.protobuf.Empty
}
var file_global_proto_depIdxs = []int32{
12, // 0: global.DisabledPluginsResponse.data:type_name -> global.PluginInfo
64, // 1: global.Formily.properties:type_name -> global.Formily.PropertiesEntry
65, // 2: global.Formily.componentProps:type_name -> global.Formily.ComponentPropsEntry
66, // 3: global.FormilyResponse.properties:type_name -> global.FormilyResponse.PropertiesEntry
66, // 1: global.Formily.properties:type_name -> global.Formily.PropertiesEntry
67, // 2: global.Formily.componentProps:type_name -> global.Formily.ComponentPropsEntry
68, // 3: global.FormilyResponse.properties:type_name -> global.FormilyResponse.PropertiesEntry
4, // 4: global.GetConfigResponse.data:type_name -> global.ConfigData
10, // 5: global.SummaryResponse.memory:type_name -> global.Usage
10, // 6: global.SummaryResponse.hardDisk:type_name -> global.Usage
9, // 7: global.SummaryResponse.netWork:type_name -> global.NetWorkInfo
67, // 8: global.PluginInfo.description:type_name -> global.PluginInfo.DescriptionEntry
71, // 9: global.SysInfoData.startTime:type_name -> google.protobuf.Timestamp
69, // 8: global.PluginInfo.description:type_name -> global.PluginInfo.DescriptionEntry
73, // 9: global.SysInfoData.startTime:type_name -> google.protobuf.Timestamp
12, // 10: global.SysInfoData.plugins:type_name -> global.PluginInfo
13, // 11: global.SysInfoResponse.data:type_name -> global.SysInfoData
71, // 12: global.TaskTreeData.startTime:type_name -> google.protobuf.Timestamp
68, // 13: global.TaskTreeData.description:type_name -> global.TaskTreeData.DescriptionEntry
73, // 12: global.TaskTreeData.startTime:type_name -> google.protobuf.Timestamp
70, // 13: global.TaskTreeData.description:type_name -> global.TaskTreeData.DescriptionEntry
15, // 14: global.TaskTreeData.children:type_name -> global.TaskTreeData
15, // 15: global.TaskTreeData.blocked:type_name -> global.TaskTreeData
15, // 16: global.TaskTreeResponse.data:type_name -> global.TaskTreeData
22, // 17: global.StreamListResponse.data:type_name -> global.StreamInfo
69, // 18: global.StreamWaitListResponse.list:type_name -> global.StreamWaitListResponse.ListEntry
71, // 18: global.StreamWaitListResponse.list:type_name -> global.StreamWaitListResponse.ListEntry
22, // 19: global.StreamInfoResponse.data:type_name -> global.StreamInfo
28, // 20: global.StreamInfo.audioTrack:type_name -> global.AudioTrackInfo
31, // 21: global.StreamInfo.videoTrack:type_name -> global.VideoTrackInfo
71, // 22: global.StreamInfo.startTime:type_name -> google.protobuf.Timestamp
72, // 23: global.StreamInfo.bufferTime:type_name -> google.protobuf.Duration
73, // 22: global.StreamInfo.startTime:type_name -> google.protobuf.Timestamp
74, // 23: global.StreamInfo.bufferTime:type_name -> google.protobuf.Duration
23, // 24: global.StreamInfo.recording:type_name -> global.RecordingDetail
72, // 25: global.RecordingDetail.fragment:type_name -> google.protobuf.Duration
71, // 26: global.TrackSnapShot.writeTime:type_name -> google.protobuf.Timestamp
74, // 25: global.RecordingDetail.fragment:type_name -> google.protobuf.Duration
73, // 26: global.TrackSnapShot.writeTime:type_name -> google.protobuf.Timestamp
24, // 27: global.TrackSnapShot.wrap:type_name -> global.Wrap
26, // 28: global.MemoryBlockGroup.list:type_name -> global.MemoryBlock
25, // 29: global.TrackSnapShotData.ring:type_name -> global.TrackSnapShot
70, // 30: global.TrackSnapShotData.reader:type_name -> global.TrackSnapShotData.ReaderEntry
72, // 30: global.TrackSnapShotData.reader:type_name -> global.TrackSnapShotData.ReaderEntry
27, // 31: global.TrackSnapShotData.memory:type_name -> global.MemoryBlockGroup
29, // 32: global.TrackSnapShotResponse.data:type_name -> global.TrackSnapShotData
71, // 33: global.SubscriberSnapShot.startTime:type_name -> google.protobuf.Timestamp
73, // 33: global.SubscriberSnapShot.startTime:type_name -> google.protobuf.Timestamp
37, // 34: global.SubscriberSnapShot.audioReader:type_name -> global.RingReaderSnapShot
37, // 35: global.SubscriberSnapShot.videoReader:type_name -> global.RingReaderSnapShot
72, // 36: global.SubscriberSnapShot.bufferTime:type_name -> google.protobuf.Duration
74, // 36: global.SubscriberSnapShot.bufferTime:type_name -> google.protobuf.Duration
38, // 37: global.SubscribersResponse.data:type_name -> global.SubscriberSnapShot
41, // 38: global.PullProxyListResponse.data:type_name -> global.PullProxyInfo
71, // 39: global.PullProxyInfo.createTime:type_name -> google.protobuf.Timestamp
71, // 40: global.PullProxyInfo.updateTime:type_name -> google.protobuf.Timestamp
72, // 41: global.PullProxyInfo.recordFragment:type_name -> google.protobuf.Duration
71, // 42: global.PushProxyInfo.createTime:type_name -> google.protobuf.Timestamp
71, // 43: global.PushProxyInfo.updateTime:type_name -> google.protobuf.Timestamp
73, // 39: global.PullProxyInfo.createTime:type_name -> google.protobuf.Timestamp
73, // 40: global.PullProxyInfo.updateTime:type_name -> google.protobuf.Timestamp
74, // 41: global.PullProxyInfo.recordFragment:type_name -> google.protobuf.Duration
73, // 42: global.PushProxyInfo.createTime:type_name -> google.protobuf.Timestamp
73, // 43: global.PushProxyInfo.updateTime:type_name -> google.protobuf.Timestamp
42, // 44: global.PushProxyListResponse.data:type_name -> global.PushProxyInfo
45, // 45: global.StreamAliasListResponse.data:type_name -> global.StreamAlias
71, // 46: global.Recording.startTime:type_name -> google.protobuf.Timestamp
73, // 46: global.Recording.startTime:type_name -> google.protobuf.Timestamp
49, // 47: global.RecordingListResponse.data:type_name -> global.Recording
71, // 48: global.PushInfo.startTime:type_name -> google.protobuf.Timestamp
73, // 48: global.PushInfo.startTime:type_name -> google.protobuf.Timestamp
51, // 49: global.PushListResponse.data:type_name -> global.PushInfo
54, // 50: global.TransformListResponse.data:type_name -> global.Transform
71, // 51: global.RecordFile.startTime:type_name -> google.protobuf.Timestamp
71, // 52: global.RecordFile.endTime:type_name -> google.protobuf.Timestamp
57, // 53: global.ResponseList.data:type_name -> global.RecordFile
71, // 54: global.Catalog.startTime:type_name -> google.protobuf.Timestamp
71, // 55: global.Catalog.endTime:type_name -> google.protobuf.Timestamp
59, // 56: global.ResponseCatalog.data:type_name -> global.Catalog
57, // 57: global.ResponseDelete.data:type_name -> global.RecordFile
2, // 58: global.Formily.PropertiesEntry.value:type_name -> global.Formily
73, // 59: global.Formily.ComponentPropsEntry.value:type_name -> google.protobuf.Any
2, // 60: global.FormilyResponse.PropertiesEntry.value:type_name -> global.Formily
74, // 61: global.api.SysInfo:input_type -> google.protobuf.Empty
74, // 62: global.api.DisabledPlugins:input_type -> google.protobuf.Empty
74, // 63: global.api.Summary:input_type -> google.protobuf.Empty
33, // 64: global.api.Shutdown:input_type -> global.RequestWithId
33, // 65: global.api.Restart:input_type -> global.RequestWithId
74, // 66: global.api.TaskTree:input_type -> google.protobuf.Empty
34, // 67: global.api.StopTask:input_type -> global.RequestWithId64
34, // 68: global.api.RestartTask:input_type -> global.RequestWithId64
17, // 69: global.api.StreamList:input_type -> global.StreamListRequest
74, // 70: global.api.WaitList:input_type -> google.protobuf.Empty
20, // 71: global.api.StreamInfo:input_type -> global.StreamSnapRequest
20, // 72: global.api.PauseStream:input_type -> global.StreamSnapRequest
20, // 73: global.api.ResumeStream:input_type -> global.StreamSnapRequest
47, // 74: global.api.SetStreamSpeed:input_type -> global.SetStreamSpeedRequest
48, // 75: global.api.SeekStream:input_type -> global.SeekStreamRequest
36, // 76: global.api.GetSubscribers:input_type -> global.SubscribersRequest
20, // 77: global.api.AudioTrackSnap:input_type -> global.StreamSnapRequest
20, // 78: global.api.VideoTrackSnap:input_type -> global.StreamSnapRequest
35, // 79: global.api.ChangeSubscribe:input_type -> global.ChangeSubscribeRequest
74, // 80: global.api.GetStreamAlias:input_type -> google.protobuf.Empty
44, // 81: global.api.SetStreamAlias:input_type -> global.SetStreamAliasRequest
20, // 82: global.api.StopPublish:input_type -> global.StreamSnapRequest
33, // 83: global.api.StopSubscribe:input_type -> global.RequestWithId
74, // 84: global.api.GetConfigFile:input_type -> google.protobuf.Empty
7, // 85: global.api.UpdateConfigFile:input_type -> global.UpdateConfigFileRequest
1, // 86: global.api.GetConfig:input_type -> global.GetConfigRequest
1, // 87: global.api.GetFormily:input_type -> global.GetConfigRequest
74, // 88: global.api.GetPullProxyList:input_type -> google.protobuf.Empty
41, // 89: global.api.AddPullProxy:input_type -> global.PullProxyInfo
33, // 90: global.api.RemovePullProxy:input_type -> global.RequestWithId
41, // 91: global.api.UpdatePullProxy:input_type -> global.PullProxyInfo
74, // 92: global.api.GetPushProxyList:input_type -> google.protobuf.Empty
42, // 93: global.api.AddPushProxy:input_type -> global.PushProxyInfo
33, // 94: global.api.RemovePushProxy:input_type -> global.RequestWithId
42, // 95: global.api.UpdatePushProxy:input_type -> global.PushProxyInfo
74, // 96: global.api.GetRecording:input_type -> google.protobuf.Empty
74, // 97: global.api.GetTransformList:input_type -> google.protobuf.Empty
56, // 98: global.api.GetRecordList:input_type -> global.ReqRecordList
63, // 99: global.api.GetRecordCatalog:input_type -> global.ReqRecordCatalog
61, // 100: global.api.DeleteRecord:input_type -> global.ReqRecordDelete
14, // 101: global.api.SysInfo:output_type -> global.SysInfoResponse
0, // 102: global.api.DisabledPlugins:output_type -> global.DisabledPluginsResponse
11, // 103: global.api.Summary:output_type -> global.SummaryResponse
32, // 104: global.api.Shutdown:output_type -> global.SuccessResponse
32, // 105: global.api.Restart:output_type -> global.SuccessResponse
16, // 106: global.api.TaskTree:output_type -> global.TaskTreeResponse
32, // 107: global.api.StopTask:output_type -> global.SuccessResponse
32, // 108: global.api.RestartTask:output_type -> global.SuccessResponse
18, // 109: global.api.StreamList:output_type -> global.StreamListResponse
19, // 110: global.api.WaitList:output_type -> global.StreamWaitListResponse
21, // 111: global.api.StreamInfo:output_type -> global.StreamInfoResponse
32, // 112: global.api.PauseStream:output_type -> global.SuccessResponse
32, // 113: global.api.ResumeStream:output_type -> global.SuccessResponse
32, // 114: global.api.SetStreamSpeed:output_type -> global.SuccessResponse
32, // 115: global.api.SeekStream:output_type -> global.SuccessResponse
39, // 116: global.api.GetSubscribers:output_type -> global.SubscribersResponse
30, // 117: global.api.AudioTrackSnap:output_type -> global.TrackSnapShotResponse
30, // 118: global.api.VideoTrackSnap:output_type -> global.TrackSnapShotResponse
32, // 119: global.api.ChangeSubscribe:output_type -> global.SuccessResponse
46, // 120: global.api.GetStreamAlias:output_type -> global.StreamAliasListResponse
32, // 121: global.api.SetStreamAlias:output_type -> global.SuccessResponse
32, // 122: global.api.StopPublish:output_type -> global.SuccessResponse
32, // 123: global.api.StopSubscribe:output_type -> global.SuccessResponse
5, // 124: global.api.GetConfigFile:output_type -> global.GetConfigFileResponse
32, // 125: global.api.UpdateConfigFile:output_type -> global.SuccessResponse
6, // 126: global.api.GetConfig:output_type -> global.GetConfigResponse
6, // 127: global.api.GetFormily:output_type -> global.GetConfigResponse
40, // 128: global.api.GetPullProxyList:output_type -> global.PullProxyListResponse
32, // 129: global.api.AddPullProxy:output_type -> global.SuccessResponse
32, // 130: global.api.RemovePullProxy:output_type -> global.SuccessResponse
32, // 131: global.api.UpdatePullProxy:output_type -> global.SuccessResponse
43, // 132: global.api.GetPushProxyList:output_type -> global.PushProxyListResponse
32, // 133: global.api.AddPushProxy:output_type -> global.SuccessResponse
32, // 134: global.api.RemovePushProxy:output_type -> global.SuccessResponse
32, // 135: global.api.UpdatePushProxy:output_type -> global.SuccessResponse
50, // 136: global.api.GetRecording:output_type -> global.RecordingListResponse
55, // 137: global.api.GetTransformList:output_type -> global.TransformListResponse
58, // 138: global.api.GetRecordList:output_type -> global.ResponseList
60, // 139: global.api.GetRecordCatalog:output_type -> global.ResponseCatalog
62, // 140: global.api.DeleteRecord:output_type -> global.ResponseDelete
101, // [101:141] is the sub-list for method output_type
61, // [61:101] is the sub-list for method input_type
61, // [61:61] is the sub-list for extension type_name
61, // [61:61] is the sub-list for extension extendee
0, // [0:61] is the sub-list for field type_name
73, // 51: global.RecordFile.startTime:type_name -> google.protobuf.Timestamp
73, // 52: global.RecordFile.endTime:type_name -> google.protobuf.Timestamp
73, // 53: global.EventRecordFile.startTime:type_name -> google.protobuf.Timestamp
73, // 54: global.EventRecordFile.endTime:type_name -> google.protobuf.Timestamp
57, // 55: global.RecordResponseList.data:type_name -> global.RecordFile
58, // 56: global.EventRecordResponseList.data:type_name -> global.EventRecordFile
73, // 57: global.Catalog.startTime:type_name -> google.protobuf.Timestamp
73, // 58: global.Catalog.endTime:type_name -> google.protobuf.Timestamp
61, // 59: global.ResponseCatalog.data:type_name -> global.Catalog
57, // 60: global.ResponseDelete.data:type_name -> global.RecordFile
2, // 61: global.Formily.PropertiesEntry.value:type_name -> global.Formily
75, // 62: global.Formily.ComponentPropsEntry.value:type_name -> google.protobuf.Any
2, // 63: global.FormilyResponse.PropertiesEntry.value:type_name -> global.Formily
76, // 64: global.api.SysInfo:input_type -> google.protobuf.Empty
76, // 65: global.api.DisabledPlugins:input_type -> google.protobuf.Empty
76, // 66: global.api.Summary:input_type -> google.protobuf.Empty
33, // 67: global.api.Shutdown:input_type -> global.RequestWithId
33, // 68: global.api.Restart:input_type -> global.RequestWithId
76, // 69: global.api.TaskTree:input_type -> google.protobuf.Empty
34, // 70: global.api.StopTask:input_type -> global.RequestWithId64
34, // 71: global.api.RestartTask:input_type -> global.RequestWithId64
17, // 72: global.api.StreamList:input_type -> global.StreamListRequest
76, // 73: global.api.WaitList:input_type -> google.protobuf.Empty
20, // 74: global.api.StreamInfo:input_type -> global.StreamSnapRequest
20, // 75: global.api.PauseStream:input_type -> global.StreamSnapRequest
20, // 76: global.api.ResumeStream:input_type -> global.StreamSnapRequest
47, // 77: global.api.SetStreamSpeed:input_type -> global.SetStreamSpeedRequest
48, // 78: global.api.SeekStream:input_type -> global.SeekStreamRequest
36, // 79: global.api.GetSubscribers:input_type -> global.SubscribersRequest
20, // 80: global.api.AudioTrackSnap:input_type -> global.StreamSnapRequest
20, // 81: global.api.VideoTrackSnap:input_type -> global.StreamSnapRequest
35, // 82: global.api.ChangeSubscribe:input_type -> global.ChangeSubscribeRequest
76, // 83: global.api.GetStreamAlias:input_type -> google.protobuf.Empty
44, // 84: global.api.SetStreamAlias:input_type -> global.SetStreamAliasRequest
20, // 85: global.api.StopPublish:input_type -> global.StreamSnapRequest
33, // 86: global.api.StopSubscribe:input_type -> global.RequestWithId
76, // 87: global.api.GetConfigFile:input_type -> google.protobuf.Empty
7, // 88: global.api.UpdateConfigFile:input_type -> global.UpdateConfigFileRequest
1, // 89: global.api.GetConfig:input_type -> global.GetConfigRequest
1, // 90: global.api.GetFormily:input_type -> global.GetConfigRequest
76, // 91: global.api.GetPullProxyList:input_type -> google.protobuf.Empty
41, // 92: global.api.AddPullProxy:input_type -> global.PullProxyInfo
33, // 93: global.api.RemovePullProxy:input_type -> global.RequestWithId
41, // 94: global.api.UpdatePullProxy:input_type -> global.PullProxyInfo
76, // 95: global.api.GetPushProxyList:input_type -> google.protobuf.Empty
42, // 96: global.api.AddPushProxy:input_type -> global.PushProxyInfo
33, // 97: global.api.RemovePushProxy:input_type -> global.RequestWithId
42, // 98: global.api.UpdatePushProxy:input_type -> global.PushProxyInfo
76, // 99: global.api.GetRecording:input_type -> google.protobuf.Empty
76, // 100: global.api.GetTransformList:input_type -> google.protobuf.Empty
56, // 101: global.api.GetRecordList:input_type -> global.ReqRecordList
56, // 102: global.api.GetEventRecordList:input_type -> global.ReqRecordList
65, // 103: global.api.GetRecordCatalog:input_type -> global.ReqRecordCatalog
63, // 104: global.api.DeleteRecord:input_type -> global.ReqRecordDelete
14, // 105: global.api.SysInfo:output_type -> global.SysInfoResponse
0, // 106: global.api.DisabledPlugins:output_type -> global.DisabledPluginsResponse
11, // 107: global.api.Summary:output_type -> global.SummaryResponse
32, // 108: global.api.Shutdown:output_type -> global.SuccessResponse
32, // 109: global.api.Restart:output_type -> global.SuccessResponse
16, // 110: global.api.TaskTree:output_type -> global.TaskTreeResponse
32, // 111: global.api.StopTask:output_type -> global.SuccessResponse
32, // 112: global.api.RestartTask:output_type -> global.SuccessResponse
18, // 113: global.api.StreamList:output_type -> global.StreamListResponse
19, // 114: global.api.WaitList:output_type -> global.StreamWaitListResponse
21, // 115: global.api.StreamInfo:output_type -> global.StreamInfoResponse
32, // 116: global.api.PauseStream:output_type -> global.SuccessResponse
32, // 117: global.api.ResumeStream:output_type -> global.SuccessResponse
32, // 118: global.api.SetStreamSpeed:output_type -> global.SuccessResponse
32, // 119: global.api.SeekStream:output_type -> global.SuccessResponse
39, // 120: global.api.GetSubscribers:output_type -> global.SubscribersResponse
30, // 121: global.api.AudioTrackSnap:output_type -> global.TrackSnapShotResponse
30, // 122: global.api.VideoTrackSnap:output_type -> global.TrackSnapShotResponse
32, // 123: global.api.ChangeSubscribe:output_type -> global.SuccessResponse
46, // 124: global.api.GetStreamAlias:output_type -> global.StreamAliasListResponse
32, // 125: global.api.SetStreamAlias:output_type -> global.SuccessResponse
32, // 126: global.api.StopPublish:output_type -> global.SuccessResponse
32, // 127: global.api.StopSubscribe:output_type -> global.SuccessResponse
5, // 128: global.api.GetConfigFile:output_type -> global.GetConfigFileResponse
32, // 129: global.api.UpdateConfigFile:output_type -> global.SuccessResponse
6, // 130: global.api.GetConfig:output_type -> global.GetConfigResponse
6, // 131: global.api.GetFormily:output_type -> global.GetConfigResponse
40, // 132: global.api.GetPullProxyList:output_type -> global.PullProxyListResponse
32, // 133: global.api.AddPullProxy:output_type -> global.SuccessResponse
32, // 134: global.api.RemovePullProxy:output_type -> global.SuccessResponse
32, // 135: global.api.UpdatePullProxy:output_type -> global.SuccessResponse
43, // 136: global.api.GetPushProxyList:output_type -> global.PushProxyListResponse
32, // 137: global.api.AddPushProxy:output_type -> global.SuccessResponse
32, // 138: global.api.RemovePushProxy:output_type -> global.SuccessResponse
32, // 139: global.api.UpdatePushProxy:output_type -> global.SuccessResponse
50, // 140: global.api.GetRecording:output_type -> global.RecordingListResponse
55, // 141: global.api.GetTransformList:output_type -> global.TransformListResponse
59, // 142: global.api.GetRecordList:output_type -> global.RecordResponseList
60, // 143: global.api.GetEventRecordList:output_type -> global.EventRecordResponseList
62, // 144: global.api.GetRecordCatalog:output_type -> global.ResponseCatalog
64, // 145: global.api.DeleteRecord:output_type -> global.ResponseDelete
105, // [105:146] is the sub-list for method output_type
64, // [64:105] is the sub-list for method input_type
64, // [64:64] is the sub-list for extension type_name
64, // [64:64] is the sub-list for extension extendee
0, // [0:64] is the sub-list for field type_name
}
func init() { file_global_proto_init() }
@@ -5373,7 +5556,7 @@ func file_global_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_global_proto_rawDesc), len(file_global_proto_rawDesc)),
NumEnums: 0,
NumMessages: 71,
NumMessages: 73,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -1708,6 +1708,96 @@ func local_request_Api_GetRecordList_0(ctx context.Context, marshaler runtime.Ma
}
var (
filter_Api_GetEventRecordList_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_GetEventRecordList_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_GetEventRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetEventRecordList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetEventRecordList_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_GetEventRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetEventRecordList(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
@@ -2896,6 +2986,31 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("GET", pattern_Api_GetEventRecordList_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/GetEventRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/event/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetEventRecordList_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_GetEventRecordList_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()
@@ -3911,6 +4026,28 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("GET", pattern_Api_GetEventRecordList_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/GetEventRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/event/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetEventRecordList_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_GetEventRecordList_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()
@@ -4043,6 +4180,8 @@ var (
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_GetEventRecordList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4, 3, 0, 4, 1, 5, 5}, []string{"api", "record", "type", "event", "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"}, ""))
@@ -4133,6 +4272,8 @@ var (
forward_Api_GetRecordList_0 = runtime.ForwardResponseMessage
forward_Api_GetEventRecordList_0 = runtime.ForwardResponseMessage
forward_Api_GetRecordCatalog_0 = runtime.ForwardResponseMessage
forward_Api_DeleteRecord_0 = runtime.ForwardResponseMessage

View File

@@ -224,11 +224,16 @@ service api {
get: "/api/transform/list"
};
}
rpc GetRecordList (ReqRecordList) returns (ResponseList) {
rpc GetRecordList (ReqRecordList) returns (RecordResponseList) {
option (google.api.http) = {
get: "/api/record/{type}/list/{streamPath=**}"
};
}
rpc GetEventRecordList (ReqRecordList) returns (EventRecordResponseList) {
option (google.api.http) = {
get: "/api/record/{type}/event/list/{streamPath=**}"
};
}
rpc GetRecordCatalog (ReqRecordCatalog) returns (ResponseCatalog) {
option (google.api.http) = {
get: "/api/record/{type}/catalog"
@@ -664,9 +669,8 @@ message ReqRecordList {
string end = 4;
uint32 pageNum = 5;
uint32 pageSize = 6;
string eventId = 7;
string type = 8;
string eventLevel = 9;
string type = 7;
string eventLevel = 8;
}
message RecordFile {
@@ -675,12 +679,21 @@ message RecordFile {
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
string eventLevel = 6;
string eventName = 7;
string eventDesc = 8;
}
message ResponseList {
message EventRecordFile {
uint32 id = 1;
string filePath = 2;
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
string eventId = 6;
string eventLevel = 7;
string eventName = 8;
string eventDesc = 9;
}
message RecordResponseList {
int32 code = 1;
string message = 2;
uint32 total = 3;
@@ -689,6 +702,15 @@ message ResponseList {
repeated RecordFile data = 6;
}
message EventRecordResponseList {
int32 code = 1;
string message = 2;
uint32 total = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated EventRecordFile data = 6;
}
message Catalog {
string streamPath = 1;
uint32 count = 2;

View File

@@ -20,46 +20,47 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Api_SysInfo_FullMethodName = "/global.api/SysInfo"
Api_DisabledPlugins_FullMethodName = "/global.api/DisabledPlugins"
Api_Summary_FullMethodName = "/global.api/Summary"
Api_Shutdown_FullMethodName = "/global.api/Shutdown"
Api_Restart_FullMethodName = "/global.api/Restart"
Api_TaskTree_FullMethodName = "/global.api/TaskTree"
Api_StopTask_FullMethodName = "/global.api/StopTask"
Api_RestartTask_FullMethodName = "/global.api/RestartTask"
Api_StreamList_FullMethodName = "/global.api/StreamList"
Api_WaitList_FullMethodName = "/global.api/WaitList"
Api_StreamInfo_FullMethodName = "/global.api/StreamInfo"
Api_PauseStream_FullMethodName = "/global.api/PauseStream"
Api_ResumeStream_FullMethodName = "/global.api/ResumeStream"
Api_SetStreamSpeed_FullMethodName = "/global.api/SetStreamSpeed"
Api_SeekStream_FullMethodName = "/global.api/SeekStream"
Api_GetSubscribers_FullMethodName = "/global.api/GetSubscribers"
Api_AudioTrackSnap_FullMethodName = "/global.api/AudioTrackSnap"
Api_VideoTrackSnap_FullMethodName = "/global.api/VideoTrackSnap"
Api_ChangeSubscribe_FullMethodName = "/global.api/ChangeSubscribe"
Api_GetStreamAlias_FullMethodName = "/global.api/GetStreamAlias"
Api_SetStreamAlias_FullMethodName = "/global.api/SetStreamAlias"
Api_StopPublish_FullMethodName = "/global.api/StopPublish"
Api_StopSubscribe_FullMethodName = "/global.api/StopSubscribe"
Api_GetConfigFile_FullMethodName = "/global.api/GetConfigFile"
Api_UpdateConfigFile_FullMethodName = "/global.api/UpdateConfigFile"
Api_GetConfig_FullMethodName = "/global.api/GetConfig"
Api_GetFormily_FullMethodName = "/global.api/GetFormily"
Api_GetPullProxyList_FullMethodName = "/global.api/GetPullProxyList"
Api_AddPullProxy_FullMethodName = "/global.api/AddPullProxy"
Api_RemovePullProxy_FullMethodName = "/global.api/RemovePullProxy"
Api_UpdatePullProxy_FullMethodName = "/global.api/UpdatePullProxy"
Api_GetPushProxyList_FullMethodName = "/global.api/GetPushProxyList"
Api_AddPushProxy_FullMethodName = "/global.api/AddPushProxy"
Api_RemovePushProxy_FullMethodName = "/global.api/RemovePushProxy"
Api_UpdatePushProxy_FullMethodName = "/global.api/UpdatePushProxy"
Api_GetRecording_FullMethodName = "/global.api/GetRecording"
Api_GetTransformList_FullMethodName = "/global.api/GetTransformList"
Api_GetRecordList_FullMethodName = "/global.api/GetRecordList"
Api_GetRecordCatalog_FullMethodName = "/global.api/GetRecordCatalog"
Api_DeleteRecord_FullMethodName = "/global.api/DeleteRecord"
Api_SysInfo_FullMethodName = "/global.api/SysInfo"
Api_DisabledPlugins_FullMethodName = "/global.api/DisabledPlugins"
Api_Summary_FullMethodName = "/global.api/Summary"
Api_Shutdown_FullMethodName = "/global.api/Shutdown"
Api_Restart_FullMethodName = "/global.api/Restart"
Api_TaskTree_FullMethodName = "/global.api/TaskTree"
Api_StopTask_FullMethodName = "/global.api/StopTask"
Api_RestartTask_FullMethodName = "/global.api/RestartTask"
Api_StreamList_FullMethodName = "/global.api/StreamList"
Api_WaitList_FullMethodName = "/global.api/WaitList"
Api_StreamInfo_FullMethodName = "/global.api/StreamInfo"
Api_PauseStream_FullMethodName = "/global.api/PauseStream"
Api_ResumeStream_FullMethodName = "/global.api/ResumeStream"
Api_SetStreamSpeed_FullMethodName = "/global.api/SetStreamSpeed"
Api_SeekStream_FullMethodName = "/global.api/SeekStream"
Api_GetSubscribers_FullMethodName = "/global.api/GetSubscribers"
Api_AudioTrackSnap_FullMethodName = "/global.api/AudioTrackSnap"
Api_VideoTrackSnap_FullMethodName = "/global.api/VideoTrackSnap"
Api_ChangeSubscribe_FullMethodName = "/global.api/ChangeSubscribe"
Api_GetStreamAlias_FullMethodName = "/global.api/GetStreamAlias"
Api_SetStreamAlias_FullMethodName = "/global.api/SetStreamAlias"
Api_StopPublish_FullMethodName = "/global.api/StopPublish"
Api_StopSubscribe_FullMethodName = "/global.api/StopSubscribe"
Api_GetConfigFile_FullMethodName = "/global.api/GetConfigFile"
Api_UpdateConfigFile_FullMethodName = "/global.api/UpdateConfigFile"
Api_GetConfig_FullMethodName = "/global.api/GetConfig"
Api_GetFormily_FullMethodName = "/global.api/GetFormily"
Api_GetPullProxyList_FullMethodName = "/global.api/GetPullProxyList"
Api_AddPullProxy_FullMethodName = "/global.api/AddPullProxy"
Api_RemovePullProxy_FullMethodName = "/global.api/RemovePullProxy"
Api_UpdatePullProxy_FullMethodName = "/global.api/UpdatePullProxy"
Api_GetPushProxyList_FullMethodName = "/global.api/GetPushProxyList"
Api_AddPushProxy_FullMethodName = "/global.api/AddPushProxy"
Api_RemovePushProxy_FullMethodName = "/global.api/RemovePushProxy"
Api_UpdatePushProxy_FullMethodName = "/global.api/UpdatePushProxy"
Api_GetRecording_FullMethodName = "/global.api/GetRecording"
Api_GetTransformList_FullMethodName = "/global.api/GetTransformList"
Api_GetRecordList_FullMethodName = "/global.api/GetRecordList"
Api_GetEventRecordList_FullMethodName = "/global.api/GetEventRecordList"
Api_GetRecordCatalog_FullMethodName = "/global.api/GetRecordCatalog"
Api_DeleteRecord_FullMethodName = "/global.api/DeleteRecord"
)
// ApiClient is the client API for Api service.
@@ -103,7 +104,8 @@ 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)
GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*RecordResponseList, error)
GetEventRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*EventRecordResponseList, error)
GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error)
DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error)
}
@@ -486,9 +488,9 @@ 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) {
func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*RecordResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseList)
out := new(RecordResponseList)
err := c.cc.Invoke(ctx, Api_GetRecordList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
@@ -496,6 +498,16 @@ func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts .
return out, nil
}
func (c *apiClient) GetEventRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*EventRecordResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EventRecordResponseList)
err := c.cc.Invoke(ctx, Api_GetEventRecordList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseCatalog)
@@ -557,7 +569,8 @@ 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)
GetRecordList(context.Context, *ReqRecordList) (*RecordResponseList, error)
GetEventRecordList(context.Context, *ReqRecordList) (*EventRecordResponseList, error)
GetRecordCatalog(context.Context, *ReqRecordCatalog) (*ResponseCatalog, error)
DeleteRecord(context.Context, *ReqRecordDelete) (*ResponseDelete, error)
mustEmbedUnimplementedApiServer()
@@ -681,9 +694,12 @@ 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) {
func (UnimplementedApiServer) GetRecordList(context.Context, *ReqRecordList) (*RecordResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRecordList not implemented")
}
func (UnimplementedApiServer) GetEventRecordList(context.Context, *ReqRecordList) (*EventRecordResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetEventRecordList not implemented")
}
func (UnimplementedApiServer) GetRecordCatalog(context.Context, *ReqRecordCatalog) (*ResponseCatalog, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRecordCatalog not implemented")
}
@@ -1395,6 +1411,24 @@ func _Api_GetRecordList_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler)
}
func _Api_GetEventRecordList_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).GetEventRecordList(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_GetEventRecordList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetEventRecordList(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 {
@@ -1590,6 +1624,10 @@ var Api_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetRecordList",
Handler: _Api_GetRecordList_Handler,
},
{
MethodName: "GetEventRecordList",
Handler: _Api_GetEventRecordList_Handler,
},
{
MethodName: "GetRecordCatalog",
Handler: _Api_GetRecordCatalog_Handler,

View File

@@ -65,8 +65,6 @@ type (
}
)
var _ IAVFrame = (*AnnexB)(nil)
func (frame *AVFrame) Clone() {
}

74
pkg/avframe_convert.go Normal file
View File

@@ -0,0 +1,74 @@
package pkg
import (
"reflect"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
type AVFrameConvert[T IAVFrame] struct {
FromTrack, ToTrack *AVTrack
lastFromCodecCtx codec.ICodecCtx
}
func NewAVFrameConvert[T IAVFrame](fromTrack *AVTrack, toTrack *AVTrack) *AVFrameConvert[T] {
ret := &AVFrameConvert[T]{}
ret.FromTrack = fromTrack
ret.ToTrack = toTrack
if ret.FromTrack == nil {
ret.FromTrack = &AVTrack{
RingWriter: &RingWriter{
Ring: util.NewRing[AVFrame](1),
},
}
}
if ret.ToTrack == nil {
ret.ToTrack = &AVTrack{
RingWriter: &RingWriter{
Ring: util.NewRing[AVFrame](1),
},
}
var to T
ret.ToTrack.FrameType = reflect.TypeOf(to).Elem()
}
return ret
}
func (c *AVFrameConvert[T]) ConvertFromAVFrame(avFrame *AVFrame) (to T, err error) {
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
if c.ToTrack.ICodecCtx == nil {
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
return
}
}
if err = avFrame.Demux(c.FromTrack.ICodecCtx); err != nil {
return
}
to.SetAllocator(avFrame.Wraps[0].GetAllocator())
to.Mux(c.ToTrack.ICodecCtx, avFrame)
return
}
func (c *AVFrameConvert[T]) Convert(frame IAVFrame) (to T, err error) {
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
// Not From Publisher
if c.FromTrack.LastValue == nil {
err = frame.Parse(c.FromTrack)
if err != nil {
return
}
}
if c.ToTrack.ICodecCtx == nil || c.lastFromCodecCtx != c.FromTrack.ICodecCtx {
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
return
}
}
c.lastFromCodecCtx = c.FromTrack.ICodecCtx
if c.FromTrack.Value.Raw, err = frame.Demux(c.FromTrack.ICodecCtx); err != nil {
return
}
to.SetAllocator(frame.GetAllocator())
to.Mux(c.ToTrack.ICodecCtx, &c.FromTrack.Value)
return
}

View File

@@ -9,7 +9,7 @@ import (
flvpb "m7s.live/v5/plugin/flv/pb"
)
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.ResponseList, err error) {
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
globalReq := &pb.ReqRecordList{
StreamPath: req.StreamPath,
Range: req.Range,
@@ -17,7 +17,6 @@ func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *p
End: req.End,
PageNum: req.PageNum,
PageSize: req.PageSize,
Mode: req.Mode,
Type: "flv",
}
return p.Server.GetRecordList(ctx, globalReq)

View File

@@ -12,11 +12,9 @@ import (
"time"
m7s "m7s.live/v5"
codec "m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
flv "m7s.live/v5/plugin/flv/pkg"
mp4 "m7s.live/v5/plugin/mp4/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
)
@@ -198,31 +196,51 @@ func (plugin *FLVPlugin) processMp4ToFlv(w http.ResponseWriter, r *http.Request,
})
}
// 创建DemuxerRange进行MP4解复用
demuxer := &mp4.DemuxerRange{
StartTime: params.startTime,
EndTime: params.endTime,
Streams: mp4Streams,
// 创建DemuxerConverterRange进行MP4解复用和转换
demuxer := &mp4.DemuxerConverterRange[*rtmp.RTMPAudio, *rtmp.RTMPVideo]{
DemuxerRange: mp4.DemuxerRange{
StartTime: params.startTime,
EndTime: params.endTime,
Streams: mp4Streams,
Logger: plugin.Logger.With("demuxer", "mp4_flv"),
},
}
// 创建FLV编码器状态
flvWriter := &flvMp4Writer{
FlvWriter: flv.NewFlvWriter(w),
plugin: plugin,
hasWritten: false,
}
// 设置回调函数
demuxer.OnVideoExtraData = flvWriter.onVideoExtraData
demuxer.OnAudioExtraData = flvWriter.onAudioExtraData
demuxer.OnVideoSample = flvWriter.onVideoSample
demuxer.OnAudioSample = flvWriter.onAudioSample
flvWriter := flv.NewFlvWriter(w)
hasWritten := false
ts := int64(0) // 初始化时间戳
tsOffset := int64(0) // 偏移时间戳
// 执行解复用和转换
err := demuxer.Demux(r.Context())
err := demuxer.Demux(r.Context(),
func(audio *rtmp.RTMPAudio) error {
if !hasWritten {
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
return err
}
}
// 计算调整后的时间戳
ts = int64(audio.Timestamp) + tsOffset
timestamp := uint32(ts)
// 写入音频数据帧
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_AUDIO, timestamp, uint32(audio.Size), audio.Buffers...)
}, func(frame *rtmp.RTMPVideo) error {
if !hasWritten {
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
return err
}
}
// 计算调整后的时间戳
ts = int64(frame.Timestamp) + tsOffset
timestamp := uint32(ts)
// 写入视频数据帧
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(frame.Size), frame.Buffers...)
})
if err != nil {
plugin.Error("MP4 to FLV conversion failed", "err", err)
if !flvWriter.hasWritten {
if !hasWritten {
http.Error(w, "Conversion failed", http.StatusInternalServerError)
}
return
@@ -231,160 +249,6 @@ func (plugin *FLVPlugin) processMp4ToFlv(w http.ResponseWriter, r *http.Request,
plugin.Info("MP4 to FLV conversion completed")
}
type ExtraDataInfo struct {
CodecType box.MP4_CODEC_TYPE
Data []byte
}
// flvMp4Writer 处理MP4到FLV的转换写入
type flvMp4Writer struct {
*flv.FlvWriter
plugin *FLVPlugin
audioExtra, videoExtra *ExtraDataInfo
hasWritten bool // 是否已经写入FLV头
ts int64 // 当前时间戳
tsOffset int64 // 时间戳偏移量,用于多文件连续播放
}
// writeFlvHeader 写入FLV文件头
func (w *flvMp4Writer) writeFlvHeader() error {
if w.hasWritten {
return nil
}
// 使用 FlvWriter 的 WriteHeader 方法
err := w.FlvWriter.WriteHeader(w.audioExtra != nil, w.videoExtra != nil) // 有音频和视频
if err != nil {
return err
}
w.hasWritten = true
if w.videoExtra != nil {
w.onVideoExtraData(w.videoExtra.CodecType, w.videoExtra.Data)
}
if w.audioExtra != nil {
w.onAudioExtraData(w.audioExtra.CodecType, w.audioExtra.Data)
}
return nil
}
// onVideoExtraData 处理视频序列头
func (w *flvMp4Writer) onVideoExtraData(codecType box.MP4_CODEC_TYPE, data []byte) error {
if !w.hasWritten {
w.videoExtra = &ExtraDataInfo{
CodecType: codecType,
Data: data,
}
return nil
}
switch codecType {
case box.MP4_CODEC_H264:
return w.WriteTag(flv.FLV_TAG_TYPE_VIDEO, uint32(w.ts), uint32(len(data)+5), []byte{(1 << 4) | 7, 0, 0, 0, 0}, data)
case box.MP4_CODEC_H265:
return w.WriteTag(flv.FLV_TAG_TYPE_VIDEO, uint32(w.ts), uint32(len(data)+5), []byte{0b1001_0000 | rtmp.PacketTypeSequenceStart, codec.FourCC_H265[0], codec.FourCC_H265[1], codec.FourCC_H265[2], codec.FourCC_H265[3]}, data)
default:
return fmt.Errorf("unsupported video codec: %v", codecType)
}
}
// onAudioExtraData 处理音频序列头
func (w *flvMp4Writer) onAudioExtraData(codecType box.MP4_CODEC_TYPE, data []byte) error {
if !w.hasWritten {
w.audioExtra = &ExtraDataInfo{
CodecType: codecType,
Data: data,
}
return nil
}
var flvCodec byte
switch codecType {
case box.MP4_CODEC_AAC:
flvCodec = 10 // AAC
case box.MP4_CODEC_G711A:
flvCodec = 7 // G.711 A-law
case box.MP4_CODEC_G711U:
flvCodec = 8 // G.711 μ-law
default:
return fmt.Errorf("unsupported audio codec: %v", codecType)
}
// 构建FLV音频标签 - 序列头
if flvCodec == 10 { // AAC 需要两个字节头部
return w.WriteTag(flv.FLV_TAG_TYPE_AUDIO, uint32(w.ts), uint32(len(data)+2), []byte{(flvCodec << 4) | (3 << 2) | (1 << 1) | 1, 0}, data)
} else {
return w.WriteTag(flv.FLV_TAG_TYPE_AUDIO, uint32(w.ts), uint32(len(data)+1), []byte{(flvCodec << 4) | (3 << 2) | (1 << 1) | 1}, data)
}
}
// onVideoSample 处理视频样本
func (w *flvMp4Writer) onVideoSample(codecType box.MP4_CODEC_TYPE, sample box.Sample) error {
if !w.hasWritten {
if err := w.writeFlvHeader(); err != nil {
return err
}
}
// 计算调整后的时间戳
w.ts = int64(sample.Timestamp) + w.tsOffset
timestamp := uint32(w.ts)
switch codecType {
case box.MP4_CODEC_H264:
frameType := byte(2) // P帧
if sample.KeyFrame {
frameType = 1 // I帧
}
return w.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(len(sample.Data)+5), []byte{(frameType << 4) | 7, 1, byte(sample.CTS >> 16), byte(sample.CTS >> 8), byte(sample.CTS)}, sample.Data)
case box.MP4_CODEC_H265:
// Enhanced RTMP格式用于H.265
var b0 byte = 0b1010_0000 // P帧标识
if sample.KeyFrame {
b0 = 0b1001_0000 // 关键帧标识
}
if sample.CTS == 0 {
// CTS为0时使用PacketTypeCodedFramesX5字节头
return w.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(len(sample.Data)+5), []byte{b0 | rtmp.PacketTypeCodedFramesX, codec.FourCC_H265[0], codec.FourCC_H265[1], codec.FourCC_H265[2], codec.FourCC_H265[3]}, sample.Data)
} else {
// CTS不为0时使用PacketTypeCodedFrames8字节头包含CTS
return w.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(len(sample.Data)+8), []byte{b0 | rtmp.PacketTypeCodedFrames, codec.FourCC_H265[0], codec.FourCC_H265[1], codec.FourCC_H265[2], codec.FourCC_H265[3], byte(sample.CTS >> 16), byte(sample.CTS >> 8), byte(sample.CTS)}, sample.Data)
}
default:
return fmt.Errorf("unsupported video codec: %v", codecType)
}
}
// onAudioSample 处理音频样本
func (w *flvMp4Writer) onAudioSample(codec box.MP4_CODEC_TYPE, sample box.Sample) error {
if !w.hasWritten {
if err := w.writeFlvHeader(); err != nil {
return err
}
}
// 计算调整后的时间戳
w.ts = int64(sample.Timestamp) + w.tsOffset
timestamp := uint32(w.ts)
var flvCodec byte
switch codec {
case box.MP4_CODEC_AAC:
flvCodec = 10 // AAC
case box.MP4_CODEC_G711A:
flvCodec = 7 // G.711 A-law
case box.MP4_CODEC_G711U:
flvCodec = 8 // G.711 μ-law
default:
return fmt.Errorf("unsupported audio codec: %v", codec)
}
// 构建FLV音频标签 - 音频帧
if flvCodec == 10 { // AAC 需要两个字节头部
return w.WriteTag(flv.FLV_TAG_TYPE_AUDIO, timestamp, uint32(len(sample.Data)+2), []byte{(flvCodec << 4) | (3 << 2) | (1 << 1) | 1, 1}, sample.Data)
} else {
// 对于非AAC编解码器如G.711),只需要一个字节头部
return w.WriteTag(flv.FLV_TAG_TYPE_AUDIO, timestamp, uint32(len(sample.Data)+1), []byte{(flvCodec << 4) | (3 << 2) | (1 << 1) | 1}, sample.Data)
}
}
// processFlvFiles 处理原生FLV文件
func (plugin *FLVPlugin) processFlvFiles(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
plugin.Info("Processing FLV files", "count", len(fileInfoList))

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.1
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: flv.proto
package pb
@@ -16,6 +16,7 @@ import (
pb "m7s.live/v5/pb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -26,26 +27,23 @@ const (
)
type ReqRecordList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ReqRecordList) Reset() {
*x = ReqRecordList{}
if protoimpl.UnsafeEnabled {
mi := &file_flv_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_flv_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqRecordList) String() string {
@@ -56,7 +54,7 @@ func (*ReqRecordList) ProtoMessage() {}
func (x *ReqRecordList) ProtoReflect() protoreflect.Message {
mi := &file_flv_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -121,24 +119,21 @@ func (x *ReqRecordList) GetMode() string {
}
type ReqRecordDelete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ReqRecordDelete) Reset() {
*x = ReqRecordDelete{}
if protoimpl.UnsafeEnabled {
mi := &file_flv_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_flv_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqRecordDelete) String() string {
@@ -149,7 +144,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_flv_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -201,86 +196,58 @@ func (x *ReqRecordDelete) GetRange() string {
var File_flv_proto protoreflect.FileDescriptor
var file_flv_proto_rawDesc = []byte{
0x0a, 0x09, 0x66, 0x6c, 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x66, 0x6c, 0x76,
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05,
0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e,
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61,
0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67,
0x65, 0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6d, 0x6f, 0x64, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54,
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x32, 0x98, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69,
0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x6c, 0x76, 0x2e, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67,
0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69,
0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x66, 0x6c, 0x76,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07, 0x43, 0x61, 0x74,
0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x67,
0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61,
0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f,
0x66, 0x6c, 0x76, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12,
0x62, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x66, 0x6c, 0x76, 0x2e,
0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a,
0x16, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22,
0x1f, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65,
0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d,
0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f,
0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x70, 0x62,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
const file_flv_proto_rawDesc = "" +
"\n" +
"\tflv.proto\x12\x03flv\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\fglobal.proto\"\xb7\x01\n" +
"\rReqRecordList\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x14\n" +
"\x05range\x18\x02 \x01(\tR\x05range\x12\x14\n" +
"\x05start\x18\x03 \x01(\tR\x05start\x12\x10\n" +
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
"\x04mode\x18\a \x01(\tR\x04mode\"\x91\x01\n" +
"\x0fReqRecordDelete\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x10\n" +
"\x03ids\x18\x02 \x03(\rR\x03ids\x12\x1c\n" +
"\tstartTime\x18\x03 \x01(\tR\tstartTime\x12\x18\n" +
"\aendTime\x18\x04 \x01(\tR\aendTime\x12\x14\n" +
"\x05range\x18\x05 \x01(\tR\x05range2\x9e\x02\n" +
"\x03api\x12]\n" +
"\x04List\x12\x12.flv.ReqRecordList\x1a\x1a.global.RecordResponseList\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/flv/api/list/{streamPath=**}\x12T\n" +
"\aCatalog\x12\x16.google.protobuf.Empty\x1a\x17.global.ResponseCatalog\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/flv/api/catalog\x12b\n" +
"\x06Delete\x12\x14.flv.ReqRecordDelete\x1a\x16.global.ResponseDelete\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/flv/api/delete/{streamPath=**}B\x1bZ\x19m7s.live/v5/plugin/flv/pbb\x06proto3"
var (
file_flv_proto_rawDescOnce sync.Once
file_flv_proto_rawDescData = file_flv_proto_rawDesc
file_flv_proto_rawDescData []byte
)
func file_flv_proto_rawDescGZIP() []byte {
file_flv_proto_rawDescOnce.Do(func() {
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(file_flv_proto_rawDescData)
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)))
})
return file_flv_proto_rawDescData
}
var file_flv_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_flv_proto_goTypes = []interface{}{
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
(*pb.ResponseList)(nil), // 3: global.ResponseList
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
var file_flv_proto_goTypes = []any{
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
(*pb.RecordResponseList)(nil), // 3: global.RecordResponseList
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
}
var file_flv_proto_depIdxs = []int32{
0, // 0: flv.api.List:input_type -> flv.ReqRecordList
2, // 1: flv.api.Catalog:input_type -> google.protobuf.Empty
1, // 2: flv.api.Delete:input_type -> flv.ReqRecordDelete
3, // 3: flv.api.List:output_type -> global.ResponseList
3, // 3: flv.api.List:output_type -> global.RecordResponseList
4, // 4: flv.api.Catalog:output_type -> global.ResponseCatalog
5, // 5: flv.api.Delete:output_type -> global.ResponseDelete
3, // [3:6] is the sub-list for method output_type
@@ -295,37 +262,11 @@ func file_flv_proto_init() {
if File_flv_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_flv_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqRecordList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_flv_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqRecordDelete); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_flv_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
@@ -336,7 +277,6 @@ func file_flv_proto_init() {
MessageInfos: file_flv_proto_msgTypes,
}.Build()
File_flv_proto = out.File
file_flv_proto_rawDesc = nil
file_flv_proto_goTypes = nil
file_flv_proto_depIdxs = nil
}

View File

@@ -8,7 +8,7 @@ package flv;
option go_package="m7s.live/v5/plugin/flv/pb";
service api {
rpc List (ReqRecordList) returns (global.ResponseList) {
rpc List (ReqRecordList) returns (global.RecordResponseList) {
option (google.api.http) = {
get: "/flv/api/list/{streamPath=**}"
};

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// source: flv.proto
package pb
@@ -17,14 +17,20 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Api_List_FullMethodName = "/flv.api/List"
Api_Catalog_FullMethodName = "/flv.api/Catalog"
Api_Delete_FullMethodName = "/flv.api/Delete"
)
// ApiClient is the client API for Api service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ApiClient interface {
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error)
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error)
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error)
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
}
@@ -37,9 +43,10 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
out := new(pb.ResponseList)
err := c.cc.Invoke(ctx, "/flv.api/List", in, out, opts...)
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.RecordResponseList)
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -47,8 +54,9 @@ func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.Ca
}
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseCatalog)
err := c.cc.Invoke(ctx, "/flv.api/Catalog", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -56,8 +64,9 @@ func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc
}
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseDelete)
err := c.cc.Invoke(ctx, "/flv.api/Delete", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -66,19 +75,22 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
// ApiServer is the server API for Api service.
// All implementations must embed UnimplementedApiServer
// for forward compatibility
// for forward compatibility.
type ApiServer interface {
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
mustEmbedUnimplementedApiServer()
}
// UnimplementedApiServer must be embedded to have forward compatible implementations.
type UnimplementedApiServer struct {
}
// UnimplementedApiServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedApiServer struct{}
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
@@ -88,6 +100,7 @@ func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.Res
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
func (UnimplementedApiServer) testEmbeddedByValue() {}
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ApiServer will
@@ -97,6 +110,13 @@ type UnsafeApiServer interface {
}
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
// If the following call pancis, it indicates UnimplementedApiServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Api_ServiceDesc, srv)
}
@@ -110,7 +130,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/List",
FullMethod: Api_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
@@ -128,7 +148,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Catalog",
FullMethod: Api_Catalog_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
@@ -146,7 +166,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Delete",
FullMethod: Api_Delete_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))

View File

@@ -153,6 +153,7 @@ var CustomFileName = func(job *m7s.RecordJob) string {
}
func (r *Recorder) createStream(start time.Time) (err error) {
r.RecordJob.RecConf.Type = "flv"
return r.CreateStream(start, CustomFileName)
}

View File

@@ -12,11 +12,11 @@ import (
m7s "m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
hls "m7s.live/v5/plugin/hls/pkg"
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
mp4 "m7s.live/v5/plugin/mp4/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
)
// requestParams 包含请求解析后的参数
@@ -186,52 +186,77 @@ func (plugin *HLSPlugin) processMp4ToTs(w http.ResponseWriter, r *http.Request,
w.Header().Set("Content-Type", "video/mp2t")
w.Header().Set("Content-Disposition", "attachment")
// 创建一个TS写入器在循环外面所有MP4文件共享同一个TsInMemory
tsWriter := &simpleTsWriter{
TsInMemory: &hls.TsInMemory{},
plugin: plugin,
// 创建MP4流列表
var mp4Streams []m7s.RecordStream
for _, info := range fileInfoList {
plugin.Debug("Processing MP4 file", "path", info.filePath, "startTime", info.startTime, "endTime", info.endTime)
mp4Streams = append(mp4Streams, m7s.RecordStream{
FilePath: info.filePath,
StartTime: info.startTime,
EndTime: info.endTime,
Type: info.recordType,
})
}
// 对于MP4到TS的转换我们采用简化的方法
// 直接将每个MP4文件转换输出
for _, info := range fileInfoList {
if r.Context().Err() != nil {
return
}
plugin.Debug("Converting MP4 file to TS", "path", info.filePath)
// 创建MP4解复用器
demuxer := &mp4.DemuxerRange{
// 创建DemuxerConverterRange进行MP4解复用和转换
demuxer := &mp4.DemuxerConverterRange[*pkg.ADTS, *pkg.AnnexB]{
DemuxerRange: mp4.DemuxerRange{
StartTime: params.startTime,
EndTime: params.endTime,
Streams: []m7s.RecordStream{{
FilePath: info.filePath,
StartTime: info.startTime,
EndTime: info.endTime,
Type: info.recordType,
}},
}
Streams: mp4Streams,
Logger: plugin.Logger.With("demuxer", "mp4_Ts"),
},
}
// 设置回调函数
demuxer.OnVideoExtraData = tsWriter.onVideoExtraData
demuxer.OnAudioExtraData = tsWriter.onAudioExtraData
demuxer.OnVideoSample = tsWriter.onVideoSample
demuxer.OnAudioSample = tsWriter.onAudioSample
// 执行解复用和转换
err := demuxer.Demux(r.Context())
if err != nil {
plugin.Error("MP4 to TS conversion failed", "err", err, "file", info.filePath)
if !tsWriter.hasWritten {
http.Error(w, "Conversion failed", http.StatusInternalServerError)
// 创建TS编码器状态
tsWriter := &hls.TsInMemory{}
hasWritten := false
// 写入PMT头的辅助函数
writePMTHeader := func() {
if !hasWritten {
var audio, video codec.FourCC
if demuxer.AudioTrack != nil && demuxer.AudioTrack.ICodecCtx != nil {
audio = demuxer.AudioTrack.ICodecCtx.FourCC()
}
return
if demuxer.VideoTrack != nil && demuxer.VideoTrack.ICodecCtx != nil {
video = demuxer.VideoTrack.ICodecCtx.FourCC()
}
tsWriter.WritePMTPacket(audio, video)
hasWritten = true
}
}
// 创建音频帧结构
audioFrame := mpegts.MpegtsPESFrame{
Pid: mpegts.PID_AUDIO,
}
// 创建视频帧结构
videoFrame := mpegts.MpegtsPESFrame{
Pid: mpegts.PID_VIDEO,
}
// 执行解复用和转换
err := demuxer.Demux(r.Context(),
func(audio *pkg.ADTS) error {
writePMTHeader()
// 写入音频帧
return tsWriter.WriteAudioFrame(audio, &audioFrame)
}, func(video *pkg.AnnexB) error {
writePMTHeader()
videoFrame.IsKeyFrame = demuxer.VideoTrack.Value.IDR
// 写入视频帧
return tsWriter.WriteVideoFrame(video, &videoFrame)
})
if err != nil {
plugin.Error("MP4 to TS conversion failed", "err", err)
if !hasWritten {
http.Error(w, "Conversion failed", http.StatusInternalServerError)
}
return
}
// 将所有累积的 TsInMemory 内容写入到响应
_, err := tsWriter.WriteTo(w)
w.WriteHeader(http.StatusOK)
_, err = tsWriter.WriteTo(w)
if err != nil {
plugin.Error("Failed to write TS data to response", "error", err)
return
@@ -240,291 +265,6 @@ func (plugin *HLSPlugin) processMp4ToTs(w http.ResponseWriter, r *http.Request,
plugin.Info("MP4 to TS conversion completed")
}
// simpleTsWriter 简化的TS写入器
type simpleTsWriter struct {
*hls.TsInMemory
plugin *HLSPlugin
hasWritten bool
spsData []byte
ppsData []byte
videoCodec box.MP4_CODEC_TYPE
audioCodec box.MP4_CODEC_TYPE
}
func (w *simpleTsWriter) WritePMT() {
// 初始化 TsInMemory 的 PMT
var videoCodec, audioCodec [4]byte
switch w.videoCodec {
case box.MP4_CODEC_H264:
copy(videoCodec[:], []byte("H264"))
case box.MP4_CODEC_H265:
copy(videoCodec[:], []byte("H265"))
}
switch w.audioCodec {
case box.MP4_CODEC_AAC:
copy(audioCodec[:], []byte("MP4A"))
}
w.WritePMTPacket(audioCodec, videoCodec)
w.hasWritten = true
}
// onVideoExtraData 处理视频序列头
func (w *simpleTsWriter) onVideoExtraData(codecType box.MP4_CODEC_TYPE, data []byte) error {
w.videoCodec = codecType
// 解析并存储SPS/PPS数据
if codecType == box.MP4_CODEC_H264 && len(data) > 0 {
if w.plugin != nil {
w.plugin.Debug("Processing H264 extra data", "size", len(data))
}
// 解析AVCC格式的extra data
if len(data) >= 8 {
// AVCC格式: configurationVersion(1) + AVCProfileIndication(1) + profile_compatibility(1) + AVCLevelIndication(1) +
// lengthSizeMinusOne(1) + numOfSequenceParameterSets(1) + ...
offset := 5 // 跳过前5个字节
if offset < len(data) {
// 读取SPS数量
numSPS := data[offset] & 0x1f
offset++
// 解析SPS
for i := 0; i < int(numSPS) && offset < len(data)-1; i++ {
if offset+1 >= len(data) {
break
}
spsLength := int(data[offset])<<8 | int(data[offset+1])
offset += 2
if offset+spsLength <= len(data) {
// 添加起始码并存储SPS
w.spsData = make([]byte, 4+spsLength)
copy(w.spsData[0:4], []byte{0x00, 0x00, 0x00, 0x01})
copy(w.spsData[4:], data[offset:offset+spsLength])
offset += spsLength
if w.plugin != nil {
w.plugin.Debug("Extracted SPS", "length", spsLength)
}
break // 只取第一个SPS
}
}
// 读取PPS数量
if offset < len(data) {
numPPS := data[offset]
offset++
// 解析PPS
for i := 0; i < int(numPPS) && offset < len(data)-1; i++ {
if offset+1 >= len(data) {
break
}
ppsLength := int(data[offset])<<8 | int(data[offset+1])
offset += 2
if offset+ppsLength <= len(data) {
// 添加起始码并存储PPS
w.ppsData = make([]byte, 4+ppsLength)
copy(w.ppsData[0:4], []byte{0x00, 0x00, 0x00, 0x01})
copy(w.ppsData[4:], data[offset:offset+ppsLength])
if w.plugin != nil {
w.plugin.Debug("Extracted PPS", "length", ppsLength)
}
break // 只取第一个PPS
}
}
}
}
}
}
return nil
}
// onAudioExtraData 处理音频序列头
func (w *simpleTsWriter) onAudioExtraData(codecType box.MP4_CODEC_TYPE, data []byte) error {
w.audioCodec = codecType
w.plugin.Debug("Processing audio extra data", "codec", codecType, "size", len(data))
return nil
}
// onVideoSample 处理视频样本
func (w *simpleTsWriter) onVideoSample(codecType box.MP4_CODEC_TYPE, sample box.Sample) error {
if !w.hasWritten {
w.WritePMT()
}
w.plugin.Debug("Processing video sample", "size", len(sample.Data), "keyFrame", sample.KeyFrame, "timestamp", sample.Timestamp)
// 转换AVCC格式到Annex-B格式
annexBData, err := w.convertAVCCToAnnexB(sample.Data, sample.KeyFrame)
if err != nil {
w.plugin.Error("Failed to convert AVCC to Annex-B", "error", err)
return err
}
if len(annexBData) == 0 {
w.plugin.Warn("Empty Annex-B data after conversion")
return nil
}
// 创建视频帧结构
videoFrame := mpegts.MpegtsPESFrame{
Pid: mpegts.PID_VIDEO,
IsKeyFrame: sample.KeyFrame,
}
// 创建 AnnexB 帧
annexBFrame := &pkg.AnnexB{
PTS: (time.Duration(sample.Timestamp) + time.Duration(sample.CTS)) * 90,
DTS: time.Duration(sample.Timestamp) * 90, // 对于MP4转换假设PTS=DTS
}
// 根据编解码器类型设置 Hevc 标志
if codecType == box.MP4_CODEC_H265 {
annexBFrame.Hevc = true
}
annexBFrame.AppendOne(annexBData)
// 使用 WriteVideoFrame 写入TS包
err = w.WriteVideoFrame(annexBFrame, &videoFrame)
if err != nil {
w.plugin.Error("Failed to write video frame", "error", err)
return err
}
return nil
}
// convertAVCCToAnnexB 将AVCC格式转换为Annex-B格式
func (w *simpleTsWriter) convertAVCCToAnnexB(avccData []byte, isKeyFrame bool) ([]byte, error) {
if len(avccData) == 0 {
return nil, fmt.Errorf("empty AVCC data")
}
var annexBBuffer []byte
// 如果是关键帧先添加SPS和PPS
if isKeyFrame {
if len(w.spsData) > 0 {
annexBBuffer = append(annexBBuffer, w.spsData...)
w.plugin.Debug("Added SPS to key frame", "spsSize", len(w.spsData))
}
if len(w.ppsData) > 0 {
annexBBuffer = append(annexBBuffer, w.ppsData...)
w.plugin.Debug("Added PPS to key frame", "ppsSize", len(w.ppsData))
}
}
// 解析AVCC格式的NAL单元
offset := 0
nalCount := 0
for offset < len(avccData) {
// AVCC格式4字节长度 + NAL数据
if offset+4 > len(avccData) {
break
}
// 读取NAL单元长度大端序
nalLength := int(avccData[offset])<<24 |
int(avccData[offset+1])<<16 |
int(avccData[offset+2])<<8 |
int(avccData[offset+3])
offset += 4
if nalLength <= 0 || offset+nalLength > len(avccData) {
w.plugin.Warn("Invalid NAL length", "length", nalLength, "remaining", len(avccData)-offset)
break
}
nalData := avccData[offset : offset+nalLength]
offset += nalLength
nalCount++
if len(nalData) > 0 {
nalType := nalData[0] & 0x1f
w.plugin.Debug("Converting NAL unit", "type", nalType, "length", nalLength)
// 添加起始码前缀
annexBBuffer = append(annexBBuffer, []byte{0x00, 0x00, 0x00, 0x01}...)
annexBBuffer = append(annexBBuffer, nalData...)
}
}
if nalCount == 0 {
return nil, fmt.Errorf("no NAL units found in AVCC data")
}
w.plugin.Debug("AVCC to Annex-B conversion completed",
"inputSize", len(avccData),
"outputSize", len(annexBBuffer),
"nalUnits", nalCount)
return annexBBuffer, nil
}
// onAudioSample 处理音频样本
func (w *simpleTsWriter) onAudioSample(codecType box.MP4_CODEC_TYPE, sample box.Sample) error {
if !w.hasWritten {
w.WritePMT()
}
w.plugin.Debug("Processing audio sample", "codec", codecType, "size", len(sample.Data), "timestamp", sample.Timestamp)
// 创建音频帧结构
audioFrame := mpegts.MpegtsPESFrame{
Pid: mpegts.PID_AUDIO,
}
// 根据编解码器类型处理音频数据
switch codecType {
case box.MP4_CODEC_AAC: // AAC
// 创建 ADTS 帧
adtsFrame := &pkg.ADTS{
DTS: time.Duration(sample.Timestamp) * 90,
}
// 将音频数据添加到帧中
copy(adtsFrame.NextN(len(sample.Data)), sample.Data)
// 使用 WriteAudioFrame 写入TS包
err := w.WriteAudioFrame(adtsFrame, &audioFrame)
if err != nil {
w.plugin.Error("Failed to write audio frame", "error", err)
return err
}
default:
// 对于非AAC音频暂时使用原来的PES包方式
pesPacket := mpegts.MpegTsPESPacket{
Header: mpegts.MpegTsPESHeader{
PacketStartCodePrefix: 0x000001,
StreamID: mpegts.STREAM_ID_AUDIO,
},
}
// 设置可选字段
pesPacket.Header.ConstTen = 0x80
pesPacket.Header.PtsDtsFlags = 0x80 // 只有PTS
pesPacket.Header.PesHeaderDataLength = 5
pesPacket.Header.Pts = uint64(sample.Timestamp)
pesPacket.Buffers = append(pesPacket.Buffers, sample.Data)
// 写入TS包
err := w.WritePESPacket(&audioFrame, pesPacket)
if err != nil {
w.plugin.Error("Failed to write audio PES packet", "error", err)
return err
}
}
return nil
}
// processTsFiles 处理原生TS文件拼接
func (plugin *HLSPlugin) processTsFiles(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
plugin.Info("Processing TS files", "count", len(fileInfoList))

View File

@@ -74,6 +74,9 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
}
query := r.URL.Query()
fileName := query.Get("streamPath")
if fileName == "" {
fileName = r.PathValue("streamPath")
}
waitTimeout, err := time.ParseDuration(query.Get("timeout"))
if err == nil {
config.Debug("request", "fileName", fileName, "timeout", waitTimeout)
@@ -114,6 +117,25 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
plBuffer.WriteString("#EXT-X-ENDLIST\n")
w.Write(plBuffer)
return
} else if recordType == "ts" {
playlist := hls.Playlist{
Version: 3,
Sequence: 0,
Targetduration: 10,
}
var plBuffer util.Buffer
playlist.Writer = &plBuffer
playlist.Init()
for i := startTime; i.Before(endTime); i = i.Add(10 * time.Second) {
playlist.WriteInf(hls.PlaylistInf{
Duration: 10,
URL: fmt.Sprintf("/hls/download/%s.ts?start=%d&end=%d", streamPath, i.Unix(), i.Add(10*time.Second).Unix()),
Title: i.Format(time.RFC3339),
})
}
plBuffer.WriteString("#EXT-X-ENDLIST\n")
w.Write(plBuffer)
return
}
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time`
config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records)
@@ -273,7 +295,7 @@ func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
} else {
http.ServeFileFS(w, r, zipReader, strings.TrimPrefix(r.URL.Path, "/hls.js"))
http.ServeFileFS(w, r, zipReader, r.URL.Path)
}
}

View File

@@ -35,6 +35,7 @@ var CustomFileName = func(job *m7s.RecordJob) string {
}
func (r *Recorder) createStream(start time.Time) (err error) {
r.RecordJob.RecConf.Type = "ts"
return r.CreateStream(start, CustomFileName)
}

View File

@@ -1,9 +1,11 @@
package mpegts
import (
"bytes"
"errors"
"fmt"
"io"
"m7s.live/v5/pkg/util"
)
@@ -179,50 +181,56 @@ func WritePSI(w io.Writer, pt uint32, psi MpegTsPSI, data []byte) (err error) {
return
}
cw := &util.Crc32Writer{W: w, Crc32: 0xffffffff}
// 使用buffer收集所有需要计算CRC32的数据
bw := &bytes.Buffer{}
// table id(8)
if err = util.WriteUint8ToByte(cw, tableId); err != nil {
if err = util.WriteUint8ToByte(bw, tableId); err != nil {
return
}
// sectionSyntaxIndicator(1) + zero(1) + reserved1(2) + sectionLength(12)
// sectionLength 前两个字节固定为00
// 1 0 11 sectionLength
if err = util.WriteUint16ToByte(cw, sectionSyntaxIndicatorAndSectionLength, true); err != nil {
if err = util.WriteUint16ToByte(bw, sectionSyntaxIndicatorAndSectionLength, true); err != nil {
return
}
// PAT TransportStreamID(16) or PMT ProgramNumber(16)
if err = util.WriteUint16ToByte(cw, transportStreamIdOrProgramNumber, true); err != nil {
if err = util.WriteUint16ToByte(bw, transportStreamIdOrProgramNumber, true); err != nil {
return
}
// reserved2(2) + versionNumber(5) + currentNextIndicator(1)
// 0x3 << 6 -> 1100 0000
// 0x3 << 6 | 1 -> 1100 0001
if err = util.WriteUint8ToByte(cw, versionNumberAndCurrentNextIndicator); err != nil {
if err = util.WriteUint8ToByte(bw, versionNumberAndCurrentNextIndicator); err != nil {
return
}
// sectionNumber(8)
if err = util.WriteUint8ToByte(cw, sectionNumber); err != nil {
if err = util.WriteUint8ToByte(bw, sectionNumber); err != nil {
return
}
// lastSectionNumber(8)
if err = util.WriteUint8ToByte(cw, lastSectionNumber); err != nil {
if err = util.WriteUint8ToByte(bw, lastSectionNumber); err != nil {
return
}
// data
if _, err = cw.Write(data); err != nil {
if _, err = bw.Write(data); err != nil {
return
}
// crc32
crc32 := util.BigLittleSwap(uint(cw.Crc32))
if err = util.WriteUint32ToByte(cw, uint32(crc32), true); err != nil {
// 写入PSI数据
if _, err = w.Write(bw.Bytes()); err != nil {
return
}
// 使用MPEG-TS CRC32算法计算CRC32
crc32 := GetCRC32(bw.Bytes())
if err = util.WriteUint32ToByte(w, crc32, true); err != nil {
return
}

View File

@@ -77,3 +77,100 @@ mp4:
此时如果有人订阅了 vod/test/123 流那么就会从数据库中查询streamPath 为 `live/test` 录制文件,并且根据拉流参数中的 start 参数筛选录制文件。
此时 123 就是某个订阅者的唯一标识。
## 拼装逻辑
```mermaid
sequenceDiagram
participant Client
participant Handler as download()
participant DB as Database
participant Muxer as MP4Muxer
participant File1 as RecordFile1
participant File2 as RecordFile2
participant FileN as RecordFileN
participant Writer as ResponseWriter
Client->>Handler: GET /download?start=xxx&end=xxx
Handler->>Handler: 解析时间范围参数
Handler->>DB: 查询时间范围内的录制文件
DB-->>Handler: 返回 streams[]
Handler->>Muxer: NewMuxer(flag)
Handler->>Muxer: CreateFTYPBox()
Note over Handler: 初始化变量lastTs, tsOffset, parts[], audioTrack, videoTrack
loop 遍历每个录制文件
Handler->>File1: os.Open(stream.FilePath)
File1-->>Handler: file handle
Handler->>File1: NewDemuxer(file)
Handler->>File1: demuxer.Demux()
File1-->>Handler: 解析完成
alt 第一个文件
Handler->>Handler: 处理开始时间偏移
loop 处理每个track
Handler->>Muxer: AddTrack(track.Cid)
Muxer-->>Handler: 新轨道
end
end
Note over Handler: 设置 tsOffset = lastTs
loop 处理每个样本 (RangeSample)
alt 最后一个文件 && 超出结束时间
Handler->>Handler: break (跳出循环)
else
Handler->>Handler: 创建 ContentPart
Handler->>Handler: 计算调整后时间戳
alt flag == 0 (常规MP4)
Handler->>Handler: 调整样本偏移量
Handler->>Muxer: AddSampleEntry(fixSample)
else flag == FLAG_FRAGMENT (分片MP4)
Handler->>File1: 读取样本数据
File1-->>Handler: sample.Data
Handler->>Muxer: CreateFlagment(track, sample)
Muxer-->>Handler: moof, mdat boxes
Handler->>Handler: 添加到 part.boxies
end
Handler->>Handler: 更新 lastTs
end
end
Handler->>Handler: 添加 part 到 parts[]
Handler->>File1: Close()
end
alt flag == 0 (常规MP4模式)
Handler->>Muxer: MakeMoov()
Muxer-->>Handler: moov box
Handler->>Handler: 计算总大小
Handler->>Writer: Set Content-Length header
Handler->>Writer: 调整样本偏移量
Handler->>Muxer: CreateBaseBox(MDAT)
Muxer-->>Handler: mdat box
Handler->>Writer: WriteTo(ftyp, moov, free, mdat header)
loop 写入所有内容片段
Handler->>Handler: part.Seek(part.Start)
Handler->>Writer: io.CopyN(writer, part.File, part.Size)
Handler->>Handler: part.Close()
end
else flag == FLAG_FRAGMENT (分片MP4模式)
Handler->>Handler: 组装所有 children boxes
Handler->>Handler: 计算总大小
Handler->>Writer: Set Content-Length header
Handler->>Writer: WriteTo(所有boxes)
loop 关闭所有文件
Handler->>Handler: part.Close()
end
end
Handler-->>Client: MP4文件流
```

View File

@@ -576,7 +576,7 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
return res, err
}
func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *pb.ResponseList, err error) {
func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
globalReq := &pb.ReqRecordList{
StreamPath: req.StreamPath,
Range: req.Range,
@@ -584,7 +584,6 @@ func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *p
End: req.End,
PageNum: req.PageNum,
PageSize: req.PageSize,
Mode: req.Mode,
Type: "mp4",
EventLevel: req.EventLevel,
}

View File

@@ -11,7 +11,6 @@ package plugin_mp4
import (
"bytes"
"fmt"
"image"
"io"
"net/http"
"os"
@@ -26,268 +25,6 @@ import (
"m7s.live/v5/plugin/mp4/pkg/box"
)
/*
根据时间范围提取视频片段
njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
*/
func (p *MP4Plugin) extractClipToFile(streamPath string, startTime, endTime time.Time, outputPath string) error {
if p.DB == nil {
return pkg.ErrNoDB
}
var flag mp4.Flag
if strings.HasSuffix(streamPath, ".fmp4") {
flag = mp4.FLAG_FRAGMENT
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
} else {
streamPath = strings.TrimSuffix(streamPath, ".mp4")
}
// 查询数据库获取符合条件的片段
queryRecord := m7s.RecordStream{
Type: "mp4",
}
var streams []m7s.RecordStream
p.DB.Where(&queryRecord).Find(&streams, "end_time>? AND start_time<? AND stream_path=?", startTime, endTime, streamPath)
if len(streams) == 0 {
return fmt.Errorf("no matching MP4 segments found")
}
// 创建输出文件
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %v", err)
}
defer outputFile.Close()
p.Info("extracting clip", "streamPath", streamPath, "start", startTime, "end", endTime, "output", outputPath)
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
n := ftyp.Size()
muxer.CurrentOffset = int64(n)
var lastTs, tsOffset int64
var parts []*ContentPart
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
mdatOffset := sampleOffset
var audioTrack, videoTrack *mp4.Track
var file *os.File
var moov box.IBox
streamCount := len(streams)
// Track ExtraData history for each track
type TrackHistory struct {
Track *mp4.Track
ExtraData []byte
}
var audioHistory, videoHistory []TrackHistory
addAudioTrack := func(track *mp4.Track) {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
t.SampleSize = track.SampleSize
t.SampleRate = track.SampleRate
t.ChannelCount = track.ChannelCount
if len(audioHistory) > 0 {
t.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
}
audioTrack = t
audioHistory = append(audioHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
}
addVideoTrack := func(track *mp4.Track) {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
t.Width = track.Width
t.Height = track.Height
if len(videoHistory) > 0 {
t.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
}
videoTrack = t
videoHistory = append(videoHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
}
addTrack := func(track *mp4.Track) {
var lastAudioTrack, lastVideoTrack *TrackHistory
if len(audioHistory) > 0 {
lastAudioTrack = &audioHistory[len(audioHistory)-1]
}
if len(videoHistory) > 0 {
lastVideoTrack = &videoHistory[len(videoHistory)-1]
}
if track.Cid.IsAudio() {
if lastAudioTrack == nil {
addAudioTrack(track)
} else if !bytes.Equal(lastAudioTrack.ExtraData, track.ExtraData) {
for _, history := range audioHistory {
if bytes.Equal(history.ExtraData, track.ExtraData) {
audioTrack = history.Track
audioTrack.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
return
}
}
addAudioTrack(track)
}
} else if track.Cid.IsVideo() {
if lastVideoTrack == nil {
addVideoTrack(track)
} else if !bytes.Equal(lastVideoTrack.ExtraData, track.ExtraData) {
for _, history := range videoHistory {
if bytes.Equal(history.ExtraData, track.ExtraData) {
videoTrack = history.Track
videoTrack.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
return
}
}
addVideoTrack(track)
}
}
}
// 处理每个片段
for i, stream := range streams {
tsOffset = lastTs
file, err = os.Open(stream.FilePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
}
defer file.Close()
p.Info("processing segment", "file", file.Name())
demuxer := mp4.NewDemuxer(file)
err = demuxer.Demux()
if err != nil {
return fmt.Errorf("demux error: %v", err)
}
trackCount := len(demuxer.Tracks)
if i == 0 || flag == mp4.FLAG_FRAGMENT {
for _, track := range demuxer.Tracks {
addTrack(track)
}
}
if trackCount != len(muxer.Tracks) {
if flag == mp4.FLAG_FRAGMENT {
moov = muxer.MakeMoov()
}
}
if i == 0 {
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
var startSample *box.Sample
if startSample, err = demuxer.SeekTimePreIDR(uint64(startTimestamp)); err != nil {
tsOffset = 0
continue
}
tsOffset = -int64(startSample.Timestamp)
}
var part *ContentPart
for track, sample := range demuxer.RangeSample {
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
break
}
if part == nil {
part = &ContentPart{
File: file,
Start: sample.Offset,
}
}
lastTs = int64(sample.Timestamp + uint32(tsOffset))
fixSample := *sample
fixSample.Timestamp += uint32(tsOffset)
if flag == 0 {
fixSample.Offset = sampleOffset + (fixSample.Offset - part.Start)
part.Size += sample.Size
if track.Cid.IsAudio() {
audioTrack.AddSampleEntry(fixSample)
} else if track.Cid.IsVideo() {
videoTrack.AddSampleEntry(fixSample)
}
} else {
part.Seek(sample.Offset, io.SeekStart)
fixSample.Data = make([]byte, sample.Size)
part.Read(fixSample.Data)
var moof, mdat box.IBox
if track.Cid.IsAudio() {
moof, mdat = muxer.CreateFlagment(audioTrack, fixSample)
} else if track.Cid.IsVideo() {
moof, mdat = muxer.CreateFlagment(videoTrack, fixSample)
}
if moof != nil {
part.boxies = append(part.boxies, moof, mdat)
part.Size += int(moof.Size() + mdat.Size())
}
}
}
if part != nil {
sampleOffset += int64(part.Size)
parts = append(parts, part)
}
}
// 写入输出文件
if flag == 0 {
moovSize := muxer.MakeMoov().Size()
dataSize := uint64(sampleOffset - mdatOffset)
// 调整sample偏移量
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
}
}
mdatBox := box.CreateBaseBox(box.TypeMDAT, dataSize+box.BasicBoxLen)
var freeBox *box.FreeBox
if mdatBox.HeaderSize() == box.BasicBoxLen {
freeBox = box.CreateFreeBox(nil)
}
// 写入文件头
_, err = box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return fmt.Errorf("failed to write header: %v", err)
}
// 写入媒体数据
for _, part := range parts {
part.Seek(part.Start, io.SeekStart)
_, err = io.CopyN(outputFile, part.File, int64(part.Size))
if err != nil {
return fmt.Errorf("failed to write media data: %v", err)
}
part.Close()
}
} else {
var children []box.IBox
children = append(children, ftyp, moov)
for _, part := range parts {
children = append(children, part.boxies...)
part.Close()
}
_, err = box.WriteTo(outputFile, children...)
if err != nil {
return fmt.Errorf("failed to write fragmented MP4: %v", err)
}
}
p.Info("clip saved successfully", "path", outputPath)
return nil
}
// bytes2hexStr 将字节数组前n个字节转为16进制字符串
// data: 原始字节数组
// length: 需要转换的字节数(超过实际长度时自动截断)
@@ -328,7 +65,7 @@ gopInterval=10
当gopSeconds=0.1 推算 gopInterval=1
当gopSeconds=0.2 推算 gopInterval=2
*/
func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime time.Time, outputPath string, gopSeconds float64, gopInterval int) error {
func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime time.Time, writer io.Writer, gopSeconds float64, gopInterval int) error {
if p.DB == nil {
return pkg.ErrNoDB
}
@@ -352,14 +89,10 @@ func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime
}
// 创建输出文件
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %v", err)
}
defer outputFile.Close()
outputFile := writer
p.Info("extracting compressed video", "streamPath", streamPath, "start", startTime, "end", endTime,
"output", outputPath, "gopSeconds", gopSeconds, "gopInterval", gopInterval)
"gopSeconds", gopSeconds, "gopInterval", gopInterval)
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
@@ -428,7 +161,7 @@ func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime
if startTimestamp < 0 {
startTimestamp = 0
}
startSample, err := demuxer.SeekTimePreIDR(uint64(startTimestamp))
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
tsOffset = -int64(startSample.Timestamp)
}
@@ -557,7 +290,7 @@ func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime
}
// 写入文件头
_, err = box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
_, err := box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return fmt.Errorf("failed to write header: %v", err)
}
@@ -582,13 +315,13 @@ func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime
children = append(children, moof, mdat)
}
_, err = box.WriteTo(outputFile, children...)
_, err := box.WriteTo(outputFile, children...)
if err != nil {
return fmt.Errorf("failed to write fragmented MP4: %v", err)
}
}
p.Info("compressed video saved", "path", outputPath,
p.Info("compressed video saved",
"originalDuration", (endTime.Sub(startTime)).Milliseconds(),
"compressedDuration", videoDuration,
"frameCount", len(filteredSamples),
@@ -604,7 +337,7 @@ outputPath=/opt/njtv/gop_tmp_1748620153000.mp4
原理根据时间戳找到最近的mp4文件再从mp4 文件中找到最近gop 生成mp4 文件
*/
func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, outputPath string) (float64, error) {
func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, writer io.Writer) (float64, error) {
if p.DB == nil {
return 0, pkg.ErrNoDB
}
@@ -628,14 +361,9 @@ func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, out
}
// 创建输出文件
outputFile, err := os.Create(outputPath)
if err != nil {
return 0, fmt.Errorf("failed to create output file: %v", err)
}
defer outputFile.Close()
outputFile := writer
p.Info("extracting compressed video", "streamPath", streamPath, "targetTime", targetTime,
"output", outputPath)
p.Info("extracting compressed video", "streamPath", streamPath, "targetTime", targetTime)
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
@@ -709,7 +437,7 @@ func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, out
startTimestamp = 0
}
//通过时间戳定位到最近的关键帧如视频IDR帧返回的startSample是该关键帧对应的样本
startSample, err := demuxer.SeekTimePreIDR(uint64(startTimestamp))
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
tsOffset = -int64(startSample.Timestamp)
}
@@ -831,7 +559,7 @@ func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, out
}
// 写入文件头
_, err = box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
_, err := box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return 0, fmt.Errorf("failed to write header: %v", err)
}
@@ -856,12 +584,12 @@ func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, out
children = append(children, moof, mdat)
}
_, err = box.WriteTo(outputFile, children...)
_, err := box.WriteTo(outputFile, children...)
if err != nil {
return 0, fmt.Errorf("failed to write fragmented MP4: %v", err)
}
}
p.Info("extract gop video saved", "path", outputPath,
p.Info("extract gop video saved",
"targetTime", targetTime,
"compressedDuration", videoDuration,
"gopElapsed", gopElapsed,
@@ -871,16 +599,100 @@ func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, out
}
/*
根据时间范围提取视频片段
njtv/glgc.mp4?
timest=1748620153000&
outputPath=/opt/njtv/gop_tmp_1748620153000.mp4
提取压缩视频
原理根据时间戳找到最近的mp4文件再从mp4 文件中找到最近gop 生成mp4 文件
GET http://192.168.0.238:8080/mp4/extract/compressed/
njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
gopSeconds=1&
gopInterval=1&
*/
func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Image, error) {
func (p *MP4Plugin) extractCompressedVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
// 合并多个 mp4
startTime, endTime, err := util.TimeRangeQueryParse(query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractCompressedVideoHandel", "streamPath", streamPath, "start", startTime, "end", endTime)
gopSeconds, _ := strconv.ParseFloat(query.Get("gopSeconds"), 64)
gopInterval, _ := strconv.Atoi(query.Get("gopInterval"))
if gopSeconds == 0 {
gopSeconds = 1
}
if gopInterval == 0 {
gopInterval = 1
}
// 设置响应头
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", "attachment; filename=\"compressed_video.mp4\"")
err = p.extractCompressedVideo(streamPath, startTime, endTime, w, gopSeconds, gopInterval)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (p *MP4Plugin) extractGopVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractGopVideoHandel", "streamPath", streamPath, "targetTime", targetTime)
// 设置响应头
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", "attachment; filename=\"gop_video.mp4\"")
_, err = p.extractGopVideo(streamPath, targetTime, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (p *MP4Plugin) snapHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("snapHandel", "streamPath", streamPath, "targetTime", targetTime)
// 设置响应头
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Disposition", "attachment; filename=\"snapshot.jpg\"")
err = p.snapToWriter(streamPath, targetTime, w)
if err != nil {
p.Info("snapHandel", "err", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (p *MP4Plugin) snapToWriter(streamPath string, targetTime time.Time, writer io.Writer) error {
if p.DB == nil {
return nil, pkg.ErrNoDB
return pkg.ErrNoDB
}
var flag mp4.Flag
@@ -898,7 +710,7 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
var streams []m7s.RecordStream
p.DB.Where(&queryRecord).Find(&streams, "end_time>=? AND start_time<=? AND stream_path=?", targetTime, targetTime, streamPath)
if len(streams) == 0 {
return nil, fmt.Errorf("no matching MP4 segments found")
return fmt.Errorf("no matching MP4 segments found")
}
muxer := mp4.NewMuxer(flag)
@@ -913,14 +725,13 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
// 压缩相关变量
findGOP := false
targetFrameInterval := 40 // 25fps对应的毫秒间隔 (1000/25=40ms)
var filteredSamples []box.Sample
var sampleIdx = 0
// 仅处理视频轨道
for _, stream := range streams {
file, err := os.Open(stream.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
return fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
}
defer file.Close()
@@ -956,31 +767,20 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
continue
}
//p.Info("extractGop", "SPS PPS", Bytes2HexStr(videoTrack.ExtraData, len(videoTrack.ExtraData)))
// 处理起始时间边界
var tsOffset int64
startTimestamp := targetTime.Sub(stream.StartTime).Milliseconds()
// p.Info("extractGop",
// "Timescale", videoTrack.Timescale,
// "targetTime", targetTime,
// "stream.StartTime", stream.StartTime,
// "startTimestamp", startTimestamp)
if startTimestamp < 0 {
startTimestamp = 0
}
//通过时间戳定位到最近的关键帧如视频IDR帧返回的startSample是该关键帧对应的样本
startSample, err := demuxer.SeekTimePreIDR(uint64(startTimestamp))
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
tsOffset = -int64(startSample.Timestamp)
}
// p.Info("extractGop", "startSample Timestamp",
// startSample.Timestamp)
// 处理样本
//RangeSample迭代的是当前时间范围内的所有样本可能包含非关键帧顺序取决于MP4文件中样本的物理存储顺序
for track, sample := range demuxer.RangeSample {
@@ -989,13 +789,6 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
}
if sample.Timestamp < startSample.Timestamp {
p.Info("extractGop", "KeyFrame", sample.KeyFrame,
"CTS", sample.CTS,
"Timestamp", sample.Timestamp,
"Offset", sample.Offset,
"Size", sample.Size,
"Duration", sample.Duration)
continue
}
//记录GOP内帧的序号没有考虑B帧的情况
@@ -1019,7 +812,6 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
if !findGOP {
continue
}
// 检查是否超过gopSeconds限制
// 确保样本数据有效
if sample.Size <= 0 || sample.Size > 10*1024*1024 { // 10MB限制
@@ -1038,14 +830,6 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
continue
}
// p.Info("extractGop", "KeyFrame", sample.KeyFrame,
// "CTS", sample.CTS,
// "Timestamp", sample.Timestamp,
// "Offset", sample.Offset,
// "Size", sample.Size,
// "Duration", sample.Duration,
// "Data", Bytes2HexStr(data, 32))
// 创建新的样本
newSample := box.Sample{
KeyFrame: sample.KeyFrame,
@@ -1062,10 +846,11 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
}
if len(filteredSamples) == 0 {
return nil, fmt.Errorf("no valid video samples found")
return fmt.Errorf("no valid video samples found")
}
// 按25fps重新计算时间戳
targetFrameInterval := 40 // 25fps对应的毫秒间隔 (1000/25=40ms)
for i := range filteredSamples {
filteredSamples[i].Timestamp = uint32(i * targetFrameInterval)
}
@@ -1076,134 +861,14 @@ func (p *MP4Plugin) snapImage(streamPath string, targetTime time.Time) (image.Im
"sampleIdx", sampleIdx,
"frameCount", len(filteredSamples))
img, err := ProcessWithFFmpeg(filteredSamples, sampleIdx, videoTrack)
err := ProcessWithFFmpeg(filteredSamples, sampleIdx, videoTrack, writer)
if err != nil {
return nil, err
return err
}
// 添加样本到轨道
p.Info("extract gop and snap saved",
"targetTime", targetTime,
"frameCount", len(filteredSamples))
return img, nil
}
/*
提取普通MP4视频
GET http://192.168.0.238:8080/mp4/extractClip/njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
*/
func (p *MP4Plugin) extractClipToFileHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
// 合并多个 mp4
startTime, endTime, err := util.TimeRangeQueryParse(query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractClipToFileHandel", "streamPath", streamPath, "start", startTime, "end", endTime)
outputPath := query.Get("outputPath")
p.extractClipToFile(streamPath, startTime, endTime, outputPath)
// 返回成功响应
w.WriteHeader(http.StatusOK)
}
/*
提取压缩视频
GET http://192.168.0.238:8080/mp4/extractCompressed/
njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
gopSeconds=1&
gopInterval=1&
*/
func (p *MP4Plugin) extractCompressedVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
// 合并多个 mp4
startTime, endTime, err := util.TimeRangeQueryParse(query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractClipToFileHandel", "streamPath", streamPath, "start", startTime, "end", endTime)
outputPath := query.Get("outputPath")
gopSeconds, _ := strconv.ParseFloat(query.Get("gopSeconds"), 64)
gopInterval, _ := strconv.Atoi(query.Get("gopInterval"))
if gopSeconds == 0 {
gopSeconds = 1
}
if gopInterval == 0 {
gopInterval = 1
}
p.extractCompressedVideo(streamPath, startTime, endTime, outputPath, gopSeconds, gopInterval)
// 返回成功响应
w.WriteHeader(http.StatusOK)
}
func (p *MP4Plugin) extractGopVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractGopVideoHandel", "streamPath", streamPath, "targetTime", targetTime)
outputPath := query.Get("outputPath")
p.extractGopVideo(streamPath, targetTime, outputPath)
// 返回成功响应
w.WriteHeader(http.StatusOK)
}
func (p *MP4Plugin) snapHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("snapHandel", "streamPath", streamPath, "targetTime", targetTime)
outputPath := query.Get("outputPath")
img, err := p.snapImage(streamPath, targetTime)
if err == nil {
//水印测试
// wImg, err := watermark.WatermarkTest(img)
// if err != nil {
// p.Info("watermarkTest", "err", err)
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
//saveAsJPG(wImg, outputPath)
saveAsJPG(img, outputPath)
} else {
p.Info("snapHandel", "err", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 返回成功响应
w.WriteHeader(http.StatusOK)
return nil
}

View File

@@ -76,11 +76,10 @@ var _ = m7s.InstallPlugin[MP4Plugin](m7s.PluginMeta{
func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/download/{streamPath...}": p.download,
"/extractClip/{streamPath...}": p.extractClipToFileHandel,
"/extractCompressed/{streamPath...}": p.extractCompressedVideoHandel,
"/extractGop/{streamPath...}": p.extractGopVideoHandel,
"/snap/{streamPath...}": p.snapHandel,
"/download/{streamPath...}": p.download,
"/extract/compressed/{streamPath...}": p.extractCompressedVideoHandel,
"/extract/gop/{streamPath...}": p.extractGopVideoHandel,
"/snap/{streamPath...}": p.snapHandel,
}
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v5.28.3
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: mp4.proto
package pb
@@ -587,123 +587,74 @@ func (x *ResponseStopRecord) GetData() uint64 {
var File_mp4_proto protoreflect.FileDescriptor
var file_mp4_proto_rawDesc = string([]byte{
0x0a, 0x09, 0x6d, 0x70, 0x34, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x6d, 0x70, 0x34,
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c, 0x6f,
0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd7, 0x01, 0x0a, 0x0d, 0x52, 0x65,
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x72,
0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x67,
0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67, 0x65,
0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18,
0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d,
0x6f, 0x64, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65,
0x76, 0x65, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61,
0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61,
0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x65, 0x66, 0x6f,
0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x66,
0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x12, 0x1e,
0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a,
0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x64,
0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a,
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04,
0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61,
0x74, 0x61, 0x22, 0x2f, 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74,
0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50,
0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53,
0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xc4, 0x04, 0x0a, 0x03,
0x61, 0x70, 0x69, 0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70,
0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a,
0x14, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07,
0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
0x17, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12,
0x12, 0x10, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c,
0x6f, 0x67, 0x12, 0x62, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d,
0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53,
0x74, 0x61, 0x72, 0x74, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14,
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73,
0x74, 0x61, 0x72, 0x74, 0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61,
0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x01, 0x2a, 0x22, 0x1e, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x63, 0x0a,
0x0a, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x2e, 0x6d, 0x70,
0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a,
0x17, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74,
0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22,
0x3a, 0x01, 0x2a, 0x22, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74,
0x6f, 0x70, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a,
0x2a, 0x7d, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76,
0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
const file_mp4_proto_rawDesc = "" +
"\n" +
"\tmp4.proto\x12\x03mp4\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\fglobal.proto\"\xd7\x01\n" +
"\rReqRecordList\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x14\n" +
"\x05range\x18\x02 \x01(\tR\x05range\x12\x14\n" +
"\x05start\x18\x03 \x01(\tR\x05start\x12\x10\n" +
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
"\x04mode\x18\a \x01(\tR\x04mode\x12\x1e\n" +
"\n" +
"eventLevel\x18\b \x01(\tR\n" +
"eventLevel\"\x91\x01\n" +
"\x0fReqRecordDelete\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x10\n" +
"\x03ids\x18\x02 \x03(\rR\x03ids\x12\x1c\n" +
"\tstartTime\x18\x03 \x01(\tR\tstartTime\x12\x18\n" +
"\aendTime\x18\x04 \x01(\tR\aendTime\x12\x14\n" +
"\x05range\x18\x05 \x01(\tR\x05range\"\x90\x02\n" +
"\x0eReqEventRecord\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x18\n" +
"\aeventId\x18\x02 \x01(\tR\aeventId\x12\x1c\n" +
"\teventName\x18\x03 \x01(\tR\teventName\x12&\n" +
"\x0ebeforeDuration\x18\x04 \x01(\tR\x0ebeforeDuration\x12$\n" +
"\rafterDuration\x18\x05 \x01(\tR\rafterDuration\x12\x1c\n" +
"\teventDesc\x18\x06 \x01(\tR\teventDesc\x12\x1e\n" +
"\n" +
"eventLevel\x18\a \x01(\tR\n" +
"eventLevel\x12\x1a\n" +
"\bfragment\x18\b \x01(\tR\bfragment\"W\n" +
"\x13ResponseEventRecord\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
"\x04data\x18\x03 \x01(\rR\x04data\"\x83\x01\n" +
"\x0eReqStartRecord\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x125\n" +
"\bfragment\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\bfragment\x12\x1a\n" +
"\bfilePath\x18\x03 \x01(\tR\bfilePath\"W\n" +
"\x13ResponseStartRecord\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
"\x04data\x18\x03 \x01(\x04R\x04data\"/\n" +
"\rReqStopRecord\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\"V\n" +
"\x12ResponseStopRecord\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
"\x04data\x18\x03 \x01(\x04R\x04data2\xca\x04\n" +
"\x03api\x12]\n" +
"\x04List\x12\x12.mp4.ReqRecordList\x1a\x1a.global.RecordResponseList\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/mp4/api/list/{streamPath=**}\x12T\n" +
"\aCatalog\x12\x16.google.protobuf.Empty\x1a\x17.global.ResponseCatalog\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/mp4/api/catalog\x12b\n" +
"\x06Delete\x12\x14.mp4.ReqRecordDelete\x1a\x16.global.ResponseDelete\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/mp4/api/delete/{streamPath=**}\x12\\\n" +
"\n" +
"EventStart\x12\x13.mp4.ReqEventRecord\x1a\x18.mp4.ResponseEventRecord\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/mp4/api/event/start\x12g\n" +
"\vStartRecord\x12\x13.mp4.ReqStartRecord\x1a\x18.mp4.ResponseStartRecord\")\x82\xd3\xe4\x93\x02#:\x01*\"\x1e/mp4/api/start/{streamPath=**}\x12c\n" +
"\n" +
"StopRecord\x12\x12.mp4.ReqStopRecord\x1a\x17.mp4.ResponseStopRecord\"(\x82\xd3\xe4\x93\x02\":\x01*\"\x1d/mp4/api/stop/{streamPath=**}B\x1bZ\x19m7s.live/v5/plugin/mp4/pbb\x06proto3"
var (
file_mp4_proto_rawDescOnce sync.Once
@@ -719,19 +670,19 @@ func file_mp4_proto_rawDescGZIP() []byte {
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_mp4_proto_goTypes = []any{
(*ReqRecordList)(nil), // 0: mp4.ReqRecordList
(*ReqRecordDelete)(nil), // 1: mp4.ReqRecordDelete
(*ReqEventRecord)(nil), // 2: mp4.ReqEventRecord
(*ResponseEventRecord)(nil), // 3: mp4.ResponseEventRecord
(*ReqStartRecord)(nil), // 4: mp4.ReqStartRecord
(*ResponseStartRecord)(nil), // 5: mp4.ResponseStartRecord
(*ReqStopRecord)(nil), // 6: mp4.ReqStopRecord
(*ResponseStopRecord)(nil), // 7: mp4.ResponseStopRecord
(*durationpb.Duration)(nil), // 8: google.protobuf.Duration
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
(*pb.ResponseList)(nil), // 10: global.ResponseList
(*pb.ResponseCatalog)(nil), // 11: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 12: global.ResponseDelete
(*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
(*ReqStopRecord)(nil), // 6: mp4.ReqStopRecord
(*ResponseStopRecord)(nil), // 7: mp4.ResponseStopRecord
(*durationpb.Duration)(nil), // 8: google.protobuf.Duration
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
(*pb.RecordResponseList)(nil), // 10: global.RecordResponseList
(*pb.ResponseCatalog)(nil), // 11: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 12: global.ResponseDelete
}
var file_mp4_proto_depIdxs = []int32{
8, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
@@ -741,7 +692,7 @@ var file_mp4_proto_depIdxs = []int32{
2, // 4: mp4.api.EventStart:input_type -> mp4.ReqEventRecord
4, // 5: mp4.api.StartRecord:input_type -> mp4.ReqStartRecord
6, // 6: mp4.api.StopRecord:input_type -> mp4.ReqStopRecord
10, // 7: mp4.api.List:output_type -> global.ResponseList
10, // 7: mp4.api.List:output_type -> global.RecordResponseList
11, // 8: mp4.api.Catalog:output_type -> global.ResponseCatalog
12, // 9: mp4.api.Delete:output_type -> global.ResponseDelete
3, // 10: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord

View File

@@ -330,7 +330,6 @@ func local_request_Api_StopRecord_0(ctx context.Context, marshaler runtime.Marsh
// UnaryRPC :call ApiServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -489,21 +488,21 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.NewClient(endpoint, opts...)
conn, err := grpc.DialContext(ctx, endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
@@ -521,7 +520,7 @@ func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.C
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "ApiClient" to call the correct interceptors. This client ignores the HTTP middlewares.
// "ApiClient" to call the correct interceptors.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {

View File

@@ -7,7 +7,7 @@ package mp4;
option go_package="m7s.live/v5/plugin/mp4/pb";
service api {
rpc List (ReqRecordList) returns (global.ResponseList) {
rpc List (ReqRecordList) returns (global.RecordResponseList) {
option (google.api.http) = {
get: "/mp4/api/list/{streamPath=**}"
};

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.3
// - protoc v5.29.3
// source: mp4.proto
package pb
@@ -33,7 +33,7 @@ const (
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ApiClient interface {
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error)
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error)
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error)
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error)
@@ -49,9 +49,9 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseList)
out := new(pb.RecordResponseList)
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
@@ -113,7 +113,7 @@ func (c *apiClient) StopRecord(ctx context.Context, in *ReqStopRecord, opts ...g
// All implementations must embed UnimplementedApiServer
// for forward compatibility.
type ApiServer interface {
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
EventStart(context.Context, *ReqEventRecord) (*ResponseEventRecord, error)
@@ -129,7 +129,7 @@ type ApiServer interface {
// pointer dereference when methods are called.
type UnimplementedApiServer struct{}
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {

139
plugin/mp4/pkg/audio.go Normal file
View File

@@ -0,0 +1,139 @@
package mp4
import (
"fmt"
"io"
"time"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pkg/box"
)
var _ pkg.IAVFrame = (*Audio)(nil)
type Audio struct {
box.Sample
allocator *util.ScalableMemoryAllocator
}
// GetAllocator implements pkg.IAVFrame.
func (a *Audio) GetAllocator() *util.ScalableMemoryAllocator {
return a.allocator
}
// SetAllocator implements pkg.IAVFrame.
func (a *Audio) SetAllocator(allocator *util.ScalableMemoryAllocator) {
a.allocator = allocator
}
// Parse implements pkg.IAVFrame.
func (a *Audio) Parse(t *pkg.AVTrack) error {
return nil
}
// ConvertCtx implements pkg.IAVFrame.
func (a *Audio) ConvertCtx(ctx codec.ICodecCtx) (codec.ICodecCtx, pkg.IAVFrame, error) {
// 返回基础编解码器上下文,不进行转换
return ctx.GetBase(), nil, nil
}
// Demux implements pkg.IAVFrame.
func (a *Audio) Demux(codecCtx codec.ICodecCtx) (any, error) {
if len(a.Data) == 0 {
return nil, fmt.Errorf("no audio data to demux")
}
// 创建内存对象
var result util.Memory
result.AppendOne(a.Data)
// 根据编解码器类型进行解复用
switch codecCtx.(type) {
case *codec.AACCtx:
// 对于 AAC直接返回原始数据
return result, nil
case *codec.PCMACtx, *codec.PCMUCtx:
// 对于 PCM 格式,直接返回原始数据
return result, nil
default:
// 对于其他格式,也直接返回原始数据
return result, nil
}
}
// Mux implements pkg.IAVFrame.
func (a *Audio) Mux(codecCtx codec.ICodecCtx, frame *pkg.AVFrame) {
// 从 AVFrame 复制数据到 MP4 Sample
a.KeyFrame = false // 音频帧通常不是关键帧
a.Timestamp = uint32(frame.Timestamp.Milliseconds())
a.CTS = uint32(frame.CTS.Milliseconds())
// 处理原始数据
if frame.Raw != nil {
switch rawData := frame.Raw.(type) {
case util.Memory: // 包括 pkg.AudioData (它是 util.Memory 的别名)
a.Data = rawData.ToBytes()
a.Size = len(a.Data)
case []byte:
// 直接复制字节数据
a.Data = rawData
a.Size = len(a.Data)
default:
// 对于其他类型,尝试转换为字节
a.Data = nil
a.Size = 0
}
} else {
a.Data = nil
a.Size = 0
}
}
// GetTimestamp implements pkg.IAVFrame.
func (a *Audio) GetTimestamp() time.Duration {
return time.Duration(a.Timestamp) * time.Millisecond
}
// GetCTS implements pkg.IAVFrame.
func (a *Audio) GetCTS() time.Duration {
return time.Duration(a.CTS) * time.Millisecond
}
// GetSize implements pkg.IAVFrame.
func (a *Audio) GetSize() int {
return a.Size
}
// Recycle implements pkg.IAVFrame.
func (a *Audio) Recycle() {
// 回收资源
if a.allocator != nil && a.Data != nil {
// 如果数据是通过分配器分配的,这里可以进行回收
// 由于我们使用的是复制的数据,这里暂时不需要特殊处理
}
a.Data = nil
a.Size = 0
a.KeyFrame = false
a.Timestamp = 0
a.CTS = 0
a.Offset = 0
a.Duration = 0
}
// String implements pkg.IAVFrame.
func (a *Audio) String() string {
return fmt.Sprintf("MP4Audio[ts:%d, cts:%d, size:%d]",
a.Timestamp, a.CTS, a.Size)
}
// Dump implements pkg.IAVFrame.
func (a *Audio) Dump(t byte, w io.Writer) {
// 输出数据到 writer
if a.Data != nil {
w.Write(a.Data)
}
}

View File

@@ -54,16 +54,8 @@ func (t *TrakBox) Unmarshal(buf []byte) (b IBox, err error) {
return t, err
}
// SampleCallback 定义样本处理回调函数类型
type SampleCallback func(sample *Sample, sampleIndex int) error
// ParseSamples parses the sample table and builds the sample list
func (t *TrakBox) ParseSamples() (samplelist []Sample) {
return t.ParseSamplesWithCallback(nil)
}
// ParseSamplesWithCallback parses the sample table and builds the sample list with optional callback
func (t *TrakBox) ParseSamplesWithCallback(callback SampleCallback) (samplelist []Sample) {
stbl := t.MDIA.MINF.STBL
var chunkOffsets []uint64
if stbl.STCO != nil {
@@ -158,17 +150,6 @@ func (t *TrakBox) ParseSamplesWithCallback(callback SampleCallback) (samplelist
}
}
// 调用回调函数处理每个样本
if callback != nil {
for i := range samplelist {
if err := callback(&samplelist[i], i); err != nil {
// 如果回调返回错误,可以选择记录或处理,但不中断解析
// 这里为了保持向后兼容性,我们继续处理
continue
}
}
}
return samplelist
}

View File

@@ -2,25 +2,31 @@ package mp4
import (
"context"
"log/slog"
"os"
"time"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pkg/box"
)
type DemuxerRange struct {
StartTime, EndTime time.Time
Streams []m7s.RecordStream
OnAudioExtraData func(codec box.MP4_CODEC_TYPE, data []byte) error
OnVideoExtraData func(codec box.MP4_CODEC_TYPE, data []byte) error
OnAudioSample func(codec box.MP4_CODEC_TYPE, sample box.Sample) error
OnVideoSample func(codec box.MP4_CODEC_TYPE, sample box.Sample) error
*slog.Logger
StartTime, EndTime time.Time
Streams []m7s.RecordStream
AudioTrack, VideoTrack *pkg.AVTrack
}
func (d *DemuxerRange) Demux(ctx context.Context) error {
func (d *DemuxerRange) Demux(ctx context.Context, onAudio func(*Audio) error, onVideo func(*Video) error) error {
var ts, tsOffset int64
allocator := util.NewScalableMemoryAllocator(1 << 10)
defer allocator.Recycle()
for _, stream := range d.Streams {
// 检查流的时间范围是否在指定范围内
if stream.EndTime.Before(d.StartTime) || stream.StartTime.After(d.EndTime) {
@@ -42,20 +48,84 @@ func (d *DemuxerRange) Demux(ctx context.Context) error {
// 处理每个轨道的额外数据 (序列头)
for _, track := range demuxer.Tracks {
switch track.Cid {
case box.MP4_CODEC_H264, box.MP4_CODEC_H265:
if d.OnVideoExtraData != nil {
err := d.OnVideoExtraData(track.Cid, track.ExtraData)
if err != nil {
return err
case box.MP4_CODEC_H264:
var h264Ctx codec.H264Ctx
h264Ctx.CodecData, err = h264parser.NewCodecDataFromAVCDecoderConfRecord(track.ExtraData)
if err == nil {
if d.VideoTrack == nil {
d.VideoTrack = &pkg.AVTrack{
ICodecCtx: &h264Ctx,
RingWriter: &pkg.RingWriter{
Ring: util.NewRing[pkg.AVFrame](1),
}}
d.VideoTrack.Logger = d.With("track", "video")
} else {
// 如果已经有视频轨道,使用现有的轨道
d.VideoTrack.ICodecCtx = &h264Ctx
}
}
case box.MP4_CODEC_AAC, box.MP4_CODEC_G711A, box.MP4_CODEC_G711U:
if d.OnAudioExtraData != nil {
err := d.OnAudioExtraData(track.Cid, track.ExtraData)
if err != nil {
return err
case box.MP4_CODEC_H265:
var h265Ctx codec.H265Ctx
h265Ctx.CodecData, err = h265parser.NewCodecDataFromAVCDecoderConfRecord(track.ExtraData)
if err == nil {
if d.VideoTrack == nil {
d.VideoTrack = &pkg.AVTrack{
ICodecCtx: &h265Ctx,
RingWriter: &pkg.RingWriter{
Ring: util.NewRing[pkg.AVFrame](1),
}}
d.VideoTrack.Logger = d.With("track", "video")
} else {
// 如果已经有视频轨道,使用现有的轨道
d.VideoTrack.ICodecCtx = &h265Ctx
}
}
case box.MP4_CODEC_AAC:
var aacCtx codec.AACCtx
aacCtx.CodecData, err = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(track.ExtraData)
if err == nil {
if d.AudioTrack == nil {
d.AudioTrack = &pkg.AVTrack{
ICodecCtx: &aacCtx,
RingWriter: &pkg.RingWriter{
Ring: util.NewRing[pkg.AVFrame](1),
}}
d.AudioTrack.Logger = d.With("track", "audio")
} else {
// 如果已经有音频轨道,使用现有的轨道
d.AudioTrack.ICodecCtx = &aacCtx
}
}
case box.MP4_CODEC_G711A:
if d.AudioTrack == nil {
d.AudioTrack = &pkg.AVTrack{
ICodecCtx: &codec.PCMACtx{
AudioCtx: codec.AudioCtx{
SampleRate: 8000,
Channels: 1,
SampleSize: 16,
},
},
RingWriter: &pkg.RingWriter{
Ring: util.NewRing[pkg.AVFrame](1),
}}
d.AudioTrack.Logger = d.With("track", "audio")
}
case box.MP4_CODEC_G711U:
if d.AudioTrack == nil {
d.AudioTrack = &pkg.AVTrack{
ICodecCtx: &codec.PCMUCtx{
AudioCtx: codec.AudioCtx{
SampleRate: 8000,
Channels: 1,
SampleSize: 16,
},
},
RingWriter: &pkg.RingWriter{
Ring: util.NewRing[pkg.AVFrame](1),
}}
d.AudioTrack.Logger = d.With("track", "audio")
}
}
}
@@ -101,21 +171,50 @@ func (d *DemuxerRange) Demux(ctx context.Context) error {
// 根据轨道类型调用相应的回调函数
switch track.Cid {
case box.MP4_CODEC_H264, box.MP4_CODEC_H265:
if d.OnVideoSample != nil {
err := d.OnVideoSample(track.Cid, sample)
if err != nil {
return err
}
if err := onVideo(&Video{
Sample: sample,
allocator: allocator,
}); err != nil {
return err
}
case box.MP4_CODEC_AAC, box.MP4_CODEC_G711A, box.MP4_CODEC_G711U:
if d.OnAudioSample != nil {
err := d.OnAudioSample(track.Cid, sample)
if err != nil {
return err
}
if err := onAudio(&Audio{
Sample: sample,
allocator: allocator,
}); err != nil {
return err
}
}
}
}
return nil
}
type DemuxerConverterRange[TA pkg.IAVFrame, TV pkg.IAVFrame] struct {
DemuxerRange
audioConverter *pkg.AVFrameConvert[TA]
videoConverter *pkg.AVFrameConvert[TV]
}
func (d *DemuxerConverterRange[TA, TV]) Demux(ctx context.Context, onAudio func(TA) error, onVideo func(TV) error) error {
d.DemuxerRange.Demux(ctx, func(audio *Audio) error {
if d.audioConverter == nil {
d.audioConverter = pkg.NewAVFrameConvert[TA](d.AudioTrack, nil)
}
target, err := d.audioConverter.Convert(audio)
if err == nil {
err = onAudio(target)
}
return err
}, func(video *Video) error {
if d.videoConverter == nil {
d.videoConverter = pkg.NewAVFrameConvert[TV](d.VideoTrack, nil)
}
target, err := d.videoConverter.Convert(video)
if err == nil {
err = onVideo(target)
}
return err
})
return nil
}

View File

@@ -6,10 +6,8 @@ import (
"slices"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pkg/box"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
. "m7s.live/v5/plugin/mp4/pkg/box"
)
type (
@@ -32,7 +30,7 @@ type (
Number uint32
CryptByteBlock uint8
SkipByteBlock uint8
PsshBoxes []*box.PsshBox
PsshBoxes []*PsshBox
}
SubSamplePattern struct {
BytesClear uint16
@@ -45,28 +43,16 @@ type (
chunkoffset uint64
}
RTMPFrame struct {
Frame any // 可以是 *rtmp.RTMPVideo 或 *rtmp.RTMPAudio
}
Demuxer struct {
reader io.ReadSeeker
Tracks []*Track
ReadSampleIdx []uint32
IsFragment bool
// pssh []*box.PsshBox
moov *box.MoovBox
mdat *box.MediaDataBox
// pssh []*PsshBox
moov *MoovBox
mdat *MediaDataBox
mdatOffset uint64
QuicTime bool
// 预生成的 RTMP 帧
RTMPVideoSequence *rtmp.RTMPVideo
RTMPAudioSequence *rtmp.RTMPAudio
RTMPFrames []RTMPFrame
// RTMP 帧生成配置
RTMPAllocator *util.ScalableMemoryAllocator
}
)
@@ -77,10 +63,6 @@ func NewDemuxer(r io.ReadSeeker) *Demuxer {
}
func (d *Demuxer) Demux() (err error) {
return d.DemuxWithAllocator(nil)
}
func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (err error) {
// decodeVisualSampleEntry := func() (offset int, err error) {
// var encv VisualSampleEntry
@@ -114,7 +96,7 @@ func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (e
// }
// return
// }
var b box.IBox
var b IBox
var offset uint64
for {
b, err = box.ReadFrom(d.reader)
@@ -125,59 +107,53 @@ func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (e
return err
}
offset += b.Size()
switch boxData := b.(type) {
case *box.FileTypeBox:
if slices.Contains(boxData.CompatibleBrands, [4]byte{'q', 't', ' ', ' '}) {
switch box := b.(type) {
case *FileTypeBox:
if slices.Contains(box.CompatibleBrands, [4]byte{'q', 't', ' ', ' '}) {
d.QuicTime = true
}
case *box.FreeBox:
case *box.MediaDataBox:
d.mdat = boxData
d.mdatOffset = offset - b.Size() + uint64(boxData.HeaderSize())
case *box.MoovBox:
if boxData.MVEX != nil {
case *FreeBox:
case *MediaDataBox:
d.mdat = box
d.mdatOffset = offset - b.Size() + uint64(box.HeaderSize())
case *MoovBox:
if box.MVEX != nil {
d.IsFragment = true
}
for _, trak := range boxData.Tracks {
for _, trak := range box.Tracks {
track := &Track{}
track.TrackId = trak.TKHD.TrackID
track.Duration = uint32(trak.TKHD.Duration)
track.Timescale = trak.MDIA.MDHD.Timescale
// 创建RTMP样本处理回调
var sampleCallback box.SampleCallback
if d.RTMPAllocator != nil {
sampleCallback = d.createRTMPSampleCallback(track, trak)
}
track.Samplelist = trak.ParseSamplesWithCallback(sampleCallback)
track.Samplelist = trak.ParseSamples()
if len(trak.MDIA.MINF.STBL.STSD.Entries) > 0 {
entryBox := trak.MDIA.MINF.STBL.STSD.Entries[0]
switch entry := entryBox.(type) {
case *box.AudioSampleEntry:
case *AudioSampleEntry:
switch entry.Type() {
case box.TypeMP4A:
track.Cid = box.MP4_CODEC_AAC
case box.TypeALAW:
track.Cid = box.MP4_CODEC_G711A
case box.TypeULAW:
track.Cid = box.MP4_CODEC_G711U
case box.TypeOPUS:
track.Cid = box.MP4_CODEC_OPUS
case TypeMP4A:
track.Cid = MP4_CODEC_AAC
case TypeALAW:
track.Cid = MP4_CODEC_G711A
case TypeULAW:
track.Cid = MP4_CODEC_G711U
case TypeOPUS:
track.Cid = MP4_CODEC_OPUS
}
track.SampleRate = entry.Samplerate
track.ChannelCount = uint8(entry.ChannelCount)
track.SampleSize = entry.SampleSize
switch extra := entry.ExtraData.(type) {
case *box.ESDSBox:
track.Cid, track.ExtraData = box.DecodeESDescriptor(extra.Data)
case *ESDSBox:
track.Cid, track.ExtraData = DecodeESDescriptor(extra.Data)
}
case *box.VisualSampleEntry:
track.ExtraData = entry.ExtraData.(*box.DataBox).Data
case *VisualSampleEntry:
track.ExtraData = entry.ExtraData.(*DataBox).Data
switch entry.Type() {
case box.TypeAVC1:
track.Cid = box.MP4_CODEC_H264
case box.TypeHVC1, box.TypeHEV1:
track.Cid = box.MP4_CODEC_H265
case TypeAVC1:
track.Cid = MP4_CODEC_H264
case TypeHVC1, TypeHEV1:
track.Cid = MP4_CODEC_H265
}
track.Width = uint32(entry.Width)
track.Height = uint32(entry.Height)
@@ -185,9 +161,9 @@ func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (e
}
d.Tracks = append(d.Tracks, track)
}
d.moov = boxData
case *box.MovieFragmentBox:
for _, traf := range boxData.TRAFs {
d.moov = box
case *MovieFragmentBox:
for _, traf := range box.TRAFs {
track := d.Tracks[traf.TFHD.TrackID-1]
track.defaultSize = traf.TFHD.DefaultSampleSize
track.defaultDuration = traf.TFHD.DefaultSampleDuration
@@ -195,7 +171,6 @@ func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (e
}
}
d.ReadSampleIdx = make([]uint32, len(d.Tracks))
// for _, track := range d.Tracks {
// if len(track.Samplelist) > 0 {
// track.StartDts = uint64(track.Samplelist[0].DTS) * 1000 / uint64(track.Timescale)
@@ -205,7 +180,7 @@ func (d *Demuxer) DemuxWithAllocator(allocator *util.ScalableMemoryAllocator) (e
return nil
}
func (d *Demuxer) SeekTime(dts uint64) (sample *box.Sample, err error) {
func (d *Demuxer) SeekTime(dts uint64) (sample *Sample, err error) {
var audioTrack, videoTrack *Track
for _, track := range d.Tracks {
if track.Cid.IsAudio() {
@@ -243,54 +218,6 @@ func (d *Demuxer) SeekTime(dts uint64) (sample *box.Sample, err error) {
return
}
/**
* @brief 函数跳帧到dts 前面的第一个关键帧位置
*
* @param 参数名dts 跳帧位置
*
* @todo 待实现的功能或改进点 audioTrack 没有同步改进
* @author erroot
* @date 250614
*
**/
func (d *Demuxer) SeekTimePreIDR(dts uint64) (sample *Sample, err error) {
var audioTrack, videoTrack *Track
for _, track := range d.Tracks {
if track.Cid.IsAudio() {
audioTrack = track
} else if track.Cid.IsVideo() {
videoTrack = track
}
}
if videoTrack != nil {
idx := videoTrack.SeekPreIDR(dts)
if idx == -1 {
return nil, errors.New("seek failed")
}
d.ReadSampleIdx[videoTrack.TrackId-1] = uint32(idx)
sample = &videoTrack.Samplelist[idx]
if audioTrack != nil {
for i, sample := range audioTrack.Samplelist {
if sample.Offset < int64(videoTrack.Samplelist[idx].Offset) {
continue
}
d.ReadSampleIdx[audioTrack.TrackId-1] = uint32(i)
break
}
}
} else if audioTrack != nil {
idx := audioTrack.Seek(dts)
if idx == -1 {
return nil, errors.New("seek failed")
}
d.ReadSampleIdx[audioTrack.TrackId-1] = uint32(idx)
sample = &audioTrack.Samplelist[idx]
} else {
return nil, pkg.ErrNoTrack
}
return
}
// func (d *Demuxer) decodeTRUN(trun *TrackRunBox) {
// dataOffset := trun.Dataoffset
// nextDts := d.currentTrack.StartDts
@@ -450,28 +377,25 @@ func (d *Demuxer) SeekTimePreIDR(dts uint64) (sample *Sample, err error) {
// return nil
// }
func (d *Demuxer) ReadSample(yield func(*Track, box.Sample) bool) {
func (d *Demuxer) ReadSample(yield func(*Track, Sample) bool) {
for {
maxdts := int64(-1)
minTsSample := box.Sample{Timestamp: uint32(maxdts)}
minTsSample := Sample{Timestamp: uint32(maxdts)}
var whichTrack *Track
whichTracki := 0
for i, track := range d.Tracks {
idx := d.ReadSampleIdx[i]
for _, track := range d.Tracks {
idx := d.ReadSampleIdx[track.TrackId-1]
if int(idx) == len(track.Samplelist) {
continue
}
if whichTrack == nil {
minTsSample = track.Samplelist[idx]
whichTrack = track
whichTracki = i
} else {
dts1 := uint64(minTsSample.Timestamp) * uint64(d.moov.MVHD.Timescale) / uint64(whichTrack.Timescale)
dts2 := uint64(track.Samplelist[idx].Timestamp) * uint64(d.moov.MVHD.Timescale) / uint64(track.Timescale)
if dts1 > dts2 {
minTsSample = track.Samplelist[idx]
whichTrack = track
whichTracki = i
}
}
// subSample := d.readSubSample(idx, whichTrack)
@@ -480,32 +404,29 @@ func (d *Demuxer) ReadSample(yield func(*Track, box.Sample) bool) {
return
}
d.ReadSampleIdx[whichTracki]++
d.ReadSampleIdx[whichTrack.TrackId-1]++
if !yield(whichTrack, minTsSample) {
return
}
}
}
func (d *Demuxer) RangeSample(yield func(*Track, *box.Sample) bool) {
func (d *Demuxer) RangeSample(yield func(*Track, *Sample) bool) {
for {
var minTsSample *box.Sample
var minTsSample *Sample
var whichTrack *Track
whichTracki := 0
for i, track := range d.Tracks {
idx := d.ReadSampleIdx[i]
for _, track := range d.Tracks {
idx := d.ReadSampleIdx[track.TrackId-1]
if int(idx) == len(track.Samplelist) {
continue
}
if whichTrack == nil {
minTsSample = &track.Samplelist[idx]
whichTrack = track
whichTracki = i
} else {
if minTsSample.Offset > track.Samplelist[idx].Offset {
minTsSample = &track.Samplelist[idx]
whichTrack = track
whichTracki = i
}
}
// subSample := d.readSubSample(idx, whichTrack)
@@ -513,7 +434,7 @@ func (d *Demuxer) RangeSample(yield func(*Track, *box.Sample) bool) {
if minTsSample == nil {
return
}
d.ReadSampleIdx[whichTracki]++
d.ReadSampleIdx[whichTrack.TrackId-1]++
if !yield(whichTrack, minTsSample) {
return
}
@@ -521,244 +442,6 @@ func (d *Demuxer) RangeSample(yield func(*Track, *box.Sample) bool) {
}
// GetMoovBox returns the Movie Box from the demuxer
func (d *Demuxer) GetMoovBox() *box.MoovBox {
func (d *Demuxer) GetMoovBox() *MoovBox {
return d.moov
}
// CreateRTMPSequenceFrame 创建 RTMP 序列帧
func (d *Demuxer) CreateRTMPSequenceFrame(track *Track, allocator *util.ScalableMemoryAllocator) (videoSeq *rtmp.RTMPVideo, audioSeq *rtmp.RTMPAudio, err error) {
switch track.Cid {
case box.MP4_CODEC_H264:
videoSeq = &rtmp.RTMPVideo{}
videoSeq.SetAllocator(allocator)
videoSeq.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
case box.MP4_CODEC_H265:
videoSeq = &rtmp.RTMPVideo{}
videoSeq.SetAllocator(allocator)
videoSeq.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
case box.MP4_CODEC_AAC:
audioSeq = &rtmp.RTMPAudio{}
audioSeq.SetAllocator(allocator)
audioSeq.Append([]byte{0xaf, 0x00}, track.ExtraData)
}
return
}
// ConvertSampleToRTMP 将 MP4 sample 转换为 RTMP 格式
func (d *Demuxer) ConvertSampleToRTMP(track *Track, sample box.Sample, allocator *util.ScalableMemoryAllocator, timestampOffset uint64) (videoFrame *rtmp.RTMPVideo, audioFrame *rtmp.RTMPAudio, err error) {
switch track.Cid {
case box.MP4_CODEC_H264:
videoFrame = &rtmp.RTMPVideo{}
videoFrame.SetAllocator(allocator)
videoFrame.CTS = sample.CTS
videoFrame.Timestamp = uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
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)
case box.MP4_CODEC_H265:
videoFrame = &rtmp.RTMPVideo{}
videoFrame.SetAllocator(allocator)
videoFrame.CTS = uint32(sample.CTS)
videoFrame.Timestamp = uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
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)
case box.MP4_CODEC_AAC:
audioFrame = &rtmp.RTMPAudio{}
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
audioFrame.AppendOne([]byte{0xaf, 0x01})
audioFrame.AddRecycleBytes(sample.Data)
case box.MP4_CODEC_G711A:
audioFrame = &rtmp.RTMPAudio{}
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
audioFrame.AppendOne([]byte{0x72})
audioFrame.AddRecycleBytes(sample.Data)
case box.MP4_CODEC_G711U:
audioFrame = &rtmp.RTMPAudio{}
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
audioFrame.AppendOne([]byte{0x82})
audioFrame.AddRecycleBytes(sample.Data)
}
return
}
// GetRTMPSequenceFrames 获取预生成的 RTMP 序列帧
func (d *Demuxer) GetRTMPSequenceFrames() (videoSeq *rtmp.RTMPVideo, audioSeq *rtmp.RTMPAudio) {
return d.RTMPVideoSequence, d.RTMPAudioSequence
}
// IterateRTMPFrames 迭代预生成的 RTMP 帧
func (d *Demuxer) IterateRTMPFrames(timestampOffset uint64, yield func(*RTMPFrame) bool) {
for i := range d.RTMPFrames {
frame := &d.RTMPFrames[i]
// 应用时间戳偏移
switch f := frame.Frame.(type) {
case *rtmp.RTMPVideo:
f.Timestamp += uint32(timestampOffset)
case *rtmp.RTMPAudio:
f.Timestamp += uint32(timestampOffset)
}
if !yield(frame) {
return
}
}
}
// GetMaxTimestamp 获取所有帧中的最大时间戳
func (d *Demuxer) GetMaxTimestamp() uint64 {
var maxTimestamp uint64
for _, frame := range d.RTMPFrames {
var timestamp uint64
switch f := frame.Frame.(type) {
case *rtmp.RTMPVideo:
timestamp = uint64(f.Timestamp)
case *rtmp.RTMPAudio:
timestamp = uint64(f.Timestamp)
}
if timestamp > maxTimestamp {
maxTimestamp = timestamp
}
}
return maxTimestamp
}
// generateRTMPFrames 生成RTMP序列帧和所有帧数据
func (d *Demuxer) generateRTMPFrames(allocator *util.ScalableMemoryAllocator) (err error) {
// 生成序列帧
for _, track := range d.Tracks {
if track.Cid.IsVideo() && d.RTMPVideoSequence == nil {
d.RTMPVideoSequence, _, err = d.CreateRTMPSequenceFrame(track, allocator)
if err != nil {
return err
}
} else if track.Cid.IsAudio() && d.RTMPAudioSequence == nil {
_, d.RTMPAudioSequence, err = d.CreateRTMPSequenceFrame(track, allocator)
if err != nil {
return err
}
}
}
// 预生成所有 RTMP 帧
d.RTMPFrames = make([]RTMPFrame, 0)
// 收集所有样本并按时间戳排序
type sampleInfo struct {
track *Track
sample box.Sample
sampleIndex uint32
trackIndex int
}
var allSamples []sampleInfo
for trackIdx, track := range d.Tracks {
for sampleIdx, sample := range track.Samplelist {
// 读取样本数据
if _, err = d.reader.Seek(sample.Offset, io.SeekStart); err != nil {
return err
}
sample.Data = allocator.Malloc(sample.Size)
if _, err = io.ReadFull(d.reader, sample.Data); err != nil {
allocator.Free(sample.Data)
return err
}
allSamples = append(allSamples, sampleInfo{
track: track,
sample: sample,
sampleIndex: uint32(sampleIdx),
trackIndex: trackIdx,
})
}
}
// 按时间戳排序样本
slices.SortFunc(allSamples, func(a, b sampleInfo) int {
timeA := uint64(a.sample.Timestamp) * uint64(d.moov.MVHD.Timescale) / uint64(a.track.Timescale)
timeB := uint64(b.sample.Timestamp) * uint64(d.moov.MVHD.Timescale) / uint64(b.track.Timescale)
if timeA < timeB {
return -1
} else if timeA > timeB {
return 1
}
return 0
})
// 预生成 RTMP 帧
for _, sampleInfo := range allSamples {
videoFrame, audioFrame, err := d.ConvertSampleToRTMP(sampleInfo.track, sampleInfo.sample, allocator, 0)
if err != nil {
return err
}
if videoFrame != nil {
d.RTMPFrames = append(d.RTMPFrames, RTMPFrame{Frame: videoFrame})
}
if audioFrame != nil {
d.RTMPFrames = append(d.RTMPFrames, RTMPFrame{Frame: audioFrame})
}
}
return nil
}
// createRTMPSampleCallback 创建RTMP样本处理回调函数
func (d *Demuxer) createRTMPSampleCallback(track *Track, trak *box.TrakBox) box.SampleCallback {
// 首先生成序列帧
if track.Cid.IsVideo() && d.RTMPVideoSequence == nil {
videoSeq, _, err := d.CreateRTMPSequenceFrame(track, d.RTMPAllocator)
if err == nil {
d.RTMPVideoSequence = videoSeq
}
} else if track.Cid.IsAudio() && d.RTMPAudioSequence == nil {
_, audioSeq, err := d.CreateRTMPSequenceFrame(track, d.RTMPAllocator)
if err == nil {
d.RTMPAudioSequence = audioSeq
}
}
return func(sample *box.Sample, sampleIndex int) error {
// 读取样本数据
if _, err := d.reader.Seek(sample.Offset, io.SeekStart); err != nil {
return err
}
sample.Data = d.RTMPAllocator.Malloc(sample.Size)
if _, err := io.ReadFull(d.reader, sample.Data); err != nil {
d.RTMPAllocator.Free(sample.Data)
return err
}
// 转换为 RTMP 格式
videoFrame, audioFrame, err := d.ConvertSampleToRTMP(track, *sample, d.RTMPAllocator, 0)
if err != nil {
return err
}
// 内部收集RTMP帧
if videoFrame != nil {
d.RTMPFrames = append(d.RTMPFrames, RTMPFrame{Frame: videoFrame})
}
if audioFrame != nil {
d.RTMPFrames = append(d.RTMPFrames, RTMPFrame{Frame: audioFrame})
}
return nil
}
}

View File

@@ -3,13 +3,16 @@ package mp4
import (
"errors"
"io"
"slices"
"strings"
"time"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
)
type HTTPReader struct {
@@ -34,40 +37,9 @@ func (p *HTTPReader) Run() (err error) {
content, err = io.ReadAll(p.ReadCloser)
demuxer = NewDemuxer(strings.NewReader(string(content)))
}
// 设置RTMP分配器以启用RTMP帧收集
demuxer.RTMPAllocator = allocator
if err = demuxer.DemuxWithAllocator(allocator); err != nil {
if err = demuxer.Demux(); err != nil {
return
}
// 获取demuxer内部收集的RTMP帧
rtmpFrames := demuxer.RTMPFrames
// 按时间戳排序所有帧
slices.SortFunc(rtmpFrames, func(a, b RTMPFrame) int {
var timeA, timeB uint64
switch f := a.Frame.(type) {
case *rtmp.RTMPVideo:
timeA = uint64(f.Timestamp)
case *rtmp.RTMPAudio:
timeA = uint64(f.Timestamp)
}
switch f := b.Frame.(type) {
case *rtmp.RTMPVideo:
timeB = uint64(f.Timestamp)
case *rtmp.RTMPAudio:
timeB = uint64(f.Timestamp)
}
if timeA < timeB {
return -1
} else if timeA > timeB {
return 1
}
return 0
})
publisher.OnSeek = func(seekTime time.Time) {
p.Stop(errors.New("seek"))
pullJob.Connection.Args.Set(util.StartKey, seekTime.Local().Format(util.LocalTimeFormat))
@@ -78,61 +50,98 @@ func (p *HTTPReader) Run() (err error) {
seekTime, _ := time.Parse(util.LocalTimeFormat, pullJob.Connection.Args.Get(util.StartKey))
demuxer.SeekTime(uint64(seekTime.UnixMilli()))
}
// 读取预生成的 RTMP 序列帧
videoSeq, audioSeq := demuxer.GetRTMPSequenceFrames()
if videoSeq != nil {
err = publisher.WriteVideo(videoSeq)
if err != nil {
return err
}
}
if audioSeq != nil {
err = publisher.WriteAudio(audioSeq)
if err != nil {
return err
for _, track := range demuxer.Tracks {
switch track.Cid {
case box.MP4_CODEC_H264:
var h264Ctx codec.H264Ctx
h264Ctx.CodecData, err = h264parser.NewCodecDataFromAVCDecoderConfRecord(track.ExtraData)
if err == nil {
publisher.SetCodecCtx(&h264Ctx, &Video{})
}
case box.MP4_CODEC_H265:
var h265Ctx codec.H265Ctx
h265Ctx.CodecData, err = h265parser.NewCodecDataFromAVCDecoderConfRecord(track.ExtraData)
if err == nil {
publisher.SetCodecCtx(&h265Ctx, &Video{
allocator: allocator,
})
}
case box.MP4_CODEC_AAC:
var aacCtx codec.AACCtx
aacCtx.CodecData, err = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(track.ExtraData)
if err == nil {
publisher.SetCodecCtx(&aacCtx, &Audio{
allocator: allocator,
})
}
}
}
// 计算最大时间戳用于累计偏移
var maxTimestamp uint64
for _, frame := range rtmpFrames {
var timestamp uint64
switch f := frame.Frame.(type) {
case *rtmp.RTMPVideo:
timestamp = uint64(f.Timestamp)
case *rtmp.RTMPAudio:
timestamp = uint64(f.Timestamp)
}
for track, sample := range demuxer.ReadSample {
timestamp := uint64(sample.Timestamp) * 1000 / uint64(track.Timescale)
if timestamp > maxTimestamp {
maxTimestamp = timestamp
}
}
var timestampOffset uint64
loop := p.PullJob.Loop
for {
// 使用预生成的 RTMP 帧进行播放
for _, frame := range rtmpFrames {
demuxer.ReadSampleIdx = make([]uint32, len(demuxer.Tracks))
for track, sample := range demuxer.ReadSample {
if p.IsStopped() {
return nil
return
}
// 应用时间戳偏移
switch f := frame.Frame.(type) {
case *rtmp.RTMPVideo:
f.Timestamp += uint32(timestampOffset)
err = publisher.WriteVideo(f)
case *rtmp.RTMPAudio:
f.Timestamp += uint32(timestampOffset)
err = publisher.WriteAudio(f)
if _, err = demuxer.reader.Seek(sample.Offset, io.SeekStart); err != nil {
return
}
if err != nil {
return err
sample.Data = allocator.Malloc(sample.Size)
if _, err = io.ReadFull(demuxer.reader, sample.Data); err != nil {
allocator.Free(sample.Data)
return
}
fixTimestamp := uint32(uint64(sample.Timestamp)*1000/uint64(track.Timescale) + timestampOffset)
switch track.Cid {
case box.MP4_CODEC_H264:
var videoFrame = Video{
Sample: sample,
allocator: allocator,
}
videoFrame.Timestamp = fixTimestamp
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_H265:
var videoFrame = Video{
Sample: sample,
allocator: allocator,
}
videoFrame.Timestamp = fixTimestamp
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_AAC:
var audioFrame = Audio{
Sample: sample,
allocator: allocator,
}
audioFrame.Timestamp = fixTimestamp
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711A:
var audioFrame = Audio{
Sample: sample,
allocator: allocator,
}
audioFrame.Timestamp = fixTimestamp
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711U:
var audioFrame = Audio{
Sample: sample,
allocator: allocator,
}
audioFrame.Sample = sample
audioFrame.SetAllocator(allocator)
audioFrame.Timestamp = fixTimestamp
err = publisher.WriteAudio(&audioFrame)
}
}
if loop >= 0 {
loop--
if loop == -1 {

View File

@@ -6,18 +6,14 @@ import (
m7s "m7s.live/v5"
"m7s.live/v5/pkg"
"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"
)
type (
RecordReader struct {
m7s.RecordFilePuller
demuxer *Demuxer
}
)
@@ -53,125 +49,8 @@ func (p *RecordReader) Run() (err error) {
var tsOffset int64 // 时间戳偏移量
// 创建可复用的 DemuxerRange 实例
demuxerRange := &DemuxerRange{}
// 设置音视频额外数据回调(序列头)
demuxerRange.OnVideoExtraData = func(codecType box.MP4_CODEC_TYPE, data []byte) error {
switch codecType {
case box.MP4_CODEC_H264:
var sequence rtmp.RTMPVideo
sequence.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, data)
err = publisher.WriteVideo(&sequence)
case box.MP4_CODEC_H265:
var sequence rtmp.RTMPVideo
sequence.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], data)
err = publisher.WriteVideo(&sequence)
}
return err
}
demuxerRange.OnAudioExtraData = func(codecType box.MP4_CODEC_TYPE, data []byte) error {
if codecType == box.MP4_CODEC_AAC {
var sequence rtmp.RTMPAudio
sequence.Append([]byte{0xaf, 0x00}, data)
err = publisher.WriteAudio(&sequence)
}
return err
}
// 设置视频样本回调
demuxerRange.OnVideoSample = func(codecType box.MP4_CODEC_TYPE, sample box.Sample) error {
if publisher.Paused != nil {
publisher.Paused.Await()
}
// 检查是否需要跳转
if needSeek, seekErr := p.CheckSeek(); seekErr != nil {
return seekErr
} else if needSeek {
return pkg.ErrSkip
}
// 简化的时间戳处理
if int64(sample.Timestamp)+tsOffset < 0 {
ts = 0
} else {
ts = int64(sample.Timestamp) + tsOffset
}
// 更新实时时间
realTime = time.Now() // 这里可以根据需要调整为更精确的时间计算
// 根据编解码器类型处理视频帧
switch codecType {
case box.MP4_CODEC_H264:
var videoFrame rtmp.RTMPVideo
videoFrame.CTS = sample.CTS
videoFrame.Timestamp = uint32(ts)
videoFrame.Append([]byte{util.Conditional[byte](sample.KeyFrame, 0x17, 0x27), 0x01, byte(videoFrame.CTS >> 24), byte(videoFrame.CTS >> 8), byte(videoFrame.CTS)}, sample.Data)
err = publisher.WriteVideo(&videoFrame)
case box.MP4_CODEC_H265:
var videoFrame rtmp.RTMPVideo
videoFrame.CTS = sample.CTS
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.AppendOne(sample.Data)
err = publisher.WriteVideo(&videoFrame)
}
return err
}
// 设置音频样本回调
demuxerRange.OnAudioSample = func(codecType box.MP4_CODEC_TYPE, sample box.Sample) error {
if publisher.Paused != nil {
publisher.Paused.Await()
}
// 检查是否需要跳转
if needSeek, seekErr := p.CheckSeek(); seekErr != nil {
return seekErr
} else if needSeek {
return pkg.ErrSkip
}
// 简化的时间戳处理
if int64(sample.Timestamp)+tsOffset < 0 {
ts = 0
} else {
ts = int64(sample.Timestamp) + tsOffset
}
// 根据编解码器类型处理音频帧
switch codecType {
case box.MP4_CODEC_AAC:
var audioFrame rtmp.RTMPAudio
audioFrame.Timestamp = uint32(ts)
audioFrame.Append([]byte{0xaf, 0x01}, sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711A:
var audioFrame rtmp.RTMPAudio
audioFrame.Timestamp = uint32(ts)
audioFrame.Append([]byte{0x72}, sample.Data)
err = publisher.WriteAudio(&audioFrame)
case box.MP4_CODEC_G711U:
var audioFrame rtmp.RTMPAudio
audioFrame.Timestamp = uint32(ts)
audioFrame.Append([]byte{0x82}, sample.Data)
err = publisher.WriteAudio(&audioFrame)
}
return err
demuxerRange := &DemuxerRange{
Logger: p.Logger.With("demuxer", "mp4"),
}
for loop := 0; loop < p.Loop; loop++ {
@@ -186,7 +65,56 @@ func (p *RecordReader) Run() (err error) {
} else {
demuxerRange.EndTime = time.Now()
}
if err = demuxerRange.Demux(p.Context); err != nil {
if err = demuxerRange.Demux(p.Context, func(a *Audio) error {
if !publisher.HasAudioTrack() {
publisher.SetCodecCtx(demuxerRange.AudioTrack.ICodecCtx, a)
}
if publisher.Paused != nil {
publisher.Paused.Await()
}
// 检查是否需要跳转
if needSeek, seekErr := p.CheckSeek(); seekErr != nil {
return seekErr
} else if needSeek {
return pkg.ErrSkip
}
// 简化的时间戳处理
if int64(a.Timestamp)+tsOffset < 0 {
ts = 0
} else {
ts = int64(a.Timestamp) + tsOffset
}
a.Timestamp = uint32(ts)
return publisher.WriteAudio(a)
}, func(v *Video) error {
if !publisher.HasVideoTrack() {
publisher.SetCodecCtx(demuxerRange.VideoTrack.ICodecCtx, v)
}
if publisher.Paused != nil {
publisher.Paused.Await()
}
// 检查是否需要跳转
if needSeek, seekErr := p.CheckSeek(); seekErr != nil {
return seekErr
} else if needSeek {
return pkg.ErrSkip
}
// 简化的时间戳处理
if int64(v.Timestamp)+tsOffset < 0 {
ts = 0
} else {
ts = int64(v.Timestamp) + tsOffset
}
// 更新实时时间
realTime = time.Now() // 这里可以根据需要调整为更精确的时间计算
v.Timestamp = uint32(ts)
return publisher.WriteVideo(v)
}); err != nil {
if err == pkg.ErrSkip {
loop--
continue

View File

@@ -13,7 +13,6 @@ import (
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/plugin/mp4/pkg/box"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
)
type WriteTrailerQueueTask struct {
@@ -136,7 +135,23 @@ var CustomFileName = func(job *m7s.RecordJob) string {
}
func (r *Recorder) createStream(start time.Time) (err error) {
return r.CreateStream(start, CustomFileName)
if r.RecordJob.RecConf.Type == "" {
r.RecordJob.RecConf.Type = "mp4"
}
err = r.CreateStream(start, CustomFileName)
if err != nil {
return
}
r.file, err = os.Create(r.Event.FilePath)
if err != nil {
return
}
if r.Event.Type == "fmp4" {
r.muxer = NewMuxerWithStreamPath(FLAG_FRAGMENT, r.Event.StreamPath)
} else {
r.muxer = NewMuxerWithStreamPath(0, r.Event.StreamPath)
}
return r.muxer.WriteInitSegment(r.file)
}
func (r *Recorder) Dispose() {
@@ -149,27 +164,7 @@ func (r *Recorder) Run() (err error) {
recordJob := &r.RecordJob
sub := recordJob.Subscriber
var audioTrack, videoTrack *Track
startTime := time.Now()
if recordJob.Event != nil {
startTime = startTime.Add(-time.Duration(recordJob.Event.BeforeDuration) * time.Millisecond)
}
err = r.createStream(startTime)
if err != nil {
return
}
r.file, err = os.Create(r.Event.FilePath)
if err != nil {
return
}
if recordJob.RecConf.Type == "fmp4" {
r.Event.Type = "fmp4"
r.muxer = NewMuxerWithStreamPath(FLAG_FRAGMENT, r.Event.StreamPath)
} else {
r.muxer = NewMuxerWithStreamPath(0, r.Event.StreamPath)
}
r.muxer.WriteInitSegment(r.file)
var at, vt *pkg.AVTrack
checkEventRecordStop := func(absTime uint32) (err error) {
if absTime >= recordJob.Event.AfterDuration+recordJob.Event.BeforeDuration {
r.RecordJob.Stop(task.ErrStopByUser)
@@ -177,19 +172,16 @@ func (r *Recorder) Run() (err error) {
return
}
checkFragment := func(absTime uint32) (err error) {
if duration := int64(absTime); time.Duration(duration)*time.Millisecond >= recordJob.RecConf.Fragment {
now := time.Now()
r.writeTailer(now)
err = r.createStream(now)
checkFragment := func(reader *pkg.AVRingReader) (err error) {
if duration := int64(reader.AbsTime); time.Duration(duration)*time.Millisecond >= recordJob.RecConf.Fragment {
r.writeTailer(reader.Value.WriteTime)
err = r.createStream(reader.Value.WriteTime)
if err != nil {
return
}
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()
@@ -198,7 +190,13 @@ func (r *Recorder) Run() (err error) {
return
}
return m7s.PlayBlock(sub, func(audio *pkg.RawAudio) error {
return m7s.PlayBlock(sub, func(audio *Audio) error {
if r.Event.StartTime.IsZero() {
err = r.createStream(sub.AudioReader.Value.WriteTime)
if err != nil {
return err
}
}
r.Event.Duration = sub.AudioReader.AbsTime
if sub.VideoReader == nil {
if recordJob.Event != nil {
@@ -208,7 +206,7 @@ func (r *Recorder) Run() (err error) {
}
}
if recordJob.RecConf.Fragment != 0 {
err := checkFragment(sub.AudioReader.AbsTime)
err := checkFragment(sub.AudioReader)
if err != nil {
return err
}
@@ -238,12 +236,16 @@ func (r *Recorder) Run() (err error) {
track.ChannelCount = uint8(ctx.Channels)
}
}
dts := sub.AudioReader.AbsTime
return r.muxer.WriteSample(r.file, audioTrack, box.Sample{
Data: audio.ToBytes(),
Timestamp: uint32(dts),
})
}, func(video *rtmp.RTMPVideo) error {
sample := audio.Sample
sample.Timestamp = uint32(sub.AudioReader.AbsTime)
return r.muxer.WriteSample(r.file, audioTrack, sample)
}, func(video *Video) error {
if r.Event.StartTime.IsZero() {
err = r.createStream(sub.VideoReader.Value.WriteTime)
if err != nil {
return err
}
}
r.Event.Duration = sub.VideoReader.AbsTime
if sub.VideoReader.Value.IDR {
if recordJob.Event != nil {
@@ -253,78 +255,53 @@ func (r *Recorder) Run() (err error) {
}
}
if recordJob.RecConf.Fragment != 0 {
err := checkFragment(sub.VideoReader.AbsTime)
err := checkFragment(sub.VideoReader)
if err != nil {
return err
}
}
}
offset := 5
bytes := video.ToBytes()
if vt == nil {
vt = sub.VideoReader.Track
ctx := vt.ICodecCtx.(pkg.IVideoCodecCtx)
width, height := uint32(ctx.Width()), uint32(ctx.Height())
switch ctx := vt.ICodecCtx.GetBase().(type) {
case *codec.H264Ctx:
track := r.muxer.AddTrack(box.MP4_CODEC_H264)
videoTrack = track
track.ExtraData = ctx.Record
track.Width = uint32(ctx.Width())
track.Height = uint32(ctx.Height())
track.Width = width
track.Height = height
case *codec.H265Ctx:
track := r.muxer.AddTrack(box.MP4_CODEC_H265)
videoTrack = track
track.ExtraData = ctx.Record
track.Width = uint32(ctx.Width())
track.Height = uint32(ctx.Height())
track.Width = width
track.Height = height
}
}
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()
}
}
ctx := vt.ICodecCtx.(pkg.IVideoCodecCtx)
width, height := uint32(ctx.Width()), uint32(ctx.Height())
if width != videoTrack.Width || height != videoTrack.Height {
r.Info("Video resolution changed, restarting recording",
"old", fmt.Sprintf("%dx%d", videoTrack.Width, videoTrack.Height),
"new", fmt.Sprintf("%dx%d", width, height))
r.writeTailer(sub.VideoReader.Value.WriteTime)
err = r.createStream(sub.VideoReader.Value.WriteTime)
if err != nil {
return nil
}
case *rtmp.H265Ctx:
if ctx.Enhanced {
switch t := bytes[0] & 0b1111; t {
case rtmp.PacketTypeCodedFrames:
offset += 3
case rtmp.PacketTypeSequenceStart:
return nil
case rtmp.PacketTypeCodedFramesX:
default:
r.Warn("unknown h265 packet type", "type", t)
return nil
}
} else if bytes[1] == 0 {
return nil
at, vt = nil, nil
if vr := sub.VideoReader; vr != nil {
vr.ResetAbsTime()
}
if ar := sub.AudioReader; ar != nil {
ar.ResetAbsTime()
}
}
return r.muxer.WriteSample(r.file, videoTrack, box.Sample{
KeyFrame: sub.VideoReader.Value.IDR,
Data: bytes[offset:],
Timestamp: uint32(sub.VideoReader.AbsTime),
CTS: video.CTS,
})
sample := video.Sample
sample.Timestamp = uint32(sub.VideoReader.AbsTime)
return r.muxer.WriteSample(r.file, videoTrack, sample)
})
}

View File

@@ -87,32 +87,8 @@ func (track *Track) makeElstBox() *EditListBox {
}
func (track *Track) Seek(dts uint64) int {
for i, sample := range track.Samplelist {
if sample.Timestamp*1000/uint32(track.Timescale) < uint32(dts) {
continue
} else if track.Cid.IsVideo() {
if sample.KeyFrame {
return i
}
} else {
return i
}
}
return -1
}
/**
* @brief 函数跳帧到dts 前面的第一个关键帧位置
*
* @param 参数名dts 跳帧位置
*
* @author erroot
* @date 250614
*
**/
func (track *Track) SeekPreIDR(dts uint64) int {
idx := 0
func (track *Track) Seek(dts uint64) (idx int) {
idx = -1
for i, sample := range track.Samplelist {
if track.Cid.IsVideo() && sample.KeyFrame {
idx = i
@@ -121,7 +97,7 @@ func (track *Track) SeekPreIDR(dts uint64) int {
break
}
}
return idx
return
}
func (track *Track) makeEdtsBox() *ContainerBox {

170
plugin/mp4/pkg/video.go Normal file
View File

@@ -0,0 +1,170 @@
package mp4
import (
"fmt"
"io"
"slices"
"time"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/mp4/pkg/box"
)
var _ pkg.IAVFrame = (*Video)(nil)
type Video struct {
box.Sample
allocator *util.ScalableMemoryAllocator
}
// GetAllocator implements pkg.IAVFrame.
func (v *Video) GetAllocator() *util.ScalableMemoryAllocator {
return v.allocator
}
// SetAllocator implements pkg.IAVFrame.
func (v *Video) SetAllocator(allocator *util.ScalableMemoryAllocator) {
v.allocator = allocator
}
// Parse implements pkg.IAVFrame.
func (v *Video) Parse(t *pkg.AVTrack) error {
t.Value.IDR = v.KeyFrame
return nil
}
// ConvertCtx implements pkg.IAVFrame.
func (v *Video) ConvertCtx(ctx codec.ICodecCtx) (codec.ICodecCtx, pkg.IAVFrame, error) {
// 返回基础编解码器上下文,不进行转换
return ctx.GetBase(), nil, nil
}
// Demux implements pkg.IAVFrame.
func (v *Video) Demux(codecCtx codec.ICodecCtx) (any, error) {
if len(v.Data) == 0 {
return nil, fmt.Errorf("no video data to demux")
}
// 创建内存读取器
var mem util.Memory
mem.AppendOne(v.Data)
reader := mem.NewReader()
var nalus pkg.Nalus
// 根据编解码器类型进行解复用
switch ctx := codecCtx.(type) {
case *codec.H264Ctx:
// 对于 H.264,解析 AVCC 格式的 NAL 单元
if err := nalus.ParseAVCC(reader, int(ctx.RecordInfo.LengthSizeMinusOne)+1); err != nil {
return nil, fmt.Errorf("failed to parse H.264 AVCC: %w", err)
}
case *codec.H265Ctx:
// 对于 H.265,解析 AVCC 格式的 NAL 单元
if err := nalus.ParseAVCC(reader, int(ctx.RecordInfo.LengthSizeMinusOne)+1); err != nil {
return nil, fmt.Errorf("failed to parse H.265 AVCC: %w", err)
}
default:
// 对于其他格式,尝试默认的 AVCC 解析4字节长度前缀
if err := nalus.ParseAVCC(reader, 4); err != nil {
return nil, fmt.Errorf("failed to parse AVCC with default settings: %w", err)
}
}
return nalus, nil
}
// Mux implements pkg.IAVFrame.
func (v *Video) Mux(codecCtx codec.ICodecCtx, frame *pkg.AVFrame) {
// 从 AVFrame 复制数据到 MP4 Sample
v.KeyFrame = frame.IDR
v.Timestamp = uint32(frame.Timestamp.Milliseconds())
v.CTS = uint32(frame.CTS.Milliseconds())
// 处理原始数据
if frame.Raw != nil {
switch rawData := frame.Raw.(type) {
case pkg.Nalus:
// 将 Nalus 转换为 AVCC 格式的字节数据
var buffer util.Buffer
// 根据编解码器类型确定 NALU 长度字段的大小
var naluSizeLen int = 4 // 默认使用 4 字节
switch ctx := codecCtx.(type) {
case *codec.H264Ctx:
naluSizeLen = int(ctx.RecordInfo.LengthSizeMinusOne) + 1
case *codec.H265Ctx:
naluSizeLen = int(ctx.RecordInfo.LengthSizeMinusOne) + 1
}
// 为每个 NALU 添加长度前缀
for _, nalu := range rawData {
util.PutBE(buffer.Malloc(naluSizeLen), nalu.Size) // 写入 NALU 长度
var buffers = slices.Clone(nalu.Buffers) // 克隆 NALU 的缓冲区
buffers.WriteTo(&buffer) // 直接写入 NALU 数据
}
v.Data = buffer
v.Size = len(v.Data)
case []byte:
// 直接复制字节数据
v.Data = rawData
v.Size = len(v.Data)
default:
// 对于其他类型,尝试转换为字节
v.Data = nil
v.Size = 0
}
} else {
v.Data = nil
v.Size = 0
}
}
// GetTimestamp implements pkg.IAVFrame.
func (v *Video) GetTimestamp() time.Duration {
return time.Duration(v.Timestamp) * time.Millisecond
}
// GetCTS implements pkg.IAVFrame.
func (v *Video) GetCTS() time.Duration {
return time.Duration(v.CTS) * time.Millisecond
}
// GetSize implements pkg.IAVFrame.
func (v *Video) GetSize() int {
return v.Size
}
// Recycle implements pkg.IAVFrame.
func (v *Video) Recycle() {
// 回收资源
if v.allocator != nil && v.Data != nil {
// 如果数据是通过分配器分配的,这里可以进行回收
// 由于我们使用的是复制的数据,这里暂时不需要特殊处理
}
v.Data = nil
v.Size = 0
v.KeyFrame = false
v.Timestamp = 0
v.CTS = 0
v.Offset = 0
v.Duration = 0
}
// String implements pkg.IAVFrame.
func (v *Video) String() string {
return fmt.Sprintf("MP4Video[ts:%d, cts:%d, size:%d, keyframe:%t]",
v.Timestamp, v.CTS, v.Size, v.KeyFrame)
}
// Dump implements pkg.IAVFrame.
func (v *Video) Dump(t byte, w io.Writer) {
// 输出数据到 writer
if v.Data != nil {
w.Write(v.Data)
}
}

View File

@@ -1,270 +1,42 @@
package plugin_mp4
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"image/jpeg"
"io"
"log"
"os"
"os/exec"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
mp4 "m7s.live/v5/plugin/mp4/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
)
func saveAsJPG(img image.Image, path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
opt := jpeg.Options{Quality: 90}
return jpeg.Encode(file, img, &opt)
}
func ExtractH264SPSPPS(extraData []byte) (sps, pps []byte, err error) {
if len(extraData) < 7 {
return nil, nil, fmt.Errorf("extradata too short")
}
// 解析 SPS 数量 (第6字节低5位)
spsCount := int(extraData[5] & 0x1F)
offset := 6 // 当前解析位置
// 提取 SPS
for i := 0; i < spsCount; i++ {
if offset+2 > len(extraData) {
return nil, nil, fmt.Errorf("invalid sps length")
}
spsLen := int(binary.BigEndian.Uint16(extraData[offset : offset+2]))
offset += 2
if offset+spsLen > len(extraData) {
return nil, nil, fmt.Errorf("sps data overflow")
}
sps = extraData[offset : offset+spsLen]
offset += spsLen
}
// 提取 PPS 数量
if offset >= len(extraData) {
return nil, nil, fmt.Errorf("missing pps count")
}
ppsCount := int(extraData[offset])
offset++
// 提取 PPS
for i := 0; i < ppsCount; i++ {
if offset+2 > len(extraData) {
return nil, nil, fmt.Errorf("invalid pps length")
}
ppsLen := int(binary.BigEndian.Uint16(extraData[offset : offset+2]))
offset += 2
if offset+ppsLen > len(extraData) {
return nil, nil, fmt.Errorf("pps data overflow")
}
pps = extraData[offset : offset+ppsLen]
offset += ppsLen
}
return sps, pps, nil
}
// 转换函数(支持动态插入参数集)
func ConvertAVCCH264ToAnnexB(data []byte, extraData []byte, isFirst *bool) ([]byte, error) {
var buf bytes.Buffer
pos := 0
for pos < len(data) {
if pos+4 > len(data) {
break
}
nalSize := binary.BigEndian.Uint32(data[pos : pos+4])
pos += 4
nalStart := pos
pos += int(nalSize)
if pos > len(data) {
break
}
nalu := data[nalStart:pos]
nalType := nalu[0] & 0x1F
// 关键帧前插入SPS/PPS仅需执行一次
if *isFirst && nalType == 5 {
sps, pps, err := ExtractH264SPSPPS(extraData)
if err != nil {
//panic(err)
return nil, err
}
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
buf.Write(sps)
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
buf.Write(pps)
//buf.Write(videoTrack.ExtraData)
*isFirst = false // 仅首帧插入
}
// 保留SEI单元类型6和所有其他单元
if nalType == 5 || nalType == 6 { // IDR/SEI用4字节起始码
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
} else {
buf.Write([]byte{0x00, 0x00, 0x01}) // 其他用3字节
}
buf.Write(nalu)
}
return buf.Bytes(), nil
}
/*
H.264与H.265的AVCC格式差异
VPS引入H.265新增视频参数集VPS用于描述多层编码、时序等信息
*/
// 提取H.265的VPS/SPS/PPSHEVCDecoderConfigurationRecord格式
func ExtractHEVCParams(extraData []byte) (vps, sps, pps []byte, err error) {
if len(extraData) < 22 {
return nil, nil, nil, errors.New("extra data too short")
}
// HEVC的extradata格式参考ISO/IEC 14496-15
offset := 22 // 跳过头部22字节
if offset+2 > len(extraData) {
return nil, nil, nil, errors.New("invalid extra data")
}
numOfArrays := int(extraData[offset])
offset++
for i := 0; i < numOfArrays; i++ {
if offset+3 > len(extraData) {
break
}
naluType := extraData[offset] & 0x3F
offset++
count := int(binary.BigEndian.Uint16(extraData[offset:]))
offset += 2
for j := 0; j < count; j++ {
if offset+2 > len(extraData) {
break
}
naluSize := int(binary.BigEndian.Uint16(extraData[offset:]))
offset += 2
if offset+naluSize > len(extraData) {
break
}
naluData := extraData[offset : offset+naluSize]
offset += naluSize
// 根据类型存储参数集
switch naluType {
case 32: // VPS
if vps == nil {
vps = make([]byte, len(naluData))
copy(vps, naluData)
}
case 33: // SPS
if sps == nil {
sps = make([]byte, len(naluData))
copy(sps, naluData)
}
case 34: // PPS
if pps == nil {
pps = make([]byte, len(naluData))
copy(pps, naluData)
}
}
}
}
if vps == nil || sps == nil || pps == nil {
return nil, nil, nil, errors.New("missing required parameter sets")
}
return vps, sps, pps, nil
}
// H.265的AVCC转Annex B
func ConvertAVCCHEVCToAnnexB(data []byte, extraData []byte, isFirst *bool) ([]byte, error) {
var buf bytes.Buffer
pos := 0
// 首帧插入VPS/SPS/PPS
if *isFirst {
vps, sps, pps, err := ExtractHEVCParams(extraData)
if err == nil {
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
buf.Write(vps)
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
buf.Write(sps)
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
buf.Write(pps)
} else {
return nil, err
}
}
// 处理NALU
for pos < len(data) {
if pos+4 > len(data) {
break
}
nalSize := binary.BigEndian.Uint32(data[pos : pos+4])
pos += 4
nalStart := pos
pos += int(nalSize)
if pos > len(data) {
break
}
nalu := data[nalStart:pos]
nalType := (nalu[0] >> 1) & 0x3F // H.265的NALU类型在头部的第2-7位
// 关键帧或参数集使用4字节起始码
if nalType == 19 || nalType == 20 || nalType >= 32 && nalType <= 34 {
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
} else {
buf.Write([]byte{0x00, 0x00, 0x01})
}
buf.Write(nalu)
}
return buf.Bytes(), nil
}
// ffmpeg -hide_banner -i gop.mp4 -vf "select=eq(n\,15)" -vframes 1 -f image2 -pix_fmt bgr24 output.bmp
func ProcessWithFFmpeg(samples []box.Sample, index int, videoTrack *mp4.Track) (image.Image, error) {
// code := "h264"
// if videoTrack.Cid == box.MP4_CODEC_H265 {
// code = "hevc"
// }
// ProcessWithFFmpeg 使用 FFmpeg 处理视频帧并生成截图
func ProcessWithFFmpeg(samples []box.Sample, index int, videoTrack *mp4.Track, output io.Writer) error {
// 创建ffmpeg命令直接输出JPEG格式
cmd := exec.Command("ffmpeg",
"-hide_banner",
//"-f", code, //"h264" 强制指定输入格式为H.264裸流
"-i", "pipe:0",
"-vf", fmt.Sprintf("select=eq(n\\,%d)", index),
"-vframes", "1",
"-pix_fmt", "bgr24",
"-f", "rawvideo",
"-f", "mjpeg",
"pipe:1")
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
return err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
return err
}
go func() {
errOutput, _ := io.ReadAll(stderr)
@@ -273,66 +45,55 @@ func ProcessWithFFmpeg(samples []box.Sample, index int, videoTrack *mp4.Track) (
if err = cmd.Start(); err != nil {
log.Printf("cmd.Start失败: %v", err)
return nil, err
return err
}
go func() {
defer stdin.Close()
isFirst := true
for _, sample := range samples {
if videoTrack.Cid == box.MP4_CODEC_H264 {
annexb, _ := ConvertAVCCH264ToAnnexB(sample.Data, videoTrack.ExtraData, &isFirst)
if _, err := stdin.Write(annexb); err != nil {
log.Printf("写入失败: %v", err)
break
}
} else {
annexb, _ := ConvertAVCCHEVCToAnnexB(sample.Data, videoTrack.ExtraData, &isFirst)
if _, err := stdin.Write(annexb); err != nil {
log.Printf("写入失败: %v", err)
break
}
convert := pkg.NewAVFrameConvert[*pkg.AnnexB](nil, nil)
switch videoTrack.Cid {
case box.MP4_CODEC_H264:
var h264Ctx codec.H264Ctx
h264Ctx.CodecData, err = h264parser.NewCodecDataFromAVCDecoderConfRecord(videoTrack.ExtraData)
if err != nil {
log.Printf("解析H264失败: %v", err)
return
}
convert.FromTrack.ICodecCtx = &h264Ctx
case box.MP4_CODEC_H265:
var h265Ctx codec.H265Ctx
h265Ctx.CodecData, err = h265parser.NewCodecDataFromAVCDecoderConfRecord(videoTrack.ExtraData)
if err != nil {
log.Printf("解析H265失败: %v", err)
return
}
convert.FromTrack.ICodecCtx = &h265Ctx
default:
log.Printf("不支持的编解码器: %v", videoTrack.Cid)
return
}
for _, sample := range samples {
annexb, err := convert.Convert(&mp4.Video{
Sample: sample,
})
if err != nil {
log.Printf("转换失败: %v", err)
continue
}
annexb.WriteTo(stdin)
}
}()
// 读取原始RGB数据
var buf bytes.Buffer
if _, err = io.Copy(&buf, stdout); err != nil {
// 从ffmpeg的stdout读取JPEG数据并写入到输出
if _, err = io.Copy(output, stdout); err != nil {
log.Printf("读取失败: %v", err)
return nil, err
return err
}
if err = cmd.Wait(); err != nil {
log.Printf("cmd.Wait失败: %v", err)
return nil, err
return err
}
//log.Printf("ffmpeg 提取成功: data size:%v", buf.Len())
// 转换为image.Image对象
data := buf.Bytes()
//width, height := parseBMPDimensions(data)
width := int(videoTrack.Width)
height := int(videoTrack.Height)
log.Printf("ffmpeg size: %v,%v", width, height)
//FFmpeg的 rawvideo 输出默认采用​​从上到下​​的扫描方式
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
//pos := (height-y-1)*width*3 + x*3
pos := (y*width + x) * 3 // 关键修复:按行顺序读取
img.Set(x, y, color.RGBA{
R: data[pos+2],
G: data[pos+1],
B: data[pos],
A: 255,
})
}
}
return img, nil
log.Printf("ffmpeg JPEG输出成功")
return nil
}

View File

@@ -46,7 +46,7 @@ func parseRGBA(rgba string) (color.RGBA, error) {
func (p *SnapPlugin) snap(publisher *m7s.Publisher, watermarkConfig *snap_pkg.WatermarkConfig) (*bytes.Buffer, error) {
// 获取视频帧
annexb, _, err := snap_pkg.GetVideoFrame(publisher, p.Server)
annexb, err := snap_pkg.GetVideoFrame(publisher, p.Server)
if err != nil {
return nil, err
}

View File

@@ -183,7 +183,7 @@ func (t *TimeSnapTask) GetTickInterval() time.Duration {
// Tick 执行定时截图操作
func (t *TimeSnapTask) Tick(any) {
// 获取视频帧
annexb, _, err := GetVideoFrame(t.job.OriginPublisher, t.job.Plugin.Server)
annexb, err := GetVideoFrame(t.job.OriginPublisher, t.job.Plugin.Server)
if err != nil {
t.Error("get video frame failed", "error", err.Error())
return

View File

@@ -10,45 +10,34 @@ import (
)
// GetVideoFrame 获取视频帧数据
func GetVideoFrame(publisher *m7s.Publisher, server *m7s.Server) ([]*pkg.AnnexB, *pkg.AVTrack, error) {
func GetVideoFrame(publisher *m7s.Publisher, server *m7s.Server) ([]*pkg.AnnexB, error) {
if publisher.VideoTrack.AVTrack == nil {
return nil, nil, pkg.ErrNotFound
return nil, pkg.ErrNotFound
}
// 等待视频就绪
if err := publisher.VideoTrack.WaitReady(); err != nil {
return nil, nil, err
return nil, err
}
// 创建读取器并等待 I 帧
reader := pkg.NewAVRingReader(publisher.VideoTrack.AVTrack, "snapshot")
if err := reader.StartRead(publisher.VideoTrack.GetIDR()); err != nil {
return nil, nil, err
return nil, err
}
defer reader.StopRead()
var track pkg.AVTrack
var annexb pkg.AnnexB
var err error
track.ICodecCtx, track.SequenceFrame, err = annexb.ConvertCtx(publisher.VideoTrack.ICodecCtx)
if err != nil {
return nil, nil, err
}
if track.ICodecCtx == nil {
return nil, nil, pkg.ErrUnsupportCodec
}
var converter = pkg.NewAVFrameConvert[*pkg.AnnexB](publisher.VideoTrack.AVTrack, nil)
var annexbList []*pkg.AnnexB
for lastFrameSequence := publisher.VideoTrack.AVTrack.LastValue.Sequence; reader.Value.Sequence <= lastFrameSequence; reader.ReadNext() {
if reader.Value.Raw == nil {
if err := reader.Value.Demux(publisher.VideoTrack.ICodecCtx); err != nil {
return nil, nil, err
}
annexb, err := converter.ConvertFromAVFrame(&reader.Value)
if err != nil {
return nil, err
}
var annexb pkg.AnnexB
annexb.Mux(track.ICodecCtx, &reader.Value)
annexbList = append(annexbList, &annexb)
annexbList = append(annexbList, annexb)
}
return annexbList, &track, nil
return annexbList, nil
}
// ProcessWithFFmpeg 使用 FFmpeg 处理视频帧并生成截图

View File

@@ -153,6 +153,8 @@ func (wsh *WebSocketHandler) Go() (err error) {
wsh.handleAnswer(signal)
case SignalTypeGetStreamList:
wsh.handleGetStreamList()
case SignalTypePing:
wsh.handlePing(signal)
default:
wsh.sendError("Unknown signal type: " + string(signal.Type))
}
@@ -161,7 +163,9 @@ func (wsh *WebSocketHandler) Go() (err error) {
// Dispose 清理资源
func (wsh *WebSocketHandler) Dispose() {
wsh.PeerConnection.Close()
if wsh.PeerConnection != nil {
wsh.PeerConnection.Close()
}
wsh.conn.Close()
}
@@ -190,6 +194,20 @@ func (wsh *WebSocketHandler) sendError(message string) error {
})
}
func (wsh *WebSocketHandler) handlePing(signal Signal) {
// 处理ping信号直接回复pong
if signal.Type == SignalTypePing {
wsh.Debug("Received ping, sending pong")
if err := wsh.sendJSON(Signal{
Type: SignalTypePong,
}); err != nil {
wsh.Error("Failed to send pong", "error", err)
}
} else {
wsh.sendError("Invalid signal type for ping: " + string(signal.Type))
}
}
// handlePublish 处理发布信号
func (wsh *WebSocketHandler) handlePublish(signal Signal) {
if publisher, err := wsh.config.Publish(wsh, signal.StreamPath); err == nil {

View File

@@ -13,6 +13,9 @@ const (
SignalTypeUnpublish SignalType = "unpublish"
SignalTypeAnswer SignalType = "answer"
SignalTypeGetStreamList SignalType = "getStreamList"
SignalTypePing SignalType = "ping"
SignalTypePong SignalType = "pong"
SignalTypeError SignalType = "error"
)
type Signal struct {

View File

@@ -314,6 +314,30 @@ func (p *Publisher) trackAdded() error {
return nil
}
func (p *Publisher) SetCodecCtx(ctx codec.ICodecCtx, data IAVFrame) {
if _, ok := ctx.(IAudioCodecCtx); ok {
t := p.AudioTrack.AVTrack
if t == nil {
t = NewAVTrack(data, p.Logger.With("track", "audio"), &p.Publish, p.audioReady, ctx)
p.AudioTrack.Set(t)
p.Call(p.trackAdded)
} else {
t.ICodecCtx = ctx
}
return
} else if _, ok := ctx.(IVideoCodecCtx); ok {
t := p.VideoTrack.AVTrack
if t == nil {
t = NewAVTrack(data, p.Logger.With("track", "video"), &p.Publish, p.videoReady, ctx)
p.VideoTrack.Set(t)
p.Call(p.trackAdded)
} else {
t.ICodecCtx = ctx
}
return
}
}
func (p *Publisher) WriteVideo(data IAVFrame) (err error) {
defer func() {
if err != nil {

View File

@@ -23,7 +23,6 @@ type (
// RecordEvent 包含录像事件的公共字段
EventRecordStream struct {
CreatedAt time.Time
*config.RecordEvent
RecordStream
}
@@ -53,6 +52,7 @@ type (
StreamPath string
AudioCodec string
VideoCodec string
CreatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" yaml:"-"`
}
)
@@ -204,9 +204,9 @@ type eventRecordCheck struct {
func (t *eventRecordCheck) Run() (err error) {
var eventRecordStreams []EventRecordStream
t.DB.Find(&eventRecordStreams, "type=? AND level=high AND stream_path=?", t.Type, t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
t.DB.Find(&eventRecordStreams, "type=? AND event_level=high AND stream_path=?", t.Type, t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
for _, recordStream := range eventRecordStreams {
t.DB.Model(&EventRecordStream{}).Where(`level=low AND start_time <= ? and end_time >= ?`, recordStream.EndTime, recordStream.StartTime).Update("level", config.EventLevelHigh)
t.DB.Model(&EventRecordStream{}).Where(`event_level=low AND start_time <= ? and end_time >= ?`, recordStream.EndTime, recordStream.StartTime).Update("event_level", config.EventLevelHigh)
}
return
}

View File

@@ -8,6 +8,7 @@ import random
import threading
import queue
import socket
import heapq
class PacketReplayer:
def __init__(self, pcap_file, target_ip, target_port):
@@ -18,31 +19,27 @@ class PacketReplayer:
self.response_queue = queue.Queue()
self.stop_reading = threading.Event()
self.socket = None
self.next_seq = None # 下一个期望的序列号
self.pending_packets = [] # 使用优先队列存储待发送的包
self.seen_packets = set() # 用于去重
self.initial_seq = None # 初始序列号
self.initial_ack = None # 初始确认号
self.client_ip = None # 客户端IP
self.client_port = None # 客户端端口
self.first_data_packet = True # 标记是否是第一个数据包
self.total_packets_sent = 0 # 发送的数据包数量
self.total_bytes_sent = 0 # 发送的总字节数
def establish_tcp_connection(self, src_port):
"""建立TCP连接"""
print(f"正在建立TCP连接 {self.target_ip}:{self.target_port}...")
try:
# 创建socket对象
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定源端口(如果指定了端口)
if src_port > 0:
try:
self.socket.bind(('0.0.0.0', src_port))
except socket.error as e:
print(f"指定端口 {src_port} 被占用,将使用随机端口")
self.socket.bind(('0.0.0.0', 0)) # 使用随机可用端口
else:
self.socket.bind(('0.0.0.0', 0)) # 使用随机可用端口
# 获取实际使用的端口
# 绑定源端口,让系统自动分配
self.socket.settimeout(5)
self.socket.connect((self.target_ip, self.target_port))
actual_port = self.socket.getsockname()[1]
print(f"使用本地端口: {actual_port}")
# 设置超时
self.socket.settimeout(5)
# 连接目标
self.socket.connect((self.target_ip, self.target_port))
print("TCP连接已建立")
return True
except Exception as e:
@@ -57,11 +54,9 @@ class PacketReplayer:
if IP not in packet:
return
# 检查源IP
if src_ip and packet[IP].src != src_ip:
return
# 检查协议和源端口
if protocol == 'tcp' and TCP in packet:
if src_port and packet[TCP].sport != src_port:
return
@@ -72,7 +67,7 @@ class PacketReplayer:
return
conn_id = (packet[IP].src, packet[UDP].sport)
self.connections[conn_id].append(packet)
elif not protocol: # 如果没有指定协议则包含所有IP包
elif not protocol:
if TCP in packet:
if src_port and packet[TCP].sport != src_port:
return
@@ -84,11 +79,97 @@ class PacketReplayer:
conn_id = (packet[IP].src, packet[UDP].sport)
self.connections[conn_id].append(packet)
def send_packet(self, packet, packet_count):
"""发送单个数据包,处理序列号"""
if TCP not in packet or IP not in packet:
return True
try:
# 检查是否是发送到目标端口的包
if packet[TCP].dport == self.target_port:
# 记录客户端信息
if self.client_ip is None:
self.client_ip = packet[IP].src
self.client_port = packet[TCP].sport
print(f"识别到客户端: {self.client_ip}:{self.client_port}")
# 获取TCP序列号和确认号
seq = packet[TCP].seq
ack = packet[TCP].ack
flags = packet[TCP].flags
# 打印数据包信息
print(f"[序号:{packet_count}] 处理数据包: src={packet[IP].src}:{packet[TCP].sport} -> dst={packet[IP].dst}:{packet[TCP].dport}, seq={seq}, ack={ack}, flags={flags}")
# 发送当前包
if Raw in packet:
# 如果是第一个数据包,记录初始序列号
if self.first_data_packet:
self.initial_seq = seq
self.next_seq = seq
self.first_data_packet = False
print(f"第一个数据包,初始序列号: {seq}")
# 如果是重传包,跳过
if seq in self.seen_packets:
print(f"跳过重传包,序列号: {seq}")
return True
# 如果序列号大于期望的序列号,将包放入待发送队列
if seq > self.next_seq:
print(f"包乱序,放入队列,序列号: {seq}, 期望序列号: {self.next_seq}")
heapq.heappush(self.pending_packets, (seq, packet))
return True
payload = packet[Raw].load
print(f"准备发送数据包,负载大小: {len(payload)} 字节")
self.socket.send(payload)
self.seen_packets.add(seq)
old_seq = self.next_seq
self.next_seq = self.next_seq + len(payload)
print(f"更新序列号: {old_seq} -> {self.next_seq}")
# 更新统计信息
self.total_packets_sent += 1
self.total_bytes_sent += len(payload)
# 检查并发送待发送队列中的包
while self.pending_packets and self.pending_packets[0][0] == self.next_seq:
_, next_packet = heapq.heappop(self.pending_packets)
if Raw in next_packet:
next_payload = next_packet[Raw].load
print(f"发送队列中的包,负载大小: {len(next_payload)} 字节")
self.socket.send(next_payload)
self.seen_packets.add(self.next_seq)
old_seq = self.next_seq
self.next_seq += len(next_payload)
print(f"更新序列号: {old_seq} -> {self.next_seq}")
# 更新统计信息
self.total_packets_sent += 1
self.total_bytes_sent += len(next_payload)
packet_time = time.strftime("%H:%M:%S", time.localtime(float(packet.time)))
print(f"[{packet_time}] [序号:{packet_count}] 已发送数据包 (序列号: {seq}, 负载大小: {len(payload)} 字节)")
else:
# 对于控制包,只记录到已处理集合
if flags & 0x02: # SYN
print(f"[序号:{packet_count}] 处理SYN包")
elif flags & 0x10: # ACK
print(f"[序号:{packet_count}] 处理ACK包")
else:
print(f"[序号:{packet_count}] 跳过无负载包")
else:
print(f"[序号:{packet_count}] 跳过非目标端口的包: src={packet[IP].src}:{packet[TCP].sport} -> dst={packet[IP].dst}:{packet[TCP].dport}")
return True
except Exception as e:
print(f"发送数据包 {packet_count} 时出错: {e}")
return False
def response_reader(self, src_port):
"""持续读取服务器响应的线程函数"""
while not self.stop_reading.is_set() and self.socket:
try:
# 使用socket接收数据
data = self.socket.recv(4096)
if data:
self.response_queue.put(data)
@@ -106,23 +187,19 @@ class PacketReplayer:
print(f"开始读取并重放数据包到 {self.target_ip}:{self.target_port}")
try:
# 使用PcapReader逐包读取
reader = PcapReader(self.pcap_file)
packet_count = 0
connection_established = False
# 读取并处理数据包
for packet in reader:
packet_count += 1
if IP not in packet:
continue
# 检查源IP
if src_ip and packet[IP].src != src_ip:
continue
# 检查协议和源端口
current_src_port = None
if protocol == 'tcp' and TCP in packet:
if src_port and packet[TCP].sport != src_port:
@@ -132,7 +209,7 @@ class PacketReplayer:
if src_port and packet[UDP].sport != src_port:
continue
current_src_port = packet[UDP].sport
elif not protocol: # 如果没有指定协议则包含所有IP包
elif not protocol:
if TCP in packet:
if src_port and packet[TCP].sport != src_port:
continue
@@ -146,37 +223,32 @@ class PacketReplayer:
else:
continue
# 找到第一个符合条件的包,建立连接
if not connection_established:
if not self.establish_tcp_connection(current_src_port):
print("无法建立连接,退出")
return
# 启动响应读取线程
self.stop_reading.clear()
reader_thread = threading.Thread(target=self.response_reader, args=(current_src_port,))
reader_thread.daemon = True
reader_thread.start()
connection_established = True
# 发送当前数据包
try:
if Raw in packet:
self.socket.send(packet[Raw].load)
packet_time = time.strftime("%H:%M:%S", time.localtime(float(packet.time)))
print(f"[{packet_time}] [序号:{packet_count}] 已发送数据包 (负载大小: {len(packet[Raw].load)} 字节)")
if delay > 0:
time.sleep(delay)
except Exception as e:
print(f"发送数据包 {packet_count} 时出错: {e}")
sys.exit(1) # 发送失败直接退出进程
if not self.send_packet(packet, packet_count):
print("发送数据包失败,退出")
return
if delay > 0:
time.sleep(delay)
print(f"\n统计信息:")
print(f"总共处理了 {packet_count} 个数据包")
print(f"成功发送了 {self.total_packets_sent} 个数据包")
print(f"总共发送了 {self.total_bytes_sent} 字节数据")
except Exception as e:
print(f"处理数据包时出错: {e}")
sys.exit(1) # 其他错误也直接退出进程
sys.exit(1)
finally:
# 关闭连接和停止读取线程
self.stop_reading.set()
if self.socket:
self.socket.close()