mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
216 lines
7.2 KiB
Go
216 lines
7.2 KiB
Go
package plugin_mp4
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"m7s.live/v5"
|
||
"m7s.live/v5/pkg/codec"
|
||
"m7s.live/v5/pkg/util"
|
||
"m7s.live/v5/plugin/mp4/pb"
|
||
mp4 "m7s.live/v5/plugin/mp4/pkg"
|
||
pkg "m7s.live/v5/plugin/mp4/pkg"
|
||
"m7s.live/v5/plugin/mp4/pkg/box"
|
||
)
|
||
|
||
type MP4Plugin struct {
|
||
pb.UnimplementedApiServer
|
||
m7s.Plugin
|
||
BeforeDuration time.Duration `default:"30s" desc:"事件录像提前时长,不配置则默认30s"`
|
||
AfterDuration time.Duration `default:"30s" desc:"事件录像结束时长,不配置则默认30s"`
|
||
RecordFileExpireDays int `desc:"录像自动删除的天数,0或未设置表示不自动删除"`
|
||
DiskMaxPercent float64 `default:"90" desc:"硬盘使用百分之上限值,超上限后触发报警,并停止当前所有磁盘写入动作。"`
|
||
AutoOverWriteDiskPercent float64 `default:"0" desc:"自动覆盖功能磁盘占用上限值,超过上限时连续录像自动删除日有录像,事件录像自动删除非重要事件录像,删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
|
||
MigrationThresholdPercent float64 `default:"60" desc:"开始迁移到次级存储的磁盘使用率阈值,当主存储达到此阈值时自动将文件迁移到次级存储"`
|
||
AutoRecovery bool `default:"false" desc:"是否自动恢复"`
|
||
ExceptionPostUrl string `desc:"第三方异常上报地址"`
|
||
EventRecordFilePath string `desc:"事件录像存放地址"`
|
||
}
|
||
|
||
const defaultConfig m7s.DefaultYaml = `publish:
|
||
speed: 1`
|
||
|
||
// var exceptionChannel = make(chan *Exception)
|
||
var _ = m7s.InstallPlugin[MP4Plugin](m7s.PluginMeta{
|
||
DefaultYaml: defaultConfig,
|
||
ServiceDesc: &pb.Api_ServiceDesc,
|
||
RegisterGRPCHandler: pb.RegisterApiHandler,
|
||
NewPuller: pkg.NewPuller,
|
||
NewRecorder: pkg.NewRecorder,
|
||
NewPullProxy: m7s.NewHTTPPullPorxy,
|
||
})
|
||
|
||
func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
|
||
return map[string]http.HandlerFunc{
|
||
"/download/{streamPath...}": p.download,
|
||
"/extract/compressed/{streamPath...}": p.extractCompressedVideoHandel,
|
||
"/extract/gop/{streamPath...}": p.extractGopVideoHandel,
|
||
"/snap/{streamPath...}": p.snapHandel,
|
||
}
|
||
}
|
||
|
||
func (p *MP4Plugin) Start() (err error) {
|
||
if p.DB != nil {
|
||
err = p.DB.AutoMigrate(&Exception{})
|
||
if err != nil {
|
||
return
|
||
}
|
||
err = p.DB.AutoMigrate(&mp4.TagModel{})
|
||
if err != nil {
|
||
return
|
||
}
|
||
if p.AutoOverWriteDiskPercent > 0 {
|
||
var storageTask StorageManagementTask
|
||
storageTask.DB = p.DB
|
||
storageTask.DiskMaxPercent = p.DiskMaxPercent
|
||
storageTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
|
||
storageTask.MigrationThresholdPercent = p.MigrationThresholdPercent
|
||
storageTask.RecordFileExpireDays = p.RecordFileExpireDays
|
||
storageTask.plugin = p
|
||
p.AddTask(&storageTask)
|
||
}
|
||
if p.AutoRecovery {
|
||
var recoveryTask RecordRecoveryTask
|
||
recoveryTask.DB = p.DB
|
||
recoveryTask.plugin = p
|
||
p.AddTask(&recoveryTask)
|
||
}
|
||
}
|
||
// go func() { //处理所有异常,录像中断异常、录像读取异常、录像导出文件中断、磁盘容量低于阈值异常、磁盘异常
|
||
// for exception := range exceptionChannel {
|
||
// p.SendToThirdPartyAPI(exception)
|
||
// }
|
||
// }()
|
||
_, port, _ := strings.Cut(p.GetCommonConf().HTTP.ListenAddr, ":")
|
||
if port == "80" {
|
||
p.PlayAddr = append(p.PlayAddr, "http://{hostName}/mp4/{streamPath}.mp4")
|
||
} else if port != "" {
|
||
p.PlayAddr = append(p.PlayAddr, fmt.Sprintf("http://{hostName}:%s/mp4/{streamPath}.mp4", port))
|
||
}
|
||
_, port, _ = strings.Cut(p.GetCommonConf().HTTP.ListenAddrTLS, ":")
|
||
if port == "443" {
|
||
p.PlayAddr = append(p.PlayAddr, "https://{hostName}/mp4/{streamPath}.mp4")
|
||
} else if port != "" {
|
||
p.PlayAddr = append(p.PlayAddr, fmt.Sprintf("https://{hostName}:%s/mp4/{streamPath}.mp4", port))
|
||
}
|
||
return
|
||
}
|
||
|
||
func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".mp4")
|
||
if r.URL.RawQuery != "" {
|
||
streamPath += "?" + r.URL.RawQuery
|
||
}
|
||
sub, err := p.Subscribe(r.Context(), streamPath)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
sub.RemoteAddr = r.RemoteAddr
|
||
var ctx util.HTTP_WS_Writer
|
||
ctx.Conn, err = sub.CheckWebSocket(w, r)
|
||
if err != nil {
|
||
return
|
||
}
|
||
ctx.WriteTimeout = p.GetCommonConf().WriteTimeout
|
||
ctx.ContentType = "video/mp4"
|
||
ctx.ServeHTTP(w, r)
|
||
|
||
muxer := pkg.NewMuxer(pkg.FLAG_FRAGMENT)
|
||
err = muxer.WriteInitSegment(&ctx)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
var audio, video *pkg.Track
|
||
var nextFragmentId uint32
|
||
if sub.Publisher.HasVideoTrack() && sub.SubVideo {
|
||
v := sub.Publisher.VideoTrack.AVTrack
|
||
if err = v.WaitReady(); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
var codecID box.MP4_CODEC_TYPE
|
||
switch v.ICodecCtx.FourCC() {
|
||
case codec.FourCC_H264:
|
||
codecID = box.MP4_CODEC_H264
|
||
case codec.FourCC_H265:
|
||
codecID = box.MP4_CODEC_H265
|
||
}
|
||
video = muxer.AddTrack(codecID)
|
||
video.Timescale = 1000
|
||
video.Samplelist = []box.Sample{
|
||
{
|
||
KeyFrame: true,
|
||
},
|
||
}
|
||
video.ICodecCtx = v.ICodecCtx
|
||
}
|
||
|
||
if sub.Publisher.HasAudioTrack() && sub.SubAudio {
|
||
a := sub.Publisher.AudioTrack.AVTrack
|
||
if err = a.WaitReady(); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
var codecID box.MP4_CODEC_TYPE
|
||
switch a.ICodecCtx.FourCC() {
|
||
case codec.FourCC_MP4A:
|
||
codecID = box.MP4_CODEC_AAC
|
||
case codec.FourCC_ALAW:
|
||
codecID = box.MP4_CODEC_G711A
|
||
case codec.FourCC_ULAW:
|
||
codecID = box.MP4_CODEC_G711U
|
||
case codec.FourCC_OPUS:
|
||
codecID = box.MP4_CODEC_OPUS
|
||
}
|
||
audio = muxer.AddTrack(codecID)
|
||
audio.Timescale = 1000
|
||
audio.ICodecCtx = a.ICodecCtx
|
||
audio.Samplelist = []box.Sample{
|
||
{
|
||
KeyFrame: true,
|
||
},
|
||
}
|
||
}
|
||
err = muxer.WriteMoov(&ctx)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
ctx.Flush()
|
||
m7s.PlayBlock(sub, func(frame *mp4.AudioFrame) (err error) {
|
||
if audio.Samplelist[0].Buffers != nil {
|
||
audio.Samplelist[0].Duration = sub.AudioReader.AbsTime - audio.Samplelist[0].Timestamp
|
||
nextFragmentId++
|
||
// Create moof box for this track
|
||
moof := audio.MakeMoof(nextFragmentId)
|
||
// Create mdat box for this track
|
||
mdat := box.CreateMemoryBox(box.TypeMDAT, audio.Samplelist[0].Memory)
|
||
box.WriteTo(&ctx, moof, mdat)
|
||
err = ctx.Flush()
|
||
}
|
||
audio.Samplelist[0].Timestamp = sub.AudioReader.AbsTime
|
||
audio.Samplelist[0].Memory = frame.Memory
|
||
return
|
||
}, func(frame *mp4.VideoFrame) (err error) {
|
||
if video.Samplelist[0].Buffers != nil {
|
||
video.Samplelist[0].Duration = sub.VideoReader.AbsTime - video.Samplelist[0].Timestamp
|
||
nextFragmentId++
|
||
// Create moof box for this track
|
||
moof := video.MakeMoof(nextFragmentId)
|
||
// Create mdat box for this track
|
||
mdat := box.CreateMemoryBox(box.TypeMDAT, video.Samplelist[0].Memory)
|
||
box.WriteTo(&ctx, moof, mdat)
|
||
err = ctx.Flush()
|
||
}
|
||
video.Samplelist[0].Memory = frame.Memory
|
||
video.Samplelist[0].Timestamp = sub.VideoReader.AbsTime
|
||
video.Samplelist[0].CTS = uint32(frame.CTS / time.Millisecond)
|
||
video.Samplelist[0].KeyFrame = sub.VideoReader.Value.IDR
|
||
return
|
||
})
|
||
}
|