mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-10-26 09:42:05 +08:00
fix: snap use global config to reduce font memory
This commit is contained in:
@@ -14,7 +14,6 @@ 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"
|
||||||
@@ -36,79 +35,51 @@ 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)
|
||||||
//transformer := snap.NewTransform().(*snap.Transformer)
|
if err := snap_pkg.ProcessWithFFmpeg(annexb, buf); err != nil {
|
||||||
//transformer.TransformJob.Init(transformer, &p.Plugin, streamPath, config.Transform{
|
return nil, err
|
||||||
// Output: []config.TransfromOutput{
|
}
|
||||||
// {
|
|
||||||
// Target: streamPath,
|
|
||||||
// StreamPath: streamPath,
|
|
||||||
// Conf: buf,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
//}).WaitStarted()
|
|
||||||
|
|
||||||
// 如果设置了水印文字,添加水印
|
// 如果设置了水印文字,添加水印
|
||||||
if p.Watermark.Text != "" {
|
if p.Watermark.Text != "" && snap_pkg.GlobalWatermarkConfig.Font != nil {
|
||||||
// 读取字体文件
|
|
||||||
fontBytes, err := os.ReadFile(p.Watermark.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 {
|
||||||
p.Error("decode image failed", "error", err.Error())
|
return nil, fmt.Errorf("decode image failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码颜色
|
|
||||||
rgba, err := parseRGBA(p.Watermark.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: p.Watermark.Text,
|
Text: snap_pkg.GlobalWatermarkConfig.Text,
|
||||||
Font: font,
|
Font: snap_pkg.GlobalWatermarkConfig.Font,
|
||||||
FontSize: p.Watermark.FontSize,
|
FontSize: snap_pkg.GlobalWatermarkConfig.FontSize,
|
||||||
Spacing: 10,
|
Spacing: 2,
|
||||||
RowSpacing: 10,
|
RowSpacing: 10,
|
||||||
ColSpacing: 20,
|
ColSpacing: 20,
|
||||||
Rows: 1,
|
Rows: 1,
|
||||||
Cols: 1,
|
Cols: 1,
|
||||||
DPI: 72,
|
DPI: 72,
|
||||||
Color: rgba,
|
Color: snap_pkg.GlobalWatermarkConfig.FontColor,
|
||||||
IsGrid: false,
|
IsGrid: false,
|
||||||
Angle: 0,
|
Angle: 0,
|
||||||
OffsetX: p.Watermark.OffsetX,
|
OffsetX: snap_pkg.GlobalWatermarkConfig.OffsetX,
|
||||||
OffsetY: p.Watermark.OffsetY,
|
OffsetY: snap_pkg.GlobalWatermarkConfig.OffsetY,
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Error("add watermark failed", "error", err.Error())
|
return nil, fmt.Errorf("add watermark failed: %w", err)
|
||||||
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 {
|
||||||
p.Error("encode image failed", "error", err.Error())
|
return nil, fmt.Errorf("encode image failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,108 +94,30 @@ func (p *SnapPlugin) doSnap(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频帧
|
// 调用 snap 进行截图
|
||||||
annexb, _, err := snap_pkg.GetVideoFrame(streamPath, p.Server)
|
buf, err := p.snap(streamPath)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理视频帧生成图片
|
// 处理保存逻辑
|
||||||
buf := new(bytes.Buffer)
|
var savePath string
|
||||||
if err := snap_pkg.ProcessWithFFmpeg(annexb, buf); err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果设置了水印文字,添加水印
|
|
||||||
if p.Watermark.Text != "" {
|
|
||||||
// 读取字体文件
|
|
||||||
fontBytes, err := os.ReadFile(p.Watermark.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.Watermark.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.Watermark.Text,
|
|
||||||
Font: font,
|
|
||||||
FontSize: p.Watermark.FontSize,
|
|
||||||
Spacing: 10,
|
|
||||||
RowSpacing: 10,
|
|
||||||
ColSpacing: 20,
|
|
||||||
Rows: 1,
|
|
||||||
Cols: 1,
|
|
||||||
DPI: 72,
|
|
||||||
Color: rgba,
|
|
||||||
IsGrid: false,
|
|
||||||
Angle: 0,
|
|
||||||
OffsetX: p.Watermark.OffsetX,
|
|
||||||
OffsetY: p.Watermark.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.SavePath != "" && p.IsManualModeSave {
|
if p.SavePath != "" && p.IsManualModeSave {
|
||||||
//判断 SavePath 是否存在
|
|
||||||
if _, err := os.Stat(p.SavePath); os.IsNotExist(err) {
|
|
||||||
os.MkdirAll(p.SavePath, 0755)
|
|
||||||
}
|
|
||||||
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.SavePath, filename)
|
||||||
|
|
||||||
// 保存到本地
|
// 保存到本地
|
||||||
err = os.WriteFile(savePath, buf.Bytes(), 0644)
|
if err := os.WriteFile(savePath, buf.Bytes(), 0644); err != nil {
|
||||||
if err != nil {
|
|
||||||
p.Error("save snapshot failed", "error", err.Error())
|
p.Error("save snapshot failed", "error", err.Error())
|
||||||
savePath = ""
|
savePath = ""
|
||||||
}
|
}
|
||||||
// 保存截图并记录到数据库
|
|
||||||
if p.DB != nil {
|
// 保存截图记录到数据库
|
||||||
// 保存记录到数据库
|
if p.DB != nil && savePath != "" {
|
||||||
record := snap_pkg.SnapRecord{
|
record := snap_pkg.SnapRecord{
|
||||||
StreamName: streamPath,
|
StreamName: streamPath,
|
||||||
SnapMode: 2, // HTTP请求截图模式
|
SnapMode: 2, // HTTP请求截图模式
|
||||||
@@ -235,15 +128,13 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,23 @@ 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"
|
||||||
snap "m7s.live/v5/plugin/snap/pkg"
|
snap "m7s.live/v5/plugin/snap/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SnapModeTimeInterval = iota
|
||||||
|
SnapModeIFrameInterval
|
||||||
|
SnapModeManual
|
||||||
|
)
|
||||||
|
|
||||||
var _ = m7s.InstallPlugin[SnapPlugin](snap.NewTransform)
|
var _ = m7s.InstallPlugin[SnapPlugin](snap.NewTransform)
|
||||||
|
|
||||||
type SnapPlugin struct {
|
type SnapPlugin struct {
|
||||||
@@ -41,7 +47,7 @@ type SnapPlugin struct {
|
|||||||
// OnInit 在插件初始化时添加定时任务
|
// OnInit 在插件初始化时添加定时任务
|
||||||
func (p *SnapPlugin) OnInit() (err error) {
|
func (p *SnapPlugin) OnInit() (err error) {
|
||||||
// 检查 Mode 的值范围
|
// 检查 Mode 的值范围
|
||||||
if p.Mode < 0 || p.Mode > 1 {
|
if p.Mode < SnapModeTimeInterval || p.Mode > SnapModeManual {
|
||||||
p.Error("invalid snap mode",
|
p.Error("invalid snap mode",
|
||||||
"mode", p.Mode,
|
"mode", p.Mode,
|
||||||
"valid_range", "0-1",
|
"valid_range", "0-1",
|
||||||
@@ -92,11 +98,13 @@ func (p *SnapPlugin) OnInit() (err error) {
|
|||||||
rgba = strings.TrimSuffix(rgba, ")")
|
rgba = strings.TrimSuffix(rgba, ")")
|
||||||
parts := strings.Split(rgba, ",")
|
parts := strings.Split(rgba, ",")
|
||||||
if len(parts) == 4 {
|
if len(parts) == 4 {
|
||||||
r, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
|
fontColor, err := parseRGBA(p.Watermark.FontColor)
|
||||||
g, _ := strconv.Atoi(strings.TrimSpace(parts[1]))
|
if err == nil {
|
||||||
b, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
|
snap.GlobalWatermarkConfig.FontColor = fontColor
|
||||||
a, _ := strconv.ParseFloat(strings.TrimSpace(parts[3]), 64)
|
} else {
|
||||||
snap.GlobalWatermarkConfig.FontColor = color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a * 255)}
|
p.Error("parse color failed", "error", err.Error())
|
||||||
|
snap.GlobalWatermarkConfig.FontColor = color.RGBA{uint8(255), uint8(255), uint8(255), uint8(255)}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ type WatermarkConfig struct {
|
|||||||
FontColor color.RGBA // 字体颜色
|
FontColor color.RGBA // 字体颜色
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user