mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
Compare commits
5 Commits
dev
...
revert-169
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
421f77c1de | ||
|
|
d2715d61d5 | ||
|
|
3649b667d9 | ||
|
|
d74b9efdbf | ||
|
|
7f2712ae71 |
@@ -46,22 +46,21 @@ cascadeserver:
|
|||||||
|
|
||||||
snap:
|
snap:
|
||||||
enable: false
|
enable: false
|
||||||
ismanualmodesave: true # 手动截图是否保存文件
|
snapsavemanual: false # 手动截图是否保存文件
|
||||||
watermark:
|
snapwatermark:
|
||||||
text: "Monibuca $T{2006-01-02 15:04:05.000}"
|
text: "Monibuca$T{2006-01-02 15:04:05}"
|
||||||
fontpath: "/System/Library/Fonts/STHeiti Light.ttc" # mac字体路径
|
fontpath: "/System/Library/Fonts/STHeiti Light.ttc" # mac字体路径
|
||||||
# fontpath: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # linux字体路径 思源黑体
|
# fontpath: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # linux字体路径 思源黑体
|
||||||
# fontpath: "C:/Windows/Fonts/msyh.ttf" # windows字体路径 微软雅黑
|
# fontpath: "C:/Windows/Fonts/msyh.ttf" # windows字体路径 微软雅黑
|
||||||
fontsize: 16
|
fontsize: 36
|
||||||
fontspacing: 2 # 添加字体间距配置
|
|
||||||
fontcolor: "rgba(255,165,0,1)"
|
fontcolor: "rgba(255,165,0,1)"
|
||||||
offsetx: 10
|
offsetx: 10
|
||||||
offsety: 10
|
offsety: 10
|
||||||
mode: 2 #截图模式:0-时间间隔,1-关键帧间隔 2-HTTP请求模式(手动触发)
|
snapmode: 1
|
||||||
timeinterval: 3s
|
snaptimeinterval: 1s
|
||||||
savepath: "./snap"
|
snapsavepath: "./snaps"
|
||||||
iframeinterval: 3 # 截图i帧间隔,默认为3,即每隔3个i帧截图一次
|
snapiframeinterval: 3
|
||||||
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
snapquerytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
||||||
filter: "^live/.*"
|
filter: "^live/.*"
|
||||||
onpub:
|
onpub:
|
||||||
transform:
|
transform:
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -102,6 +102,7 @@ require (
|
|||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.2 // indirect
|
github.com/pion/turn/v2 v2.1.2 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.0 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ snap:
|
|||||||
snapsavepath: "snaps" # 截图保存路径
|
snapsavepath: "snaps" # 截图保存路径
|
||||||
filter: ".*" # 截图流过滤器,支持正则表达式
|
filter: ".*" # 截图流过滤器,支持正则表达式
|
||||||
snapiframeinterval: 3 # 间隔多少帧截图
|
snapiframeinterval: 3 # 间隔多少帧截图
|
||||||
snapmode: 1 # 截图模式:0-时间间隔,1-关键帧间隔 2-HTTP请求模式(手动触发)
|
snapmode: 1 # 截图模式:0-时间间隔,1-关键帧间隔
|
||||||
snapquerytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
snapquerytimedelta: 3 # 查询截图时允许的最大时间差(秒)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,11 +56,11 @@ GET /query?streamPath={streamPath}&snapTime={timestamp}
|
|||||||
## 截图模式说明
|
## 截图模式说明
|
||||||
|
|
||||||
### 时间间隔模式 (snapmode: 0)
|
### 时间间隔模式 (snapmode: 0)
|
||||||
- 按照配置的 `timeinterval` 定时对流进行截图
|
- 按照配置的 `snaptimeinterval` 定时对流进行截图
|
||||||
- 适合需要固定时间间隔截图的场景
|
- 适合需要固定时间间隔截图的场景
|
||||||
|
|
||||||
### 关键帧间隔模式 (snapmode: 1)
|
### 关键帧间隔模式 (snapmode: 1)
|
||||||
- 按照配置的 `iframeinterval` 对关键帧进行截图
|
- 按照配置的 `snapiframeinterval` 对关键帧进行截图
|
||||||
- 适合需要按视频内容变化进行截图的场景
|
- 适合需要按视频内容变化进行截图的场景
|
||||||
|
|
||||||
### HTTP请求模式 (snapmode: 2)
|
### HTTP请求模式 (snapmode: 2)
|
||||||
@@ -90,15 +90,15 @@ GET /query?streamPath={streamPath}&snapTime={timestamp}
|
|||||||
配置示例:
|
配置示例:
|
||||||
```yaml
|
```yaml
|
||||||
snap:
|
snap:
|
||||||
watermark:
|
snapwatermark:
|
||||||
text: "测试水印 $T{2006-01-02 15:04:05}"
|
text: "测试水印 $T{2006-01-02 15:04:05}"
|
||||||
fontpath: "/path/to/font.ttf"
|
fontpath: "/path/to/font.ttf"
|
||||||
fontcolor: "rgba(255,0,0,0.5)"
|
fontcolor: "rgba(255,0,0,0.5)"
|
||||||
fontsize: 48
|
fontsize: 48
|
||||||
offsetx: 20
|
offsetx: 20
|
||||||
offsety: 20
|
offsety: 20
|
||||||
mode: 0
|
snapmode: 0
|
||||||
timeinterval: 1m
|
snaptimeinterval: 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数据库记录
|
## 数据库记录
|
||||||
@@ -115,24 +115,24 @@ snap:
|
|||||||
1. 基础配置示例:
|
1. 基础配置示例:
|
||||||
```yaml
|
```yaml
|
||||||
snap:
|
snap:
|
||||||
timeinterval: 30s
|
snaptimeinterval: 30s
|
||||||
savepath: "./snapshots"
|
snapsavepath: "./snapshots"
|
||||||
mode: 1
|
snapmode: 1
|
||||||
iframeinterval: 5
|
snapiframeinterval: 5
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 带水印的配置示例:
|
2. 带水印的配置示例:
|
||||||
```yaml
|
```yaml
|
||||||
snap:
|
snap:
|
||||||
watermark:
|
snapwatermark:
|
||||||
text: "测试水印"
|
text: "测试水印"
|
||||||
fontpath: "/path/to/font.ttf"
|
fontpath: "/path/to/font.ttf"
|
||||||
fontcolor: "rgba(255,0,0,0.5)"
|
fontcolor: "rgba(255,0,0,0.5)"
|
||||||
fontsize: 48
|
fontsize: 48
|
||||||
offsetx: 20
|
offsetx: 20
|
||||||
offsety: 20
|
offsety: 20
|
||||||
mode: 0
|
snapmode: 0
|
||||||
timeinterval: 1m
|
snaptimeinterval: 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
3. API调用示例:
|
3. API调用示例:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
"m7s.live/v5/pkg"
|
"m7s.live/v5/pkg"
|
||||||
snap_pkg "m7s.live/v5/plugin/snap/pkg"
|
snap_pkg "m7s.live/v5/plugin/snap/pkg"
|
||||||
"m7s.live/v5/plugin/snap/pkg/watermark"
|
"m7s.live/v5/plugin/snap/pkg/watermark"
|
||||||
@@ -35,51 +36,79 @@ func parseRGBA(rgba string) (color.RGBA, error) {
|
|||||||
|
|
||||||
// snap 方法负责实际的截图操作
|
// snap 方法负责实际的截图操作
|
||||||
func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) {
|
func (p *SnapPlugin) snap(streamPath string) (*bytes.Buffer, error) {
|
||||||
// 获取视频帧
|
|
||||||
annexb, _, err := snap_pkg.GetVideoFrame(streamPath, p.Server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理视频帧生成图片
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
if err := snap_pkg.ProcessWithFFmpeg(annexb, buf); err != nil {
|
//transformer := snap.NewTransform().(*snap.Transformer)
|
||||||
return nil, err
|
//transformer.TransformJob.Init(transformer, &p.Plugin, streamPath, config.Transform{
|
||||||
}
|
// Output: []config.TransfromOutput{
|
||||||
|
// {
|
||||||
|
// Target: streamPath,
|
||||||
|
// StreamPath: streamPath,
|
||||||
|
// Conf: buf,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
//}).WaitStarted()
|
||||||
|
|
||||||
// 如果设置了水印文字,添加水印
|
// 如果设置了水印文字,添加水印
|
||||||
if p.Watermark.Text != "" && snap_pkg.GlobalWatermarkConfig.Font != nil {
|
if p.SnapWatermark.Text != "" {
|
||||||
|
// 读取字体文件
|
||||||
|
fontBytes, err := os.ReadFile(p.SnapWatermark.FontPath)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("read font file failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析字体
|
||||||
|
font, err := truetype.Parse(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("parse font failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 解码图片
|
// 解码图片
|
||||||
img, _, err := image.Decode(bytes.NewReader(buf.Bytes()))
|
img, _, err := image.Decode(bytes.NewReader(buf.Bytes()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decode image failed: %w", err)
|
p.Error("decode image failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码颜色
|
||||||
|
rgba, err := parseRGBA(p.SnapWatermark.FontColor)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("parse color failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 确保alpha通道正确
|
||||||
|
if rgba.A == 0 {
|
||||||
|
rgba.A = 255 // 如果完全透明,改为不透明
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加水印
|
// 添加水印
|
||||||
result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{
|
result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{
|
||||||
Text: snap_pkg.GlobalWatermarkConfig.Text,
|
Text: p.SnapWatermark.Text,
|
||||||
Font: snap_pkg.GlobalWatermarkConfig.Font,
|
Font: font,
|
||||||
FontSize: snap_pkg.GlobalWatermarkConfig.FontSize,
|
FontSize: p.SnapWatermark.FontSize,
|
||||||
Spacing: snap_pkg.GlobalWatermarkConfig.FontSpacing,
|
Spacing: 10,
|
||||||
RowSpacing: 10,
|
RowSpacing: 10,
|
||||||
ColSpacing: 20,
|
ColSpacing: 20,
|
||||||
Rows: 1,
|
Rows: 1,
|
||||||
Cols: 1,
|
Cols: 1,
|
||||||
DPI: 72,
|
DPI: 72,
|
||||||
Color: snap_pkg.GlobalWatermarkConfig.FontColor,
|
Color: rgba,
|
||||||
IsGrid: false,
|
IsGrid: false,
|
||||||
Angle: 0,
|
Angle: 0,
|
||||||
OffsetX: snap_pkg.GlobalWatermarkConfig.OffsetX,
|
OffsetX: p.SnapWatermark.OffsetX,
|
||||||
OffsetY: snap_pkg.GlobalWatermarkConfig.OffsetY,
|
OffsetY: p.SnapWatermark.OffsetY,
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("add watermark failed: %w", err)
|
p.Error("add watermark failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空原buffer并写入新图片
|
// 清空原buffer并写入新图片
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := imaging.Encode(buf, result, imaging.JPEG); err != nil {
|
if err := imaging.Encode(buf, result, imaging.JPEG); err != nil {
|
||||||
return nil, fmt.Errorf("encode image failed: %w", err)
|
p.Error("encode image failed", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,30 +123,105 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用 snap 进行截图
|
// 获取视频帧
|
||||||
buf, err := p.snap(streamPath)
|
annexb, _, err := snap_pkg.GetVideoFrame(streamPath, p.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Error("snap failed", "error", err.Error())
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理保存逻辑
|
// 处理视频帧生成图片
|
||||||
var savePath string
|
buf := new(bytes.Buffer)
|
||||||
if p.SavePath != "" && p.IsManualModeSave {
|
if err := snap_pkg.ProcessWithFFmpeg(annexb, buf); err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果设置了水印文字,添加水印
|
||||||
|
if p.SnapWatermark.Text != "" {
|
||||||
|
// 读取字体文件
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析字体
|
||||||
|
font, err := truetype.Parse(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("parse font failed", "error", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码图片
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
p.Error("decode image failed", "error", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码颜色
|
||||||
|
rgba, err := parseRGBA(p.SnapWatermark.FontColor)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("parse color failed", "error", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 确保alpha通道正确
|
||||||
|
if rgba.A == 0 {
|
||||||
|
rgba.A = 255 // 如果完全透明,改为不透明
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加水印
|
||||||
|
result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{
|
||||||
|
Text: p.SnapWatermark.Text,
|
||||||
|
Font: font,
|
||||||
|
FontSize: p.SnapWatermark.FontSize,
|
||||||
|
Spacing: 10,
|
||||||
|
RowSpacing: 10,
|
||||||
|
ColSpacing: 20,
|
||||||
|
Rows: 1,
|
||||||
|
Cols: 1,
|
||||||
|
DPI: 72,
|
||||||
|
Color: rgba,
|
||||||
|
IsGrid: false,
|
||||||
|
Angle: 0,
|
||||||
|
OffsetX: p.SnapWatermark.OffsetX,
|
||||||
|
OffsetY: p.SnapWatermark.OffsetY,
|
||||||
|
}, false)
|
||||||
|
if err != nil {
|
||||||
|
p.Error("add watermark failed", "error", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空原buffer并写入新图片
|
||||||
|
buf.Reset()
|
||||||
|
if err := imaging.Encode(buf, result, imaging.JPEG); err != nil {
|
||||||
|
p.Error("encode image failed", "error", err.Error())
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存截图并记录到数据库
|
||||||
|
if p.DB != nil {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
filename := fmt.Sprintf("%s_%s.jpg", streamPath, now.Format("20060102150405.000"))
|
filename := fmt.Sprintf("%s_%s.jpg", streamPath, now.Format("20060102150405.000"))
|
||||||
filename = strings.ReplaceAll(filename, "/", "_")
|
filename = strings.ReplaceAll(filename, "/", "_")
|
||||||
savePath = filepath.Join(p.SavePath, filename)
|
savePath := filepath.Join(p.SnapSavePath, filename)
|
||||||
|
|
||||||
// 保存到本地
|
if p.SnapSavePath != "" {
|
||||||
if err := os.WriteFile(savePath, buf.Bytes(), 0644); err != nil {
|
// 保存到本地
|
||||||
p.Error("save snapshot failed", "error", err.Error())
|
err = os.WriteFile(savePath, buf.Bytes(), 0644)
|
||||||
savePath = ""
|
if err != nil {
|
||||||
}
|
p.Error("save snapshot failed", "error", err.Error())
|
||||||
|
savePath = ""
|
||||||
// 保存截图记录到数据库
|
}
|
||||||
if p.DB != nil && savePath != "" {
|
// 保存记录到数据库
|
||||||
record := snap_pkg.SnapRecord{
|
record := snap_pkg.SnapRecord{
|
||||||
StreamName: streamPath,
|
StreamName: streamPath,
|
||||||
SnapMode: 2, // HTTP请求截图模式
|
SnapMode: 2, // HTTP请求截图模式
|
||||||
@@ -128,13 +232,15 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) {
|
|||||||
p.Error("save snapshot record failed", "error", err.Error())
|
p.Error("save snapshot record failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回图片
|
|
||||||
rw.Header().Set("Content-Type", "image/jpeg")
|
rw.Header().Set("Content-Type", "image/jpeg")
|
||||||
rw.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
rw.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||||
|
|
||||||
if _, err := buf.WriteTo(rw); err != nil {
|
if _, err := buf.WriteTo(rw); err != nil {
|
||||||
p.Error("write response failed", "error", err.Error())
|
p.Error("write response failed", "error", err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +281,7 @@ func (p *SnapPlugin) querySnap(rw http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 计算时间差(秒)
|
// 计算时间差(秒)
|
||||||
timeDiff := targetTime.Sub(record.SnapTime).Seconds()
|
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)
|
http.Error(rw, "no snapshot found within time delta", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
snap_pkg "m7s.live/v5/plugin/snap/pkg"
|
snap_pkg "m7s.live/v5/plugin/snap/pkg"
|
||||||
|
|
||||||
m7s "m7s.live/v5"
|
m7s "m7s.live/v5"
|
||||||
@@ -19,50 +19,34 @@ var _ = m7s.InstallPlugin[SnapPlugin](snap.NewTransform)
|
|||||||
|
|
||||||
type SnapPlugin struct {
|
type SnapPlugin struct {
|
||||||
m7s.Plugin
|
m7s.Plugin
|
||||||
Watermark struct {
|
SnapWatermark struct {
|
||||||
Text string `default:"" desc:"水印文字内容"`
|
Text string `default:"" desc:"水印文字内容"`
|
||||||
FontPath string `default:"" desc:"水印字体文件路径"`
|
FontPath string `default:"" desc:"水印字体文件路径"`
|
||||||
FontColor string `default:"rgba(255,165,0,1)" desc:"水印字体颜色,支持rgba格式"`
|
FontColor string `default:"rgba(255,165,0,1)" desc:"水印字体颜色,支持rgba格式"`
|
||||||
FontSize float64 `default:"36" desc:"水印字体大小"`
|
FontSize float64 `default:"36" desc:"水印字体大小"`
|
||||||
FontSpacing float64 `default:"2" desc:"水印字体间距"`
|
OffsetX int `default:"0" desc:"水印位置X"`
|
||||||
OffsetX int `default:"0" desc:"水印位置X"`
|
OffsetY int `default:"0" desc:"水印位置Y"`
|
||||||
OffsetY int `default:"0" desc:"水印位置Y"`
|
|
||||||
} `desc:"水印配置"`
|
} `desc:"水印配置"`
|
||||||
// 定时任务相关配置
|
// 定时任务相关配置
|
||||||
TimeInterval time.Duration `default:"1m" desc:"截图间隔"`
|
SnapTimeInterval time.Duration `default:"1m" desc:"截图间隔"`
|
||||||
SavePath string `default:"snaps" desc:"截图保存路径"`
|
SnapSavePath string `default:"snaps" desc:"截图保存路径"`
|
||||||
Filter string `default:".*" desc:"截图流过滤器,支持正则表达式"`
|
Filter string `default:".*" desc:"截图流过滤器,支持正则表达式"`
|
||||||
IFrameInterval int `default:"3" desc:"间隔多少帧截图"`
|
SnapIFrameInterval int `default:"3" desc:"间隔多少帧截图"`
|
||||||
Mode int `default:"1" desc:"截图模式 0:间隔时间 1:间隔关键帧"`
|
SnapMode int `default:"1" desc:"截图模式 0:间隔时间 1:间隔关键帧"`
|
||||||
QueryTimeDelta int `default:"3" desc:"查询截图时允许的最大时间差(秒)"`
|
SnapQueryTimeDelta int `default:"3" desc:"查询截图时允许的最大时间差(秒)"`
|
||||||
IsManualModeSave bool `default:"false" desc:"手动截图是否保存文件"`
|
SnapSaveManual bool `default:"false" desc:"手动截图是否保存文件"`
|
||||||
filterRegex *regexp.Regexp
|
filterRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnInit 在插件初始化时添加定时任务
|
// OnInit 在插件初始化时添加定时任务
|
||||||
func (p *SnapPlugin) OnInit() (err error) {
|
func (p *SnapPlugin) OnInit() (err error) {
|
||||||
// 检查 Mode 的值范围
|
// 检查 SnapMode 的值范围
|
||||||
if p.Mode < snap.SnapModeTimeInterval || p.Mode > snap.SnapModeManual {
|
if p.SnapMode < 0 || p.SnapMode > 1 {
|
||||||
p.Error("invalid snap mode",
|
p.Error("invalid snap mode",
|
||||||
"mode", p.Mode,
|
"mode", p.SnapMode,
|
||||||
"valid_range", "0-1",
|
"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)
|
||||||
}
|
|
||||||
// 检查 interval 是否大于0
|
|
||||||
if p.TimeInterval < 0 {
|
|
||||||
p.Error("invalid snap time interval",
|
|
||||||
"interval", p.TimeInterval,
|
|
||||||
"valid_range", ">=0",
|
|
||||||
)
|
|
||||||
return fmt.Errorf("invalid snap time interval: %d, valid range is >=0", p.TimeInterval)
|
|
||||||
}
|
|
||||||
if p.IFrameInterval < 0 {
|
|
||||||
p.Error("invalid snap i-frame interval",
|
|
||||||
"interval", p.IFrameInterval,
|
|
||||||
"valid_range", ">=0",
|
|
||||||
)
|
|
||||||
return fmt.Errorf("invalid snap i-frame interval: %d, valid range is >=0", p.IFrameInterval)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
@@ -75,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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,35 +71,32 @@ func (p *SnapPlugin) OnInit() (err error) {
|
|||||||
|
|
||||||
// 初始化全局水印配置
|
// 初始化全局水印配置
|
||||||
snap.GlobalWatermarkConfig = snap.WatermarkConfig{
|
snap.GlobalWatermarkConfig = snap.WatermarkConfig{
|
||||||
Text: p.Watermark.Text,
|
Text: p.SnapWatermark.Text,
|
||||||
FontPath: p.Watermark.FontPath,
|
FontPath: p.SnapWatermark.FontPath,
|
||||||
FontSize: p.Watermark.FontSize,
|
FontSize: p.SnapWatermark.FontSize,
|
||||||
FontSpacing: p.Watermark.FontSpacing,
|
FontColor: color.RGBA{}, // 将在下面解析
|
||||||
FontColor: color.RGBA{}, // 将在下面解析
|
OffsetX: p.SnapWatermark.OffsetX,
|
||||||
OffsetX: p.Watermark.OffsetX,
|
OffsetY: p.SnapWatermark.OffsetY,
|
||||||
OffsetY: p.Watermark.OffsetY,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Watermark.Text != "" {
|
if p.SnapWatermark.Text != "" {
|
||||||
// 判断字体是否存在
|
// 判断字体是否存在
|
||||||
if _, err := os.Stat(p.Watermark.FontPath); os.IsNotExist(err) {
|
if _, err := os.Stat(p.SnapWatermark.FontPath); os.IsNotExist(err) {
|
||||||
p.Error("watermark font file not found", "path", p.Watermark.FontPath)
|
p.Error("watermark font file not found", "path", p.SnapWatermark.FontPath)
|
||||||
return fmt.Errorf("watermark font file not found: %w", err)
|
return fmt.Errorf("watermark font file not found: %w", err)
|
||||||
}
|
}
|
||||||
// 解析颜色
|
// 解析颜色
|
||||||
if p.Watermark.FontColor != "" {
|
if p.SnapWatermark.FontColor != "" {
|
||||||
rgba := p.Watermark.FontColor
|
rgba := p.SnapWatermark.FontColor
|
||||||
rgba = strings.TrimPrefix(rgba, "rgba(")
|
rgba = strings.TrimPrefix(rgba, "rgba(")
|
||||||
rgba = strings.TrimSuffix(rgba, ")")
|
rgba = strings.TrimSuffix(rgba, ")")
|
||||||
parts := strings.Split(rgba, ",")
|
parts := strings.Split(rgba, ",")
|
||||||
if len(parts) == 4 {
|
if len(parts) == 4 {
|
||||||
fontColor, err := parseRGBA(p.Watermark.FontColor)
|
r, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||||
if err == nil {
|
g, _ := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||||
snap.GlobalWatermarkConfig.FontColor = fontColor
|
b, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
|
||||||
} else {
|
a, _ := strconv.ParseFloat(strings.TrimSpace(parts[3]), 64)
|
||||||
p.Error("parse color failed", "error", err.Error())
|
snap.GlobalWatermarkConfig.FontColor = color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a * 255)}
|
||||||
snap.GlobalWatermarkConfig.FontColor = color.RGBA{uint8(255), uint8(255), uint8(255), uint8(255)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,18 +118,18 @@ func (p *SnapPlugin) OnInit() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//如果截图模式不是时间模式,则不加定时任务
|
//如果截图模式不是时间模式,则不加定时任务
|
||||||
if p.Mode != snap_pkg.SnapModeTimeInterval {
|
if p.SnapMode != 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果间隔时间小于0,则不添加定时任务;等于0则走onpub的transform
|
// 如果间隔时间小于0,则不添加定时任务;等于0则走onpub的transform
|
||||||
if p.TimeInterval <= 0 {
|
if p.SnapTimeInterval <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 添加定时任务
|
// 添加定时任务
|
||||||
p.AddTask(&SnapTimerTask{
|
p.AddTask(&SnapTimerTask{
|
||||||
Interval: p.TimeInterval,
|
Interval: p.SnapTimeInterval,
|
||||||
SavePath: p.SavePath,
|
SavePath: p.SnapSavePath,
|
||||||
Plugin: p,
|
Plugin: p,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ import (
|
|||||||
"m7s.live/v5/pkg/task"
|
"m7s.live/v5/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
SnapModeTimeInterval = iota
|
|
||||||
SnapModeIFrameInterval
|
|
||||||
SnapModeManual
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetVideoFrame 获取视频帧数据
|
// GetVideoFrame 获取视频帧数据
|
||||||
func GetVideoFrame(streamPath string, server *m7s.Server) (pkg.AnnexB, *pkg.AVTrack, error) {
|
func GetVideoFrame(streamPath string, server *m7s.Server) (pkg.AnnexB, *pkg.AVTrack, error) {
|
||||||
// 获取发布者
|
// 获取发布者
|
||||||
@@ -162,34 +156,34 @@ type Transformer struct {
|
|||||||
|
|
||||||
func (t *Transformer) Start() (err error) {
|
func (t *Transformer) Start() (err error) {
|
||||||
// 获取配置,带默认值检查
|
// 获取配置,带默认值检查
|
||||||
if t.TransformJob.Plugin.Config.Has("TimeInterval") {
|
if t.TransformJob.Plugin.Config.Has("SnapTimeInterval") {
|
||||||
t.snapTimeInterval = t.TransformJob.Plugin.Config.Get("TimeInterval").GetValue().(time.Duration)
|
t.snapTimeInterval = t.TransformJob.Plugin.Config.Get("SnapTimeInterval").GetValue().(time.Duration)
|
||||||
} else {
|
} else {
|
||||||
t.snapTimeInterval = time.Minute // 默认1分钟
|
t.snapTimeInterval = time.Minute // 默认1分钟
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.TransformJob.Plugin.Config.Has("SavePath") {
|
if t.TransformJob.Plugin.Config.Has("SnapSavePath") {
|
||||||
t.savePath = t.TransformJob.Plugin.Config.Get("SavePath").GetValue().(string)
|
t.savePath = t.TransformJob.Plugin.Config.Get("SnapSavePath").GetValue().(string)
|
||||||
} else {
|
} else {
|
||||||
t.savePath = "snaps" // 默认保存路径
|
t.savePath = "snaps" // 默认保存路径
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.TransformJob.Plugin.Config.Has("Mode") {
|
if t.TransformJob.Plugin.Config.Has("SnapMode") {
|
||||||
t.snapMode = t.TransformJob.Plugin.Config.Get("Mode").GetValue().(int)
|
t.snapMode = t.TransformJob.Plugin.Config.Get("SnapMode").GetValue().(int)
|
||||||
} else {
|
} else {
|
||||||
t.snapMode = SnapModeIFrameInterval // 默认使用关键帧模式
|
t.snapMode = 1 // 默认使用关键帧模式
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查snapmode是否有效
|
// 检查snapmode是否有效
|
||||||
if t.snapMode != SnapModeIFrameInterval && t.snapMode != SnapModeTimeInterval {
|
if t.snapMode != 0 && t.snapMode != 1 {
|
||||||
t.Debug("invalid snap mode, skip snapshot",
|
t.Debug("invalid snap mode, skip snapshot",
|
||||||
"mode", t.snapMode,
|
"mode", t.snapMode,
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.TransformJob.Plugin.Config.Has("IFrameInterval") {
|
if t.TransformJob.Plugin.Config.Has("SnapIFrameInterval") {
|
||||||
t.snapFrameInterval = t.TransformJob.Plugin.Config.Get("IFrameInterval").GetValue().(int)
|
t.snapFrameInterval = t.TransformJob.Plugin.Config.Get("SnapIFrameInterval").GetValue().(int)
|
||||||
} else {
|
} else {
|
||||||
t.snapFrameInterval = 3 // 默认每3个I帧截图一次
|
t.snapFrameInterval = 3 // 默认每3个I帧截图一次
|
||||||
}
|
}
|
||||||
@@ -215,7 +209,7 @@ func (t *Transformer) Start() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是时间间隔模式且间隔时间不为0,则跳过订阅模式
|
// 如果是时间间隔模式且间隔时间不为0,则跳过订阅模式
|
||||||
if t.snapMode == SnapModeTimeInterval && t.snapTimeInterval != 0 {
|
if t.snapMode == 0 && t.snapTimeInterval != 0 {
|
||||||
t.Info("snap interval is set, skipping subscriber mode",
|
t.Info("snap interval is set, skipping subscriber mode",
|
||||||
"interval", t.snapTimeInterval,
|
"interval", t.snapTimeInterval,
|
||||||
"save_path", t.savePath,
|
"save_path", t.savePath,
|
||||||
@@ -228,20 +222,6 @@ func (t *Transformer) Start() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transformer) Go() error {
|
func (t *Transformer) Go() error {
|
||||||
// 检查snapmode是否有效
|
|
||||||
if t.snapMode != SnapModeIFrameInterval && t.snapMode != SnapModeTimeInterval {
|
|
||||||
t.Debug("invalid snap mode, skip snapshot",
|
|
||||||
"mode", t.snapMode,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if t.snapMode == SnapModeTimeInterval && t.snapTimeInterval != 0 {
|
|
||||||
t.Info("snap interval is set, skipping subscriber mode",
|
|
||||||
"interval", t.snapTimeInterval,
|
|
||||||
"save_path", t.savePath,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// 1. 通过 TransformJob 获取 Subscriber
|
// 1. 通过 TransformJob 获取 Subscriber
|
||||||
subscriber := t.TransformJob.Subscriber
|
subscriber := t.TransformJob.Subscriber
|
||||||
|
|
||||||
|
|||||||
@@ -22,19 +22,18 @@ var (
|
|||||||
|
|
||||||
// WatermarkConfig 水印配置
|
// WatermarkConfig 水印配置
|
||||||
type WatermarkConfig struct {
|
type WatermarkConfig struct {
|
||||||
Text string // 水印文字
|
Text string // 水印文字
|
||||||
FontPath string // 字体文件路径
|
FontPath string // 字体文件路径
|
||||||
FontSize float64 // 字体大小
|
FontSize float64 // 字体大小
|
||||||
FontColor color.RGBA // 字体颜色
|
FontColor color.RGBA // 字体颜色
|
||||||
FontSpacing float64 // 字体间距
|
OffsetX int // X轴偏移
|
||||||
OffsetX int // X轴偏移
|
OffsetY int // Y轴偏移
|
||||||
OffsetY int // Y轴偏移
|
font *truetype.Font // 缓存的字体对象
|
||||||
Font *truetype.Font // 缓存的字体对象
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFont 加载字体文件
|
// LoadFont 加载字体文件
|
||||||
func (w *WatermarkConfig) LoadFont() error {
|
func (w *WatermarkConfig) LoadFont() error {
|
||||||
if w.Font != nil {
|
if w.font != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ func (w *WatermarkConfig) LoadFont() error {
|
|||||||
fontCacheLock.RUnlock()
|
fontCacheLock.RUnlock()
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
w.Font = cachedFont
|
w.font = cachedFont
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ func (w *WatermarkConfig) LoadFont() error {
|
|||||||
fontCache[w.FontPath] = font
|
fontCache[w.FontPath] = font
|
||||||
fontCacheLock.Unlock()
|
fontCacheLock.Unlock()
|
||||||
|
|
||||||
w.Font = font
|
w.font = font
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ func AddWatermark(imgData []byte, config WatermarkConfig) ([]byte, error) {
|
|||||||
// 添加水印
|
// 添加水印
|
||||||
result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{
|
result, err := watermark.DrawWatermarkSingle(img, watermark.TextConfig{
|
||||||
Text: config.Text,
|
Text: config.Text,
|
||||||
Font: config.Font,
|
Font: config.font,
|
||||||
FontSize: config.FontSize,
|
FontSize: config.FontSize,
|
||||||
Spacing: 10,
|
Spacing: 10,
|
||||||
RowSpacing: 10,
|
RowSpacing: 10,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func (t *SnapTimerTask) Tick(any) {
|
|||||||
if t.Plugin.DB != nil {
|
if t.Plugin.DB != nil {
|
||||||
record := snap_pkg.SnapRecord{
|
record := snap_pkg.SnapRecord{
|
||||||
StreamName: streamPath,
|
StreamName: streamPath,
|
||||||
SnapMode: t.Plugin.Mode,
|
SnapMode: t.Plugin.SnapMode,
|
||||||
SnapTime: now,
|
SnapTime: now,
|
||||||
SnapPath: savePath,
|
SnapPath: savePath,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user