diff --git a/go.mod b/go.mod index 72a14b0..e7805e2 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v2 v2.1.2 // indirect github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/webrtc/v4 v4.0.7 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/plugin/snap/README.md b/plugin/snap/README.md index 5545cdc..a5a04ee 100644 --- a/plugin/snap/README.md +++ b/plugin/snap/README.md @@ -56,11 +56,11 @@ GET /query?streamPath={streamPath}&snapTime={timestamp} ## 截图模式说明 ### 时间间隔模式 (snapmode: 0) -- 按照配置的 `timeinterval` 定时对流进行截图 +- 按照配置的 `snaptimeinterval` 定时对流进行截图 - 适合需要固定时间间隔截图的场景 ### 关键帧间隔模式 (snapmode: 1) -- 按照配置的 `iframeinterval` 对关键帧进行截图 +- 按照配置的 `snapiframeinterval` 对关键帧进行截图 - 适合需要按视频内容变化进行截图的场景 ### HTTP请求模式 (snapmode: 2) @@ -90,15 +90,15 @@ GET /query?streamPath={streamPath}&snapTime={timestamp} 配置示例: ```yaml snap: - watermark: + snapwatermark: text: "测试水印 $T{2006-01-02 15:04:05}" fontpath: "/path/to/font.ttf" fontcolor: "rgba(255,0,0,0.5)" fontsize: 48 offsetx: 20 offsety: 20 - mode: 0 - timeinterval: 1m + snapmode: 0 + snaptimeinterval: 1m ``` ## 数据库记录 @@ -115,24 +115,24 @@ snap: 1. 基础配置示例: ```yaml snap: - timeinterval: 30s - savepath: "./snapshots" - mode: 1 - iframeinterval: 5 + snaptimeinterval: 30s + snapsavepath: "./snapshots" + snapmode: 1 + snapiframeinterval: 5 ``` 2. 带水印的配置示例: ```yaml snap: - watermark: + snapwatermark: text: "测试水印" fontpath: "/path/to/font.ttf" fontcolor: "rgba(255,0,0,0.5)" fontsize: 48 offsetx: 20 offsety: 20 - mode: 0 - timeinterval: 1m + snapmode: 0 + snaptimeinterval: 1m ``` 3. API调用示例: diff --git a/plugin/snap/api.go b/plugin/snap/api.go index ce8f35f..64f2dac 100755 --- a/plugin/snap/api.go +++ b/plugin/snap/api.go @@ -49,9 +49,9 @@ func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) { //}).WaitStarted() // 如果设置了水印文字,添加水印 - if p.Watermark.Text != "" { + if p.SnapWatermark.Text != "" { // 读取字体文件 - fontBytes, err := os.ReadFile(p.Watermark.FontPath) + fontBytes, err := os.ReadFile(p.SnapWatermark.FontPath) if err != nil { p.Error("read font file failed", "error", err.Error()) return nil, err @@ -72,7 +72,7 @@ func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) { } // 解码颜色 - rgba, err := parseRGBA(p.Watermark.FontColor) + rgba, err := parseRGBA(p.SnapWatermark.FontColor) if err != nil { p.Error("parse color failed", "error", err.Error()) return nil, err @@ -84,9 +84,9 @@ func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) { // 添加水印 result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{ - Text: p.Watermark.Text, + Text: p.SnapWatermark.Text, Font: font, - FontSize: p.Watermark.FontSize, + FontSize: p.SnapWatermark.FontSize, Spacing: 10, RowSpacing: 10, ColSpacing: 20, @@ -96,8 +96,8 @@ func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) { Color: rgba, IsGrid: false, Angle: 0, - OffsetX: p.Watermark.OffsetX, - OffsetY: p.Watermark.OffsetY, + OffsetX: p.SnapWatermark.OffsetX, + OffsetY: p.SnapWatermark.OffsetY, }, false) if err != nil { p.Error("add watermark failed", "error", err.Error()) @@ -138,9 +138,9 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) { } // 如果设置了水印文字,添加水印 - if p.Watermark.Text != "" { + if p.SnapWatermark.Text != "" { // 读取字体文件 - fontBytes, err := os.ReadFile(p.Watermark.FontPath) + fontBytes, err := os.ReadFile(p.SnapWatermark.FontPath) if err != nil { p.Error("read font file failed", "error", err.Error()) http.Error(rw, err.Error(), http.StatusInternalServerError) @@ -164,7 +164,7 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) { } // 解码颜色 - rgba, err := parseRGBA(p.Watermark.FontColor) + rgba, err := parseRGBA(p.SnapWatermark.FontColor) if err != nil { p.Error("parse color failed", "error", err.Error()) http.Error(rw, err.Error(), http.StatusInternalServerError) @@ -177,9 +177,9 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) { // 添加水印 result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{ - Text: p.Watermark.Text, + Text: p.SnapWatermark.Text, Font: font, - FontSize: p.Watermark.FontSize, + FontSize: p.SnapWatermark.FontSize, Spacing: 10, RowSpacing: 10, ColSpacing: 20, @@ -189,8 +189,8 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) { Color: rgba, IsGrid: false, Angle: 0, - OffsetX: p.Watermark.OffsetX, - OffsetY: p.Watermark.OffsetY, + OffsetX: p.SnapWatermark.OffsetX, + OffsetY: p.SnapWatermark.OffsetY, }, false) if err != nil { p.Error("add watermark failed", "error", err.Error()) @@ -207,23 +207,20 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) { } } - if p.SavePath != "" && p.IsManualModeSave { - //判断 SavePath 是否存在 - if _, err := os.Stat(p.SavePath); os.IsNotExist(err) { - os.MkdirAll(p.SavePath, 0755) - } + // 保存截图并记录到数据库 + if p.DB != nil { now := time.Now() filename := fmt.Sprintf("%s_%s.jpg", streamPath, now.Format("20060102150405.000")) filename = strings.ReplaceAll(filename, "/", "_") - savePath := filepath.Join(p.SavePath, filename) - // 保存到本地 - err = os.WriteFile(savePath, buf.Bytes(), 0644) - if err != nil { - p.Error("save snapshot failed", "error", err.Error()) - savePath = "" - } - // 保存截图并记录到数据库 - if p.DB != nil { + savePath := filepath.Join(p.SnapSavePath, filename) + + if p.SnapSavePath != "" { + // 保存到本地 + err = os.WriteFile(savePath, buf.Bytes(), 0644) + if err != nil { + p.Error("save snapshot failed", "error", err.Error()) + savePath = "" + } // 保存记录到数据库 record := snap_pkg.SnapRecord{ StreamName: streamPath, @@ -284,7 +281,7 @@ func (p *SnapPlugin) querySnap(rw http.ResponseWriter, r *http.Request) { // 计算时间差(秒) timeDiff := targetTime.Sub(record.SnapTime).Seconds() - if timeDiff > float64(time.Duration(p.QueryTimeDelta)*time.Second) { + if timeDiff > float64(time.Duration(p.SnapQueryTimeDelta)*time.Second) { http.Error(rw, "no snapshot found within time delta", http.StatusNotFound) return } diff --git a/plugin/snap/index.go b/plugin/snap/index.go index 4a93330..9e9671c 100755 --- a/plugin/snap/index.go +++ b/plugin/snap/index.go @@ -19,7 +19,7 @@ var _ = m7s.InstallPlugin[SnapPlugin](snap.NewTransform) type SnapPlugin struct { m7s.Plugin - Watermark struct { + SnapWatermark struct { Text string `default:"" desc:"水印文字内容"` FontPath string `default:"" desc:"水印字体文件路径"` FontColor string `default:"rgba(255,165,0,1)" desc:"水印字体颜色,支持rgba格式"` @@ -28,25 +28,25 @@ type SnapPlugin struct { OffsetY int `default:"0" desc:"水印位置Y"` } `desc:"水印配置"` // 定时任务相关配置 - TimeInterval time.Duration `default:"1m" desc:"截图间隔"` - SavePath string `default:"snaps" desc:"截图保存路径"` - Filter string `default:".*" desc:"截图流过滤器,支持正则表达式"` - IFrameInterval int `default:"3" desc:"间隔多少帧截图"` - Mode int `default:"1" desc:"截图模式 0:间隔时间 1:间隔关键帧"` - QueryTimeDelta int `default:"3" desc:"查询截图时允许的最大时间差(秒)"` - IsManualModeSave bool `default:"false" desc:"手动截图是否保存文件"` - filterRegex *regexp.Regexp + SnapTimeInterval time.Duration `default:"1m" desc:"截图间隔"` + SnapSavePath string `default:"snaps" desc:"截图保存路径"` + Filter string `default:".*" desc:"截图流过滤器,支持正则表达式"` + SnapIFrameInterval int `default:"3" desc:"间隔多少帧截图"` + SnapMode int `default:"1" desc:"截图模式 0:间隔时间 1:间隔关键帧"` + SnapQueryTimeDelta int `default:"3" desc:"查询截图时允许的最大时间差(秒)"` + SnapSaveManual bool `default:"false" desc:"手动截图是否保存文件"` + filterRegex *regexp.Regexp } // OnInit 在插件初始化时添加定时任务 func (p *SnapPlugin) OnInit() (err error) { - // 检查 Mode 的值范围 - if p.Mode < 0 || p.Mode > 1 { + // 检查 SnapMode 的值范围 + if p.SnapMode < 0 || p.SnapMode > 1 { p.Error("invalid snap mode", - "mode", p.Mode, + "mode", p.SnapMode, "valid_range", "0-1", ) - return fmt.Errorf("invalid snap mode: %d, valid range is 0-1", p.Mode) + return fmt.Errorf("invalid snap mode: %d, valid range is 0-1", p.SnapMode) } // 初始化数据库 @@ -59,7 +59,7 @@ func (p *SnapPlugin) OnInit() (err error) { } // 创建保存目录 - if err = os.MkdirAll(p.SavePath, 0755); err != nil { + if err = os.MkdirAll(p.SnapSavePath, 0755); err != nil { return } @@ -71,23 +71,23 @@ func (p *SnapPlugin) OnInit() (err error) { // 初始化全局水印配置 snap.GlobalWatermarkConfig = snap.WatermarkConfig{ - Text: p.Watermark.Text, - FontPath: p.Watermark.FontPath, - FontSize: p.Watermark.FontSize, + Text: p.SnapWatermark.Text, + FontPath: p.SnapWatermark.FontPath, + FontSize: p.SnapWatermark.FontSize, FontColor: color.RGBA{}, // 将在下面解析 - OffsetX: p.Watermark.OffsetX, - OffsetY: p.Watermark.OffsetY, + OffsetX: p.SnapWatermark.OffsetX, + OffsetY: p.SnapWatermark.OffsetY, } - if p.Watermark.Text != "" { + if p.SnapWatermark.Text != "" { // 判断字体是否存在 - if _, err := os.Stat(p.Watermark.FontPath); os.IsNotExist(err) { - p.Error("watermark font file not found", "path", p.Watermark.FontPath) + if _, err := os.Stat(p.SnapWatermark.FontPath); os.IsNotExist(err) { + p.Error("watermark font file not found", "path", p.SnapWatermark.FontPath) return fmt.Errorf("watermark font file not found: %w", err) } // 解析颜色 - if p.Watermark.FontColor != "" { - rgba := p.Watermark.FontColor + if p.SnapWatermark.FontColor != "" { + rgba := p.SnapWatermark.FontColor rgba = strings.TrimPrefix(rgba, "rgba(") rgba = strings.TrimSuffix(rgba, ")") parts := strings.Split(rgba, ",") @@ -118,18 +118,18 @@ func (p *SnapPlugin) OnInit() (err error) { } //如果截图模式不是时间模式,则不加定时任务 - if p.Mode != 0 { + if p.SnapMode != 0 { return } // 如果间隔时间小于0,则不添加定时任务;等于0则走onpub的transform - if p.TimeInterval <= 0 { + if p.SnapTimeInterval <= 0 { return } // 添加定时任务 p.AddTask(&SnapTimerTask{ - Interval: p.TimeInterval, - SavePath: p.SavePath, + Interval: p.SnapTimeInterval, + SavePath: p.SnapSavePath, Plugin: p, }) diff --git a/plugin/snap/pkg/transform.go b/plugin/snap/pkg/transform.go index 80f8352..2a74755 100644 --- a/plugin/snap/pkg/transform.go +++ b/plugin/snap/pkg/transform.go @@ -156,20 +156,20 @@ type Transformer struct { func (t *Transformer) Start() (err error) { // 获取配置,带默认值检查 - if t.TransformJob.Plugin.Config.Has("TimeInterval") { - t.snapTimeInterval = t.TransformJob.Plugin.Config.Get("TimeInterval").GetValue().(time.Duration) + if t.TransformJob.Plugin.Config.Has("SnapTimeInterval") { + t.snapTimeInterval = t.TransformJob.Plugin.Config.Get("SnapTimeInterval").GetValue().(time.Duration) } else { t.snapTimeInterval = time.Minute // 默认1分钟 } - if t.TransformJob.Plugin.Config.Has("SavePath") { - t.savePath = t.TransformJob.Plugin.Config.Get("SavePath").GetValue().(string) + if t.TransformJob.Plugin.Config.Has("SnapSavePath") { + t.savePath = t.TransformJob.Plugin.Config.Get("SnapSavePath").GetValue().(string) } else { t.savePath = "snaps" // 默认保存路径 } - if t.TransformJob.Plugin.Config.Has("Mode") { - t.snapMode = t.TransformJob.Plugin.Config.Get("Mode").GetValue().(int) + if t.TransformJob.Plugin.Config.Has("SnapMode") { + t.snapMode = t.TransformJob.Plugin.Config.Get("SnapMode").GetValue().(int) } else { t.snapMode = 1 // 默认使用关键帧模式 } @@ -182,8 +182,8 @@ func (t *Transformer) Start() (err error) { return nil } - if t.TransformJob.Plugin.Config.Has("IFrameInterval") { - t.snapFrameInterval = t.TransformJob.Plugin.Config.Get("IFrameInterval").GetValue().(int) + if t.TransformJob.Plugin.Config.Has("SnapIFrameInterval") { + t.snapFrameInterval = t.TransformJob.Plugin.Config.Get("SnapIFrameInterval").GetValue().(int) } else { t.snapFrameInterval = 3 // 默认每3个I帧截图一次 } diff --git a/plugin/snap/tick.go b/plugin/snap/tick.go index 4e6d528..d42b1df 100644 --- a/plugin/snap/tick.go +++ b/plugin/snap/tick.go @@ -57,7 +57,7 @@ func (t *SnapTimerTask) Tick(any) { if t.Plugin.DB != nil { record := snap_pkg.SnapRecord{ StreamName: streamPath, - SnapMode: t.Plugin.Mode, + SnapMode: t.Plugin.SnapMode, SnapTime: now, SnapPath: savePath, }