Files
plugin-record/main.go
eanfs df6486a022 Eanfs v4 (#41)
* [feature] 支持录制完成后上传到Minio

* change module id

* Update mod name

* reset go.mod

* Update for minio uploading

* Update for log

* [feature] support all Recorder

* Update

* Merge branch 'v4' into githubv4

* v4:
  git commit for minio

* fix error

* Update

* Update

* Update for support max Duration

* Update v4.6.5

* Update for chang Config name

* [refactor] update for recording duration

* Update for remove orgion file

* Update mod

* Update

* fix: close mp4 record error

* Update readme

* Fix file not upload Successfully

* feat(recording): 支持录制检查回调

* feat:增加数据库录制检查

* Update 录制文件没有写入结束标志

* 更新依赖包

* fix(record): 自动删除的录像文件。

* Update for sqllite to db error
2025-06-20 16:33:44 +08:00

221 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package record
import (
_ "embed"
"errors"
"fmt"
"io"
"net"
"os"
"sync"
"time"
"gorm.io/gorm"
. "m7s.live/engine/v4"
"m7s.live/engine/v4/codec"
"m7s.live/engine/v4/config"
"m7s.live/engine/v4/util"
)
type RecordConfig struct {
config.Subscribe
config.HTTP
Flv Record `desc:"flv录制配置"`
Mp4 Record `desc:"mp4录制配置"`
Fmp4 Record `desc:"fmp4录制配置"`
Hls Record `desc:"hls录制配置"`
Raw Record `desc:"视频裸流录制配置"`
RawAudio Record `desc:"音频裸流录制配置"`
recordings sync.Map
beforeDuration int `desc:"事件前缓存时长"`
afterDuration int `desc:"事件后缓存时长"`
MysqlDSN string `desc:"mysql数据库连接字符串"`
ExceptionPostUrl string `desc:"第三方异常上报地址"`
SqliteDbPath string `desc:"sqlite数据库路径"`
DiskMaxPercent float64 `desc:"硬盘使用百分之上限值,超过后报警"`
LocalIp string `desc:"本机IP"`
RecordFileExpireDays int `desc:"录像自动删除的天数,0或未设置表示不自动删除"`
RecordPathNotShowStreamPath bool `desc:"录像路径中是否包含streamPath默认true"`
Storage StorageConfig `desc:"MINIO 配置"`
}
//go:embed default.yaml
var defaultYaml DefaultYaml
var ErrRecordExist = errors.New("recorder exist")
var RecordPluginConfig = &RecordConfig{
Flv: Record{
Path: "record/flv",
Ext: ".flv",
GetDurationFn: getFLVDuration,
},
Fmp4: Record{
Path: "record/fmp4",
Ext: ".mp4",
},
Mp4: Record{
Path: "record/mp4",
Ext: ".mp4",
},
Hls: Record{
Path: "record/hls",
Ext: ".m3u8",
},
Raw: Record{
Path: "record/raw",
Ext: ".", // 默认h264扩展名为.h264,h265扩展名为.h265
},
RawAudio: Record{
Path: "record/raw",
Ext: ".", // 默认aac扩展名为.aac,pcma扩展名为.pcma,pcmu扩展名为.pcmu
},
beforeDuration: 30,
afterDuration: 30,
MysqlDSN: "",
ExceptionPostUrl: "http://www.163.com",
SqliteDbPath: "./m7sv4.db",
DiskMaxPercent: 80.00,
LocalIp: getLocalIP(),
RecordFileExpireDays: 0,
RecordPathNotShowStreamPath: true,
}
var plugin = InstallPlugin(RecordPluginConfig, defaultYaml)
var exceptionChannel = make(chan *Exception)
var db *gorm.DB
func (conf *RecordConfig) OnEvent(event any) {
switch v := event.(type) {
case FirstConfig, config.Config:
//if conf.MysqlDSN == "" {
// plugin.Error("mysqlDSN 数据库连接配置为空无法运行请在config.yaml里配置")
//}
go func() { //处理所有异常,录像中断异常、录像读取异常、录像导出文件中断、磁盘容量低于阈值异常、磁盘异常
for exception := range exceptionChannel {
SendToThirdPartyAPI(exception)
}
}()
if conf.MysqlDSN == "" {
plugin.Info("sqliteDb filepath is" + conf.SqliteDbPath)
db = initSqliteDB(conf.SqliteDbPath)
} else {
plugin.Info("mysqlDSN is" + conf.MysqlDSN)
db = initMysqlDB(conf.MysqlDSN)
}
if conf.RecordFileExpireDays > 0 { //当有设置录像文件自动删除时间时,则开始运行录像自动删除的进程
//主要逻辑为
//搜索event_records表中event_level值为1的非重要数据并将其create_time与当前时间比对大于RecordFileExpireDays则进行删除数据库标记is_delete为1磁盘上删除录像文件
go func() {
for {
var eventRecords []EventRecord
expireTime := time.Now().AddDate(0, 0, -conf.RecordFileExpireDays)
// 创建包含查询条件的 EventRecord 对象
// queryRecord := EventRecord{
// IsDelete: "0", // 查询条件is_delete = 1
// }
fmt.Printf(" 进行录像文件自动删除: 即将删除创建时间小于 %s 的录像文件。\n", expireTime.Format("2006-01-02 15:04:05"))
err = db.Where("create_time < ?", expireTime).Find(&eventRecords).Error
if err == nil {
if len(eventRecords) > 0 {
for _, record := range eventRecords {
fmt.Printf("执行删除 录像ID: %d, 创建时间: %s, 录像文件: %s\n", record.RecId, record.CreateTime, record.Filepath)
err = os.Remove(record.Filepath)
if err != nil {
fmt.Println("error is " + err.Error())
}
err = db.Delete(record).Error
if err != nil {
fmt.Println("error is " + err.Error())
}
}
}
}
// 等待 1 分钟后继续执行
<-time.After(1 * time.Minute)
}
}()
}
//检查录像任务是否存在,不存在则启动
conf.CheckRecordDB()
conf.Flv.Init()
conf.Mp4.Init()
conf.Fmp4.Init()
conf.Hls.Init()
conf.Raw.Init()
conf.RawAudio.Init()
case SEpublish:
streamPath := v.Target.Path
if conf.Flv.NeedRecord(streamPath) {
go NewFLVRecorder(OrdinaryMode).Start(streamPath)
}
if conf.Mp4.NeedRecord(streamPath) {
go NewMP4Recorder().Start(streamPath)
}
if conf.Fmp4.NeedRecord(streamPath) {
go NewFMP4Recorder().Start(streamPath)
}
if conf.Hls.NeedRecord(streamPath) {
go NewHLSRecorder().Start(streamPath)
}
if conf.Raw.NeedRecord(streamPath) {
go NewRawRecorder().Start(streamPath)
}
if conf.RawAudio.NeedRecord(streamPath) {
go NewRawAudioRecorder().Start(streamPath)
}
}
}
func (conf *RecordConfig) getRecorderConfigByType(t string) (recorder *Record) {
switch t {
case "flv":
recorder = &conf.Flv
case "mp4":
recorder = &conf.Mp4
case "fmp4":
recorder = &conf.Fmp4
case "hls":
recorder = &conf.Hls
case "raw":
recorder = &conf.Raw
case "raw_audio":
recorder = &conf.RawAudio
}
return
}
func getFLVDuration(file io.ReadSeeker) uint32 {
_, err := file.Seek(-4, io.SeekEnd)
if err == nil {
var tagSize uint32
if tagSize, err = util.ReadByteToUint32(file, true); err == nil {
_, err = file.Seek(-int64(tagSize)-4, io.SeekEnd)
if err == nil {
_, timestamp, _, err := codec.ReadFLVTag(file)
if err == nil {
return timestamp
}
}
}
}
return 0
}
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
return ipNet.IP.String()
}
}
}
return ""
}