Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions example/default/config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
global:
http:
listenaddr: :8080
location:
"^/hdl/(.*)": "/flv/$1" # 兼容 v4
"^/stress/api/(.*)": "/test/api/stress/$1" # 5.0.x
"^/monitor/(.*)": "/debug/$1" # 5.0.x
loglevel: debug
loglevel: info
admin:
enablelogin: false
Comment on lines 1 to 10
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example config change does more than demonstrate the new file-name feature (it changes global loglevel, adds an HTTP listen address, and enables the mp4 plugin by default). If the PR is only meant to add filename support, consider keeping unrelated example-default behavior unchanged or moving these changes to a separate PR to avoid surprising defaults.

Copilot uses AI. Check for mistakes.
# S3存储配置示例(需要时取消注释并填写实际值)
# storage:
# s3:
# endpoint: "s3.amazonaws.com" # S3服务端点
# region: "us-east-1" # AWS区域
# accessKeyID: "your-access-key-id" # 访问密钥ID
# secretAccessKey: "your-secret-access-key" # 秘密访问密钥
# bucket: "your-bucket-name" # 存储桶名称
# pathPrefix: "monibuca/recordings" # 文件路径前缀
# forcePathStyle: false # 强制路径样式(MinIO需要设为true)
# useSSL: true # 是否使用SSL
# timeout: "30s" # 上传超时时间
# pullproxy:
# - id: 1 # 唯一ID标识,必须大于0
# name: "camera-1" # 拉流代理名称
Expand Down Expand Up @@ -57,23 +71,23 @@ gb28181:
- platformservergbid: "34020000002000000002" #上级平台GBID
channeldbid: "34020000001110000003_34020000001320000005" #通道DBID,格式为设备ID_通道ID
mp4:
# enable: false
enable: true
# publish:
# delayclosetimeout: 3s
# onpub:
# record:
# ^live/.+:
# fragment: 10s
# filepath: record/$0
# storage:
# s3:
# endpoint: "storage-dev.xiding.tech"
# accessKeyId: "xidinguser"
# secretAccessKey: "U2FsdGVkX1/7uyvj0trCzSNFsfDZ66dMSAEZjNlvW1c="
# bucket: "vidu-media-bucket"
# pathPrefix: ""
# forcePathStyle: true
# useSSL: true
# ^live/(.+)$:
# filepath: s3rec/$0
# filename: $0.mp4
# storage:
# s3:
# endpoint: "storage-dev.xiding.tech"
# accessKeyId: "xidinguser"
# secretAccessKey: "U2FsdGVkX1/7uyvj0trCzSNFsfDZ66dMSAEZjNlvW1c="
# bucket: "vidu-media-bucket"
# pathPrefix: "recordings"
# forcePathStyle: true
# useSSL: true
# pull:
# live/test: /Users/dexter/Movies/1744963190.mp4
onsub:
Expand All @@ -90,8 +104,16 @@ flv:
# onpub:
# record:
# ^live/.+:
# fragment: 1m
# filepath: record/$0
# storage:
# s3:
# endpoint: "s3.amazonaws.com"
# accessKeyId: "your-access-key-id"
# secretAccessKey: "your-secret-access-key"
# bucket: "your-bucket-name"
# pathPrefix: "flv-recordings"
# forcePathStyle: false
# useSSL: true
publish:
delayclosetimeout: 3s
onsub:
Expand Down
5 changes: 3 additions & 2 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ type (
}
Record struct {
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式,event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式,event=事件录像模式;default:'auto'"`
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
FilePath string `desc:"录制文件路径"` // 录制文件路径
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
FilePath string `desc:"录制文件路径"` // 录制文件路径
FileName string `json:"fileName" desc:"录制文件名" gorm:"-"`
Fragment time.Duration `desc:"分片时长"` // 分片时长
RealTime bool `desc:"是否实时录制"` // 是否实时录制
Append bool `desc:"是否追加录制"` // 是否追加录制
Expand Down
8 changes: 8 additions & 0 deletions plugin/flv/pkg/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"time"

task "github.com/langhuihui/gotask"
Expand Down Expand Up @@ -148,6 +149,12 @@ type Recorder struct {
}

var CustomFileName = func(job *m7s.RecordJob) string {
if fn := job.RecConf.FileName; fn != "" {
if !strings.HasSuffix(strings.ToLower(fn), ".flv") {
fn = fn + ".flv"
}
return filepath.Join(job.RecConf.FilePath, fn)
Comment on lines +152 to +156
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom fileName is used directly with filepath.Join. If fileName is absolute or contains path separators (e.g. "../x"), Join can escape RecConf.FilePath and write outside the intended directory. Sanitize/validate the provided name (e.g., reject separators/absolute paths, or force filepath.Base) before joining.

Suggested change
if fn := job.RecConf.FileName; fn != "" {
if !strings.HasSuffix(strings.ToLower(fn), ".flv") {
fn = fn + ".flv"
}
return filepath.Join(job.RecConf.FilePath, fn)
if fn := strings.TrimSpace(job.RecConf.FileName); fn != "" {
if !strings.HasSuffix(strings.ToLower(fn), ".flv") {
fn = fn + ".flv"
}
cleanFn := filepath.Clean(fn)
if filepath.IsAbs(cleanFn) || cleanFn == "." || cleanFn == ".." || cleanFn != filepath.Base(cleanFn) {
cleanFn = filepath.Base(cleanFn)
}
if cleanFn == "" || cleanFn == "." || cleanFn == ".." {
cleanFn = fmt.Sprintf("%d.flv", time.Now().Unix())
}
return filepath.Join(job.RecConf.FilePath, cleanFn)

Copilot uses AI. Check for mistakes.
}
if job.RecConf.Fragment == 0 || job.RecConf.Append {
return fmt.Sprintf("%s.flv", job.RecConf.FilePath)
}
Expand All @@ -160,6 +167,7 @@ func (r *Recorder) createStream(start time.Time) (err error) {
if err != nil {
return
}
r.Debug("flv create file", "filePath", r.Event.FilePath)

// 获取存储实例
st := r.RecordJob.GetStorage()
Expand Down
32 changes: 22 additions & 10 deletions plugin/hls/pkg/record.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package hls

import (
"context"
"fmt"
"path/filepath"
"time"
"context"
"fmt"
"strings"
"path/filepath"
"time"
Comment on lines +4 to +8
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports (spacing/ordering) are not gofmt-formatted (spaces instead of tabs, ordering differs). This will create noisy diffs and can fail formatting checks if enforced. Please run gofmt on this file.

Suggested change
"context"
"fmt"
"strings"
"path/filepath"
"time"
"context"
"fmt"
"path/filepath"
"strings"
"time"

Copilot uses AI. Check for mistakes.

"m7s.live/v5"
"m7s.live/v5/pkg/codec"
Expand All @@ -28,15 +29,26 @@ type Recorder struct {
}

var CustomFileName = func(job *m7s.RecordJob) string {
if job.RecConf.Fragment == 0 || job.RecConf.Append {
return fmt.Sprintf("%s/%s.ts", job.RecConf.FilePath, time.Now().Format("20060102150405"))
}
return filepath.Join(job.RecConf.FilePath, time.Now().Format("20060102150405")+".ts")
if fn := job.RecConf.FileName; fn != "" {
if !strings.HasSuffix(strings.ToLower(fn), ".ts") {
fn = fn + ".ts"
}
return filepath.Join(job.RecConf.FilePath, fn)
}
Comment on lines 31 to +37
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom fileName is used directly with filepath.Join. If fileName is absolute or contains path separators (e.g. "../x"), Join can escape RecConf.FilePath and write outside the intended directory. Sanitize/validate the provided name (e.g., reject separators/absolute paths, or force filepath.Base) before joining.

Copilot uses AI. Check for mistakes.
if job.RecConf.Fragment == 0 || job.RecConf.Append {
return fmt.Sprintf("%s/%s.ts", job.RecConf.FilePath, time.Now().Format("20060102150405"))
}
return filepath.Join(job.RecConf.FilePath, time.Now().Format("20060102150405")+".ts")
}

func (r *Recorder) createStream(start time.Time) (err error) {
r.RecordJob.RecConf.Type = "ts"
return r.CreateStream(start, CustomFileName)
r.RecordJob.RecConf.Type = "ts"
err = r.CreateStream(start, CustomFileName)
if err != nil {
return err
}
r.Debug("ts create file", "filePath", r.Event.FilePath)
return nil
}

func (r *Recorder) writeTailer(end time.Time) {
Expand Down
12 changes: 10 additions & 2 deletions plugin/mp4/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
p.Info("read", "file", file.Name())

// 创建解复用器并解析文件
demuxer := mp4.NewDemuxer(file)
Expand Down Expand Up @@ -558,16 +557,22 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord) (res *mp4pb.ResponseStartRecord, err error) {
var recordExists bool
var filePath = "."
var fileName = ""
var fragment = time.Minute
if req.Fragment != nil {
fragment = req.Fragment.AsDuration()
}
if req.FilePath != "" {
filePath = req.FilePath
}
if req.FileName != "" {
fileName = req.FileName
}

p.Debug("mp4 plugin start record", "streamPath", req.StreamPath, "filePath", filePath, "fileName", fileName, "fragment", fragment)
res = &mp4pb.ResponseStartRecord{}
_, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath && job.RecConf.FileName == req.FileName
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate-recording check compares against req.FilePath/req.FileName rather than the normalized variables (filePath/fileName). When req.FilePath is omitted (defaults to "."), this check won’t match an existing job and can allow multiple recordings for the same stream+effective path/name. Compare against filePath/fileName instead (or normalize req values first).

Suggested change
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath && job.RecConf.FileName == req.FileName
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == filePath && job.RecConf.FileName == fileName

Copilot uses AI. Check for mistakes.
})
if recordExists {
err = pkg.ErrRecordExists
Expand All @@ -578,7 +583,9 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
Append: false,
Fragment: fragment,
FilePath: filePath,
FileName: fileName,
}

var stream *m7s.Publisher
var ok bool
if stream, ok = p.Server.Streams.SafeGet(req.StreamPath); !ok {
Expand All @@ -595,6 +602,7 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
}
}
job := p.Record(stream, recordConf, nil)
p.Debug("mp4 record job", "taskPtr", uint64(job.GetTaskPointer()))
res.Data = uint64(job.GetTaskPointer())
err = job.WaitStarted()
return
Expand Down
17 changes: 13 additions & 4 deletions plugin/mp4/pb/mp4.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 30 additions & 3 deletions plugin/mp4/pb/mp4.pb.gw.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion plugin/mp4/pb/mp4.proto
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ message ReqStartRecord {
string streamPath = 1;
google.protobuf.Duration fragment = 2;
string filePath = 3;
string fileName = 4;
}

message ResponseStartRecord {
Expand Down Expand Up @@ -171,4 +172,4 @@ message ResponseTagList {
string message = 2;
repeated TagInfo list = 3;
uint32 total = 4;
}
}
2 changes: 1 addition & 1 deletion plugin/mp4/pb/mp4_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading