mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
Compare commits
11 Commits
feat-mp42t
...
v5.0.3-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fa1f3ed8 | ||
|
|
830da3aaab | ||
|
|
5a04dc814d | ||
|
|
af5d2bc1f2 | ||
|
|
a3e0c1864e | ||
|
|
33d385d2bf | ||
|
|
29c47a8d08 | ||
|
|
5bf5e7bb20 | ||
|
|
4b74ea5841 | ||
|
|
43710fb017 | ||
|
|
962dda8d08 |
90
api.go
90
api.go
@@ -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
279
doc/arch/auth.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Stream Authentication Mechanism
|
||||
|
||||
Monibuca V5 provides a comprehensive stream authentication mechanism to control access permissions for publishing and subscribing to streams. The authentication mechanism supports multiple methods, including key-based signature authentication and custom authentication handlers.
|
||||
|
||||
## Authentication Principles
|
||||
|
||||
### 1. Authentication Flow Sequence Diagrams
|
||||
|
||||
#### Publishing Authentication Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Publishing Client
|
||||
participant Plugin as Plugin
|
||||
participant AuthHandler as Auth Handler
|
||||
participant Server as Server
|
||||
|
||||
Client->>Plugin: Publishing Request (streamPath, args)
|
||||
Plugin->>Plugin: Check EnableAuth && Type == PublishTypeServer
|
||||
|
||||
alt Authentication Enabled
|
||||
Plugin->>Plugin: Look for custom auth handler
|
||||
|
||||
alt Custom Handler Exists
|
||||
Plugin->>AuthHandler: onAuthPub(publisher)
|
||||
AuthHandler->>AuthHandler: Execute custom auth logic
|
||||
AuthHandler-->>Plugin: Auth result
|
||||
else Use Key-based Auth
|
||||
Plugin->>Plugin: Check if conf.Key exists
|
||||
alt Key Configured
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: Validate timestamp
|
||||
Plugin->>Plugin: Validate secret length
|
||||
Plugin->>Plugin: Calculate MD5 signature
|
||||
Plugin->>Plugin: Compare signatures
|
||||
Plugin-->>Plugin: Auth result
|
||||
end
|
||||
end
|
||||
|
||||
alt Auth Failed
|
||||
Plugin-->>Client: Auth failed, reject publishing
|
||||
else Auth Success
|
||||
Plugin->>Server: Create Publisher and add to stream management
|
||||
Server-->>Plugin: Publishing successful
|
||||
Plugin-->>Client: Publishing established successfully
|
||||
end
|
||||
else Auth Disabled
|
||||
Plugin->>Server: Create Publisher directly
|
||||
Server-->>Plugin: Publishing successful
|
||||
Plugin-->>Client: Publishing established successfully
|
||||
end
|
||||
```
|
||||
|
||||
#### Subscribing Authentication Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Subscribing Client
|
||||
participant Plugin as Plugin
|
||||
participant AuthHandler as Auth Handler
|
||||
participant Server as Server
|
||||
|
||||
Client->>Plugin: Subscribing Request (streamPath, args)
|
||||
Plugin->>Plugin: Check EnableAuth && Type == SubscribeTypeServer
|
||||
|
||||
alt Authentication Enabled
|
||||
Plugin->>Plugin: Look for custom auth handler
|
||||
|
||||
alt Custom Handler Exists
|
||||
Plugin->>AuthHandler: onAuthSub(subscriber)
|
||||
AuthHandler->>AuthHandler: Execute custom auth logic
|
||||
AuthHandler-->>Plugin: Auth result
|
||||
else Use Key-based Auth
|
||||
Plugin->>Plugin: Check if conf.Key exists
|
||||
alt Key Configured
|
||||
Plugin->>Plugin: auth(streamPath, key, secret, expire)
|
||||
Plugin->>Plugin: Validate timestamp
|
||||
Plugin->>Plugin: Validate secret length
|
||||
Plugin->>Plugin: Calculate MD5 signature
|
||||
Plugin->>Plugin: Compare signatures
|
||||
Plugin-->>Plugin: Auth result
|
||||
end
|
||||
end
|
||||
|
||||
alt Auth Failed
|
||||
Plugin-->>Client: Auth failed, reject subscribing
|
||||
else Auth Success
|
||||
Plugin->>Server: Create Subscriber and wait for Publisher
|
||||
Server->>Server: Wait for stream publishing and track ready
|
||||
Server-->>Plugin: Subscribing ready
|
||||
Plugin-->>Client: Start streaming data transmission
|
||||
end
|
||||
else Auth Disabled
|
||||
Plugin->>Server: Create Subscriber directly
|
||||
Server-->>Plugin: Subscribing successful
|
||||
Plugin-->>Client: Start streaming data transmission
|
||||
end
|
||||
```
|
||||
|
||||
### 2. Authentication Trigger Points
|
||||
|
||||
Authentication is triggered in the following two scenarios:
|
||||
|
||||
- **Publishing Authentication**: Triggered when there's a publishing request in the `PublishWithConfig` method
|
||||
- **Subscribing Authentication**: Triggered when there's a subscribing request in the `SubscribeWithConfig` method
|
||||
|
||||
### 3. Authentication Condition Checks
|
||||
|
||||
Authentication is only executed when the following conditions are met simultaneously:
|
||||
|
||||
```go
|
||||
if p.config.EnableAuth && publisher.Type == PublishTypeServer
|
||||
```
|
||||
|
||||
- `EnableAuth`: Authentication is enabled in the plugin configuration
|
||||
- `Type == PublishTypeServer/SubscribeTypeServer`: Only authenticate server-type publishing/subscribing
|
||||
|
||||
### 4. Authentication Method Priority
|
||||
|
||||
The system executes authentication in the following priority order:
|
||||
|
||||
1. **Custom Authentication Handler** (Highest priority)
|
||||
2. **Key-based Signature Authentication**
|
||||
3. **No Authentication** (Default pass)
|
||||
|
||||
## Custom Authentication Handlers
|
||||
|
||||
### Publishing Authentication Handler
|
||||
|
||||
```go
|
||||
onAuthPub := p.Meta.OnAuthPub
|
||||
if onAuthPub == nil {
|
||||
onAuthPub = p.Server.Meta.OnAuthPub
|
||||
}
|
||||
if onAuthPub != nil {
|
||||
if err = onAuthPub(publisher).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Authentication handler lookup order:
|
||||
1. Plugin-level authentication handler `p.Meta.OnAuthPub`
|
||||
2. Server-level authentication handler `p.Server.Meta.OnAuthPub`
|
||||
|
||||
### Subscribing Authentication Handler
|
||||
|
||||
```go
|
||||
onAuthSub := p.Meta.OnAuthSub
|
||||
if onAuthSub == nil {
|
||||
onAuthSub = p.Server.Meta.OnAuthSub
|
||||
}
|
||||
if onAuthSub != nil {
|
||||
if err = onAuthSub(subscriber).Await(); err != nil {
|
||||
p.Warn("auth failed", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key-based Signature Authentication
|
||||
|
||||
When there's no custom authentication handler, if a Key is configured, the system will use MD5-based signature authentication mechanism.
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
```go
|
||||
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
|
||||
// 1. Validate expiration time
|
||||
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
|
||||
return fmt.Errorf("auth failed expired")
|
||||
}
|
||||
|
||||
// 2. Validate secret length
|
||||
if len(secret) != 32 {
|
||||
return fmt.Errorf("auth failed secret length must be 32")
|
||||
}
|
||||
|
||||
// 3. Calculate the true secret
|
||||
trueSecret := md5.Sum([]byte(key + streamPath + expire))
|
||||
|
||||
// 4. Compare secrets
|
||||
if secret == hex.EncodeToString(trueSecret[:]) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("auth failed invalid secret")
|
||||
}
|
||||
```
|
||||
|
||||
### Signature Calculation Steps
|
||||
|
||||
1. **Construct signature string**: `key + streamPath + expire`
|
||||
2. **MD5 encryption**: Perform MD5 hash on the signature string
|
||||
3. **Hexadecimal encoding**: Convert MD5 result to 32-character hexadecimal string
|
||||
4. **Verify signature**: Compare calculation result with client-provided secret
|
||||
|
||||
### Parameter Description
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-----------|------|-------------|---------|
|
||||
| key | string | Secret key set in configuration file | "mySecretKey" |
|
||||
| streamPath | string | Stream path | "live/test" |
|
||||
| expire | string | Expiration timestamp (hexadecimal) | "64a1b2c3" |
|
||||
| secret | string | Client-calculated signature (32-char hex) | "5d41402abc4b2a76b9719d911017c592" |
|
||||
|
||||
### Timestamp Handling
|
||||
|
||||
- Expiration time uses hexadecimal Unix timestamp
|
||||
- System validates if current time exceeds expiration time
|
||||
- Timestamp parsing failure or expiration will cause authentication failure
|
||||
|
||||
## API Key Generation
|
||||
|
||||
The system also provides API interfaces for key generation, supporting authentication needs for admin dashboard:
|
||||
|
||||
```go
|
||||
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// JWT Token validation
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
_, err := p.Server.ValidateToken(tokenString)
|
||||
|
||||
// Generate publishing or subscribing key
|
||||
streamPath := r.PathValue("streamPath")
|
||||
t := r.PathValue("type")
|
||||
expire := r.URL.Query().Get("expire")
|
||||
|
||||
if t == "publish" {
|
||||
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
} else if t == "subscribe" {
|
||||
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
|
||||
rw.Write([]byte(hex.EncodeToString(secret[:])))
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Enable Authentication
|
||||
|
||||
```yaml
|
||||
# Plugin configuration
|
||||
rtmp:
|
||||
enableAuth: true
|
||||
publish:
|
||||
key: "your-publish-key"
|
||||
subscribe:
|
||||
key: "your-subscribe-key"
|
||||
```
|
||||
|
||||
### Publishing URL Example
|
||||
|
||||
```
|
||||
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
|
||||
```
|
||||
|
||||
### Subscribing URL Example
|
||||
|
||||
```
|
||||
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Key Protection**: Keys in configuration files should be properly secured to prevent leakage
|
||||
2. **Time Window**: Set reasonable expiration times to balance security and usability
|
||||
3. **HTTPS Transport**: Use HTTPS for transmitting authentication parameters in production
|
||||
4. **Logging**: Authentication failures are logged as warnings for security auditing
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common causes of authentication failure:
|
||||
|
||||
- `auth failed expired`: Timestamp expired or format error
|
||||
- `auth failed secret length must be 32`: Incorrect secret length
|
||||
- `auth failed invalid secret`: Signature verification failed
|
||||
- `invalid token`: JWT verification failed during API key generation
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
### Plugin Development
|
||||
|
||||
[plugin/README.md](../plugin/README.md)
|
||||
[plugin/README.md](../../plugin/README.md)
|
||||
|
||||
## Task System
|
||||
|
||||
|
||||
@@ -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验证失败
|
||||
621
pb/global.pb.go
621
pb/global.pb.go
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,8 +65,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var _ IAVFrame = (*AnnexB)(nil)
|
||||
|
||||
func (frame *AVFrame) Clone() {
|
||||
|
||||
}
|
||||
|
||||
74
pkg/avframe_convert.go
Normal file
74
pkg/avframe_convert.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"m7s.live/v5/pkg/codec"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
type AVFrameConvert[T IAVFrame] struct {
|
||||
FromTrack, ToTrack *AVTrack
|
||||
lastFromCodecCtx codec.ICodecCtx
|
||||
}
|
||||
|
||||
func NewAVFrameConvert[T IAVFrame](fromTrack *AVTrack, toTrack *AVTrack) *AVFrameConvert[T] {
|
||||
ret := &AVFrameConvert[T]{}
|
||||
ret.FromTrack = fromTrack
|
||||
ret.ToTrack = toTrack
|
||||
if ret.FromTrack == nil {
|
||||
ret.FromTrack = &AVTrack{
|
||||
RingWriter: &RingWriter{
|
||||
Ring: util.NewRing[AVFrame](1),
|
||||
},
|
||||
}
|
||||
}
|
||||
if ret.ToTrack == nil {
|
||||
ret.ToTrack = &AVTrack{
|
||||
RingWriter: &RingWriter{
|
||||
Ring: util.NewRing[AVFrame](1),
|
||||
},
|
||||
}
|
||||
var to T
|
||||
ret.ToTrack.FrameType = reflect.TypeOf(to).Elem()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *AVFrameConvert[T]) ConvertFromAVFrame(avFrame *AVFrame) (to T, err error) {
|
||||
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
|
||||
if c.ToTrack.ICodecCtx == nil {
|
||||
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = avFrame.Demux(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
to.SetAllocator(avFrame.Wraps[0].GetAllocator())
|
||||
to.Mux(c.ToTrack.ICodecCtx, avFrame)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AVFrameConvert[T]) Convert(frame IAVFrame) (to T, err error) {
|
||||
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
|
||||
// Not From Publisher
|
||||
if c.FromTrack.LastValue == nil {
|
||||
err = frame.Parse(c.FromTrack)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.ToTrack.ICodecCtx == nil || c.lastFromCodecCtx != c.FromTrack.ICodecCtx {
|
||||
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.lastFromCodecCtx = c.FromTrack.ICodecCtx
|
||||
if c.FromTrack.Value.Raw, err = frame.Demux(c.FromTrack.ICodecCtx); err != nil {
|
||||
return
|
||||
}
|
||||
to.SetAllocator(frame.GetAllocator())
|
||||
to.Mux(c.ToTrack.ICodecCtx, &c.FromTrack.Value)
|
||||
return
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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时使用PacketTypeCodedFramesX(5字节头)
|
||||
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时使用PacketTypeCodedFrames(8字节头,包含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))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.19.1
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v5.29.3
|
||||
// source: flv.proto
|
||||
|
||||
package pb
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
pb "m7s.live/v5/pb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,26 +27,23 @@ const (
|
||||
)
|
||||
|
||||
type ReqRecordList struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
|
||||
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
|
||||
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
|
||||
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
|
||||
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
|
||||
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
|
||||
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqRecordList) Reset() {
|
||||
*x = ReqRecordList{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_flv_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_flv_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqRecordList) String() string {
|
||||
@@ -56,7 +54,7 @@ func (*ReqRecordList) ProtoMessage() {}
|
||||
|
||||
func (x *ReqRecordList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_flv_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -121,24 +119,21 @@ func (x *ReqRecordList) GetMode() string {
|
||||
}
|
||||
|
||||
type ReqRecordDelete struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
|
||||
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
|
||||
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
|
||||
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
|
||||
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) Reset() {
|
||||
*x = ReqRecordDelete{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_flv_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_flv_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqRecordDelete) String() string {
|
||||
@@ -149,7 +144,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
|
||||
|
||||
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_flv_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -201,86 +196,58 @@ func (x *ReqRecordDelete) GetRange() string {
|
||||
|
||||
var File_flv_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_flv_proto_rawDesc = []byte{
|
||||
0x0a, 0x09, 0x66, 0x6c, 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x66, 0x6c, 0x76,
|
||||
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
|
||||
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
|
||||
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c,
|
||||
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x52,
|
||||
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
|
||||
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e,
|
||||
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61,
|
||||
0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67,
|
||||
0x65, 0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6d, 0x6f, 0x64, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f,
|
||||
0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18,
|
||||
0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
|
||||
0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x32, 0x98, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69,
|
||||
0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x6c, 0x76, 0x2e, 0x52,
|
||||
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67,
|
||||
0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69,
|
||||
0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x66, 0x6c, 0x76,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07, 0x43, 0x61, 0x74,
|
||||
0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x67,
|
||||
0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61,
|
||||
0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f,
|
||||
0x66, 0x6c, 0x76, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12,
|
||||
0x62, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x66, 0x6c, 0x76, 0x2e,
|
||||
0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a,
|
||||
0x16, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22,
|
||||
0x1f, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65,
|
||||
0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d,
|
||||
0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f,
|
||||
0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x70, 0x62,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_flv_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\tflv.proto\x12\x03flv\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\fglobal.proto\"\xb7\x01\n" +
|
||||
"\rReqRecordList\x12\x1e\n" +
|
||||
"\n" +
|
||||
"streamPath\x18\x01 \x01(\tR\n" +
|
||||
"streamPath\x12\x14\n" +
|
||||
"\x05range\x18\x02 \x01(\tR\x05range\x12\x14\n" +
|
||||
"\x05start\x18\x03 \x01(\tR\x05start\x12\x10\n" +
|
||||
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
|
||||
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
|
||||
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
|
||||
"\x04mode\x18\a \x01(\tR\x04mode\"\x91\x01\n" +
|
||||
"\x0fReqRecordDelete\x12\x1e\n" +
|
||||
"\n" +
|
||||
"streamPath\x18\x01 \x01(\tR\n" +
|
||||
"streamPath\x12\x10\n" +
|
||||
"\x03ids\x18\x02 \x03(\rR\x03ids\x12\x1c\n" +
|
||||
"\tstartTime\x18\x03 \x01(\tR\tstartTime\x12\x18\n" +
|
||||
"\aendTime\x18\x04 \x01(\tR\aendTime\x12\x14\n" +
|
||||
"\x05range\x18\x05 \x01(\tR\x05range2\x9e\x02\n" +
|
||||
"\x03api\x12]\n" +
|
||||
"\x04List\x12\x12.flv.ReqRecordList\x1a\x1a.global.RecordResponseList\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/flv/api/list/{streamPath=**}\x12T\n" +
|
||||
"\aCatalog\x12\x16.google.protobuf.Empty\x1a\x17.global.ResponseCatalog\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/flv/api/catalog\x12b\n" +
|
||||
"\x06Delete\x12\x14.flv.ReqRecordDelete\x1a\x16.global.ResponseDelete\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/flv/api/delete/{streamPath=**}B\x1bZ\x19m7s.live/v5/plugin/flv/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_flv_proto_rawDescOnce sync.Once
|
||||
file_flv_proto_rawDescData = file_flv_proto_rawDesc
|
||||
file_flv_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_flv_proto_rawDescGZIP() []byte {
|
||||
file_flv_proto_rawDescOnce.Do(func() {
|
||||
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(file_flv_proto_rawDescData)
|
||||
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)))
|
||||
})
|
||||
return file_flv_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_flv_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_flv_proto_goTypes = []interface{}{
|
||||
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
|
||||
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
(*pb.ResponseList)(nil), // 3: global.ResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
|
||||
var file_flv_proto_goTypes = []any{
|
||||
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
|
||||
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
(*pb.RecordResponseList)(nil), // 3: global.RecordResponseList
|
||||
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
|
||||
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
|
||||
}
|
||||
var file_flv_proto_depIdxs = []int32{
|
||||
0, // 0: flv.api.List:input_type -> flv.ReqRecordList
|
||||
2, // 1: flv.api.Catalog:input_type -> google.protobuf.Empty
|
||||
1, // 2: flv.api.Delete:input_type -> flv.ReqRecordDelete
|
||||
3, // 3: flv.api.List:output_type -> global.ResponseList
|
||||
3, // 3: flv.api.List:output_type -> global.RecordResponseList
|
||||
4, // 4: flv.api.Catalog:output_type -> global.ResponseCatalog
|
||||
5, // 5: flv.api.Delete:output_type -> global.ResponseDelete
|
||||
3, // [3:6] is the sub-list for method output_type
|
||||
@@ -295,37 +262,11 @@ func file_flv_proto_init() {
|
||||
if File_flv_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_flv_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqRecordList); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_flv_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ReqRecordDelete); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_flv_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
@@ -336,7 +277,6 @@ func file_flv_proto_init() {
|
||||
MessageInfos: file_flv_proto_msgTypes,
|
||||
}.Build()
|
||||
File_flv_proto = out.File
|
||||
file_flv_proto_rawDesc = nil
|
||||
file_flv_proto_goTypes = nil
|
||||
file_flv_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ package flv;
|
||||
option go_package="m7s.live/v5/plugin/flv/pb";
|
||||
|
||||
service api {
|
||||
rpc List (ReqRecordList) returns (global.ResponseList) {
|
||||
rpc List (ReqRecordList) returns (global.RecordResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/flv/api/list/{streamPath=**}"
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.1
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// source: flv.proto
|
||||
|
||||
package pb
|
||||
@@ -17,14 +17,20 @@ import (
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Api_List_FullMethodName = "/flv.api/List"
|
||||
Api_Catalog_FullMethodName = "/flv.api/Catalog"
|
||||
Api_Delete_FullMethodName = "/flv.api/Delete"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ApiClient interface {
|
||||
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error)
|
||||
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error)
|
||||
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error)
|
||||
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
|
||||
}
|
||||
@@ -37,9 +43,10 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
|
||||
return &apiClient{cc}
|
||||
}
|
||||
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
|
||||
out := new(pb.ResponseList)
|
||||
err := c.cc.Invoke(ctx, "/flv.api/List", in, out, opts...)
|
||||
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.RecordResponseList)
|
||||
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,8 +54,9 @@ func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.Ca
|
||||
}
|
||||
|
||||
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.ResponseCatalog)
|
||||
err := c.cc.Invoke(ctx, "/flv.api/Catalog", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,8 +64,9 @@ func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc
|
||||
}
|
||||
|
||||
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.ResponseDelete)
|
||||
err := c.cc.Invoke(ctx, "/flv.api/Delete", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,19 +75,22 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
// All implementations must embed UnimplementedApiServer
|
||||
// for forward compatibility
|
||||
// for forward compatibility.
|
||||
type ApiServer interface {
|
||||
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
|
||||
List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error)
|
||||
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
|
||||
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
// UnimplementedApiServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedApiServer struct {
|
||||
}
|
||||
// UnimplementedApiServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedApiServer struct{}
|
||||
|
||||
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
|
||||
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
|
||||
@@ -88,6 +100,7 @@ func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.Res
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
|
||||
func (UnimplementedApiServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ApiServer will
|
||||
@@ -97,6 +110,13 @@ type UnsafeApiServer interface {
|
||||
}
|
||||
|
||||
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
|
||||
// If the following call pancis, it indicates UnimplementedApiServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Api_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
@@ -110,7 +130,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/List",
|
||||
FullMethod: Api_List_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
|
||||
@@ -128,7 +148,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/Catalog",
|
||||
FullMethod: Api_Catalog_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
|
||||
@@ -146,7 +166,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/flv.api/Delete",
|
||||
FullMethod: Api_Delete_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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文件流
|
||||
```
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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=**}"
|
||||
};
|
||||
|
||||
@@ -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
139
plugin/mp4/pkg/audio.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
170
plugin/mp4/pkg/video.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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/PPS(HEVCDecoderConfigurationRecord格式)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 处理视频帧并生成截图
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
24
publisher.go
24
publisher.go
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user