Skip to content

Supports specifying a file name during video recording, facilitating third-party access to the API for retrieving the recorded file name.#379

Open
eanfs wants to merge 1 commit intolanghuihui:v5from
eanfs:feature/support-record-with-file-name
Open

Supports specifying a file name during video recording, facilitating third-party access to the API for retrieving the recorded file name.#379
eanfs wants to merge 1 commit intolanghuihui:v5from
eanfs:feature/support-record-with-file-name

Conversation

@eanfs
Copy link
Copy Markdown
Contributor

@eanfs eanfs commented Dec 15, 2025

Supports specifying a file name during video recording, facilitating third-party access to the API for retrieving the recorded file name.

eanfs added a commit to eanfs/monibuca that referenced this pull request Jan 12, 2026
1. 安全修复:
   - 在 MP4/FLV/HLS 插件中添加文件名安全验证
   - 使用 filepath.Base() 清理路径分隔符,防止路径遍历攻击
   - 验证文件名不为空且不是特殊路径(. 或 ..)

2. 逻辑修复:
   - 修复 MP4 API 中的录制任务查找逻辑
   - 正确处理空文件名场景,避免重复录制任务

3. 代码质量:
   - 格式化 HLS 插件代码(修复空格/制表符混用问题)
   - 统一数据库模型字段命名(Filename -> FileName)

4. 配置解析修复:
   - 修复配置解析时的类型转换问题
   - 使用 unmarshal 函数正确处理类型转换
eanfs added a commit to eanfs/monibuca that referenced this pull request Jan 12, 2026
…e' into eanfs-v5

* pullrequests/eanfs/feature/support-record-with-file-name:
  fix: 修复 PR langhuihui#379 中的安全问题和逻辑缺陷
  cluster pro test
@langhuihui langhuihui requested a review from Copilot April 9, 2026 08:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for specifying an explicit output filename when starting a recording (notably via the MP4 plugin API), and persists the derived filename into the recorded stream metadata to make it discoverable to API consumers.

Changes:

  • Add fileName to the MP4 StartRecord request, propagate it into config.Record, and use it when generating the recording output path.
  • Store a Filename value (derived from the output path) on RecordStream so downstream APIs can return it.
  • Update MP4/FLV/HLS recorders’ filename generation logic to honor RecConf.FileName, plus regenerate MP4 protobuf/gateway artifacts and adjust the example config.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
recoder.go Persists RecordStream.Filename derived from the recording output path.
plugin/mp4/pkg/record.go Honors RecConf.FileName when generating MP4 output names; adds extra task fields/logging.
plugin/mp4/pb/mp4.proto Adds fileName to ReqStartRecord.
plugin/mp4/pb/mp4.pb.gw.go Regenerated gateway code; adds req.Body != nil guards before draining bodies.
plugin/mp4/pb/mp4.pb.go Regenerated protobuf bindings for the new field.
plugin/mp4/pb/mp4_grpc.pb.go Regenerated gRPC bindings.
plugin/mp4/api.go Accepts fileName in StartRecord and stores it into config.Record.
plugin/hls/pkg/record.go Honors RecConf.FileName for TS output naming; adds debug log on create.
plugin/flv/pkg/record.go Honors RecConf.FileName for FLV output naming; adds debug log on create.
pkg/config/types.go Adds Record.FileName to config model.
example/default/config.yaml Updates example defaults and shows filename usage in comments.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugin/mp4/api.go
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.
Comment thread plugin/mp4/pkg/record.go
Comment on lines 148 to 159
var CustomFileName = func(job *m7s.RecordJob) string {
// 如果指定了文件名,使用指定的文件名
if fn := job.RecConf.FileName; fn != "" {
// 确保文件名包含 .mp4 扩展名
if !strings.HasSuffix(strings.ToLower(fn), ".mp4") {
fn = fn + ".mp4"
}
return filepath.Join(job.RecConf.FilePath, fn)
}
// 否则使用时间戳生成文件名
now := time.Now()
return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond()))
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
var CustomFileName = func(job *m7s.RecordJob) string {
// 如果指定了文件名,使用指定的文件名
if fn := job.RecConf.FileName; fn != "" {
// 确保文件名包含 .mp4 扩展名
if !strings.HasSuffix(strings.ToLower(fn), ".mp4") {
fn = fn + ".mp4"
}
return filepath.Join(job.RecConf.FilePath, fn)
}
// 否则使用时间戳生成文件名
now := time.Now()
return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond()))
func defaultMP4FileName() string {
now := time.Now()
return fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond())
}
func sanitizeMP4FileName(fn string) string {
if fn == "" {
return ""
}
normalized := strings.ReplaceAll(fn, "\\", "/")
if filepath.IsAbs(fn) || strings.Contains(normalized, "/") {
fn = filepath.Base(normalized)
}
if fn == "" || fn == "." {
return ""
}
// 确保文件名包含 .mp4 扩展名
if !strings.HasSuffix(strings.ToLower(fn), ".mp4") {
fn = fn + ".mp4"
}
return fn
}
var CustomFileName = func(job *m7s.RecordJob) string {
// 如果指定了文件名,使用指定的文件名
if fn := sanitizeMP4FileName(job.RecConf.FileName); fn != "" {
return filepath.Join(job.RecConf.FilePath, fn)
}
// 否则使用时间戳生成文件名
return filepath.Join(job.RecConf.FilePath, defaultMP4FileName())

Copilot uses AI. Check for mistakes.
Comment thread plugin/mp4/pkg/record.go
}

func (task *writeTrailerTask) Start() (err error) {
task.Info("write trailer start")
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.

Logging "write trailer start" at Info level will run for every trailer write (potentially every fragment), which can significantly increase log volume in production. Consider lowering this to Debug/Trace or removing it unless it’s needed for normal operation.

Suggested change
task.Info("write trailer start")
task.Debug("write trailer start")

Copilot uses AI. Check for mistakes.
Comment thread plugin/hls/pkg/record.go
Comment on lines +4 to +8
"context"
"fmt"
"strings"
"path/filepath"
"time"
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.
Comment thread plugin/hls/pkg/record.go
Comment on lines 31 to +37
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)
}
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.
Comment thread plugin/flv/pkg/record.go
Comment on lines +152 to +156
if fn := job.RecConf.FileName; fn != "" {
if !strings.HasSuffix(strings.ToLower(fn), ".flv") {
fn = fn + ".flv"
}
return filepath.Join(job.RecConf.FilePath, fn)
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.
Comment on lines 1 to 10
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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants