mirror of
https://github.com/Monibuca/plugin-record.git
synced 2025-10-18 22:44:44 +08:00
增加分片录制能力以及hls录制能力
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# RECORD插件
|
||||
|
||||
对流进行录制的功能插件,提供Flv和fmp4格式的录制功能。
|
||||
对流进行录制的功能插件,提供Flv、fmp4、hls格式的录制功能。
|
||||
|
||||
## 插件地址
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
- 配置中的path 表示要保存的文件的根路径,可以使用相对路径或者绝对路径
|
||||
- filter 代表要过滤的StreamPath正则表达式,如果不匹配,则表示不录制。为空代表不进行过滤
|
||||
- fragment表示分片大小(秒),0代表不分片
|
||||
|
||||
```yaml
|
||||
record:
|
||||
subscribe:
|
||||
@@ -28,16 +30,19 @@ record:
|
||||
path: ./flv
|
||||
autorecord: false
|
||||
filter: ""
|
||||
fragment: 0
|
||||
mp4:
|
||||
ext: .mp4
|
||||
path: ./mp4
|
||||
autorecord: false
|
||||
filter: ""
|
||||
fragment: 0
|
||||
hls:
|
||||
ext: .m3u8
|
||||
path: ./hls
|
||||
autorecord: false
|
||||
filter: ""
|
||||
fragment: 0
|
||||
```
|
||||
|
||||
## API
|
||||
|
106
config.go
Normal file
106
config.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FileWr interface {
|
||||
io.Reader
|
||||
io.Writer
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
type VideoFileInfo struct {
|
||||
Path string
|
||||
Size int64
|
||||
Duration uint32
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Ext string //文件扩展名
|
||||
Path string //存储文件的目录
|
||||
AutoRecord bool
|
||||
Filter string
|
||||
Fragment int //分片大小(秒)0表示不分片
|
||||
filterReg *regexp.Regexp
|
||||
fs http.Handler
|
||||
CreateFileFn func(filename string, append bool) (FileWr, error) `yaml:"-"`
|
||||
GetDurationFn func(file io.ReadSeeker) uint32 `yaml:"-"`
|
||||
}
|
||||
|
||||
func (r *Record) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.fs.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (r *Record) NeedRecord(streamPath string) bool {
|
||||
return r.AutoRecord && (r.filterReg == nil || r.filterReg.MatchString(streamPath))
|
||||
}
|
||||
|
||||
func (r *Record) Init() {
|
||||
os.MkdirAll(r.Path, 0755)
|
||||
if r.Filter != "" {
|
||||
r.filterReg = regexp.MustCompile(r.Filter)
|
||||
}
|
||||
r.fs = http.FileServer(http.Dir(r.Path))
|
||||
r.CreateFileFn = func(filename string, append bool) (file FileWr, err error) {
|
||||
filePath := filepath.Join(r.Path, filename)
|
||||
flag := os.O_CREATE
|
||||
if append {
|
||||
flag = flag | os.O_RDWR | os.O_APPEND
|
||||
} else {
|
||||
flag = flag | os.O_TRUNC | os.O_WRONLY
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return file, err
|
||||
}
|
||||
file, err = os.OpenFile(filePath, flag, 0755)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Record) Tree(dstPath string, level int) (files []*VideoFileInfo, err error) {
|
||||
var dstF *os.File
|
||||
dstF, err = os.Open(dstPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer dstF.Close()
|
||||
fileInfo, err := dstF.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !fileInfo.IsDir() { //如果dstF是文件
|
||||
if path.Ext(fileInfo.Name()) == r.Ext {
|
||||
p := strings.TrimPrefix(dstPath, r.Path)
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
files = append(files, &VideoFileInfo{
|
||||
Path: strings.TrimPrefix(p, "/"),
|
||||
Size: fileInfo.Size(),
|
||||
Duration: r.GetDurationFn(dstF),
|
||||
})
|
||||
}
|
||||
return
|
||||
} else { //如果dstF是文件夹
|
||||
var dir []os.FileInfo
|
||||
dir, err = dstF.Readdir(0) //获取文件夹下各个文件或文件夹的fileInfo
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, fileInfo = range dir {
|
||||
var _files []*VideoFileInfo
|
||||
_files, err = r.Tree(filepath.Join(dstPath, fileInfo.Name()), level+1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
files = append(files, _files...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
47
flv.go
Normal file
47
flv.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/codec"
|
||||
)
|
||||
|
||||
type FLVRecorder struct {
|
||||
Recorder
|
||||
}
|
||||
|
||||
func (r *FLVRecorder) OnEvent(event any) {
|
||||
r.Recorder.OnEvent(event)
|
||||
switch v := event.(type) {
|
||||
case ISubscriber:
|
||||
// 写入文件头
|
||||
if !r.append {
|
||||
r.Write(codec.FLVHeader)
|
||||
}
|
||||
case HaveFLV:
|
||||
if r.Fragment != 0 {
|
||||
if r.newFile {
|
||||
r.newFile = false
|
||||
if file, err := r.CreateFileFn(filepath.Join(r.Stream.Path, strconv.FormatInt(time.Now().Unix(), 10)+r.Ext), false); err == nil {
|
||||
r.SetIO(file)
|
||||
r.Write(codec.FLVHeader)
|
||||
if r.Video.Track != nil {
|
||||
flvTag := VideoDeConf(r.Video.Track.DecoderConfiguration).GetFLV()
|
||||
flvTag.WriteTo(r)
|
||||
}
|
||||
if r.Audio.Track != nil && r.Audio.Track.CodecID == codec.CodecID_AAC {
|
||||
flvTag := AudioDeConf(r.Audio.Track.DecoderConfiguration).GetFLV()
|
||||
flvTag.WriteTo(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flvTag := v.GetFLV()
|
||||
if _, err := flvTag.WriteTo(r); err != nil {
|
||||
r.Stop()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/codec"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
Subscriber
|
||||
Append bool
|
||||
}
|
||||
|
||||
func (r *Recorder) OnEvent(event any) {
|
||||
switch v := event.(type) {
|
||||
case ISubscriber:
|
||||
// 写入文件头
|
||||
if !r.Append {
|
||||
r.Write(codec.FLVHeader)
|
||||
}
|
||||
case HaveFLV:
|
||||
flvTag := v.GetFLV()
|
||||
if _, err := flvTag.WriteTo(r); err != nil {
|
||||
r.Stop()
|
||||
}
|
||||
}
|
||||
r.Subscriber.OnEvent(event)
|
||||
}
|
112
hls.go
Normal file
112
hls.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/codec"
|
||||
"m7s.live/engine/v4/codec/mpegts"
|
||||
"m7s.live/plugin/hls/v4"
|
||||
)
|
||||
|
||||
type HLSRecorder struct {
|
||||
playlist hls.Playlist
|
||||
asc codec.AudioSpecificConfig
|
||||
video_cc, audio_cc uint16
|
||||
packet mpegts.MpegTsPESPacket
|
||||
Recorder
|
||||
tsWriter io.WriteCloser
|
||||
}
|
||||
|
||||
func (h *HLSRecorder) OnEvent(event any) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
h.Error("HLSRecorder Stop", zap.Error(err))
|
||||
h.Stop()
|
||||
}
|
||||
}()
|
||||
h.Recorder.OnEvent(event)
|
||||
switch v := event.(type) {
|
||||
case *HLSRecorder:
|
||||
h.playlist = hls.Playlist{
|
||||
Writer: h.Writer,
|
||||
Version: 3,
|
||||
Sequence: 0,
|
||||
Targetduration: h.Fragment * 1000,
|
||||
}
|
||||
if err = h.playlist.Init(); err != nil {
|
||||
return
|
||||
}
|
||||
if err = h.createHlsTsSegmentFile(); err != nil {
|
||||
return
|
||||
}
|
||||
case AudioDeConf:
|
||||
h.asc, err = hls.DecodeAudioSpecificConfig(v.AVCC[0])
|
||||
case *AudioFrame:
|
||||
if h.packet, err = hls.AudioPacketToPES(v, h.asc); err != nil {
|
||||
return
|
||||
}
|
||||
pes := &mpegts.MpegtsPESFrame{
|
||||
Pid: 0x102,
|
||||
IsKeyFrame: false,
|
||||
ContinuityCounter: byte(h.audio_cc % 16),
|
||||
ProgramClockReferenceBase: uint64(v.DTS),
|
||||
}
|
||||
//frame.ProgramClockReferenceBase = 0
|
||||
if err = mpegts.WritePESPacket(h.tsWriter, pes, h.packet); err != nil {
|
||||
return
|
||||
}
|
||||
h.audio_cc = uint16(pes.ContinuityCounter)
|
||||
case *VideoFrame:
|
||||
h.packet, err = hls.VideoPacketToPES(v, h.Video.Track.DecoderConfiguration)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if h.Fragment != 0 {
|
||||
if h.newFile {
|
||||
h.newFile = false
|
||||
h.createHlsTsSegmentFile()
|
||||
}
|
||||
}
|
||||
pes := &mpegts.MpegtsPESFrame{
|
||||
Pid: 0x101,
|
||||
IsKeyFrame: v.IFrame,
|
||||
ContinuityCounter: byte(h.video_cc % 16),
|
||||
ProgramClockReferenceBase: uint64(v.DTS),
|
||||
}
|
||||
if err = mpegts.WritePESPacket(h.tsWriter, pes, h.packet); err != nil {
|
||||
return
|
||||
}
|
||||
h.video_cc = uint16(pes.ContinuityCounter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 创建一个新的ts文件
|
||||
func (h *HLSRecorder) createHlsTsSegmentFile() (err error) {
|
||||
tsFilename := strconv.FormatInt(time.Now().Unix(), 10) + ".ts"
|
||||
fw, err := h.CreateFileFn(filepath.Join(h.Stream.Path, tsFilename), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.tsWriter = fw
|
||||
inf := hls.PlaylistInf{
|
||||
Duration: float64(h.Fragment),
|
||||
Title: tsFilename,
|
||||
}
|
||||
if err = h.playlist.WriteInf(inf); err != nil {
|
||||
return
|
||||
}
|
||||
if err = mpegts.WriteDefaultPATPacket(fw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = mpegts.WriteDefaultPMTPacket(fw); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
104
main.go
104
main.go
@@ -4,38 +4,33 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/codec"
|
||||
"m7s.live/engine/v4/config"
|
||||
"m7s.live/engine/v4/util"
|
||||
"m7s.live/plugin/record/v4/flv"
|
||||
"m7s.live/plugin/record/v4/mp4"
|
||||
)
|
||||
|
||||
type RecordConfig struct {
|
||||
config.Subscribe
|
||||
Flv config.Record
|
||||
Mp4 config.Record
|
||||
Hls config.Record
|
||||
Flv Record
|
||||
Mp4 Record
|
||||
Hls Record
|
||||
recordings sync.Map
|
||||
}
|
||||
|
||||
var recordConfig = &RecordConfig{
|
||||
Flv: config.Record{
|
||||
Flv: Record{
|
||||
Path: "./flv",
|
||||
Ext: ".flv",
|
||||
GetDurationFn: getDuration,
|
||||
},
|
||||
Mp4: config.Record{
|
||||
Mp4: Record{
|
||||
Path: "./mp4",
|
||||
Ext: ".mp4",
|
||||
},
|
||||
Hls: config.Record{
|
||||
Hls: Record{
|
||||
Path: "./hls",
|
||||
Ext: ".m3u8",
|
||||
},
|
||||
@@ -51,22 +46,19 @@ func (conf *RecordConfig) OnEvent(event any) {
|
||||
conf.Hls.Init()
|
||||
case SEpublish:
|
||||
if conf.Flv.NeedRecord(v.Stream.Path) {
|
||||
var recorder flv.Recorder
|
||||
if file, err := conf.Flv.CreateFileFn(v.Stream.Path+".flv", recorder.Append); err == nil {
|
||||
go func() {
|
||||
plugin.SubscribeBlock(v.Stream.Path, &recorder)
|
||||
file.Close()
|
||||
}()
|
||||
}
|
||||
var flv FLVRecorder
|
||||
flv.Record = &conf.Flv
|
||||
plugin.Subscribe(v.Stream.Path, &flv)
|
||||
}
|
||||
if conf.Mp4.NeedRecord(v.Stream.Path) {
|
||||
if file, err := conf.Mp4.CreateFileFn(v.Stream.Path+".mp4", false); err == nil {
|
||||
recorder := mp4.NewRecorder(file)
|
||||
go func() {
|
||||
plugin.SubscribeBlock(v.Stream.Path, recorder)
|
||||
recorder.Close()
|
||||
}()
|
||||
}
|
||||
mp4 := NewMP4Recorder()
|
||||
mp4.Record = &conf.Mp4
|
||||
plugin.Subscribe(v.Stream.Path, mp4)
|
||||
}
|
||||
if conf.Hls.NeedRecord(v.Stream.Path) {
|
||||
var hls HLSRecorder
|
||||
hls.Record = &conf.Hls
|
||||
plugin.Subscribe(v.Stream.Path, &hls)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +66,7 @@ func (conf *RecordConfig) OnEvent(event any) {
|
||||
func (conf *RecordConfig) API_list(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
t := query.Get("type")
|
||||
var recorder *config.Record
|
||||
var recorder *Record
|
||||
switch t {
|
||||
case "", "flv":
|
||||
recorder = &conf.Flv
|
||||
@@ -108,70 +100,42 @@ func (conf *RecordConfig) API_start(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
t := query.Get("type")
|
||||
var recorderConf *config.Record
|
||||
var recorder ISubscriber
|
||||
var filePath string
|
||||
var point unsafe.Pointer
|
||||
var closer io.Closer
|
||||
switch t {
|
||||
case "":
|
||||
t = "flv"
|
||||
fallthrough
|
||||
case "flv":
|
||||
recorderConf = &conf.Flv
|
||||
var flvRecoder flv.Recorder
|
||||
var flvRecoder FLVRecorder
|
||||
flvRecoder.Record = &conf.Flv
|
||||
recorder = &flvRecoder
|
||||
point = unsafe.Pointer(&flvRecoder)
|
||||
filePath = filepath.Join(recorderConf.Path, streamPath+".flv")
|
||||
flvRecoder.Append = query.Get("append") != "" && util.Exist(filePath)
|
||||
file, err := recorderConf.CreateFileFn(filePath, flvRecoder.Append)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
closer = file
|
||||
flvRecoder.append = query.Get("append") != "" && util.Exist(filePath)
|
||||
case "mp4":
|
||||
recorderConf = &conf.Mp4
|
||||
filePath = filepath.Join(recorderConf.Path, streamPath+".mp4")
|
||||
file, err := recorderConf.CreateFileFn(filePath, false)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
mp4Recorder := mp4.NewRecorder(file)
|
||||
recorder = mp4Recorder
|
||||
point = unsafe.Pointer(mp4Recorder)
|
||||
closer = mp4Recorder
|
||||
recorder := NewMP4Recorder()
|
||||
recorder.Record = &conf.Mp4
|
||||
case "hls":
|
||||
recorderConf = &conf.Hls
|
||||
recorder := &HLSRecorder{}
|
||||
recorder.Record = &conf.Hls
|
||||
default:
|
||||
http.Error(w, "type not supported", http.StatusBadRequest)
|
||||
}
|
||||
if err := plugin.Subscribe(streamPath, recorder); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
closer.Close()
|
||||
} else {
|
||||
conf.recordings.Store(uintptr(point), recorder)
|
||||
go func() {
|
||||
recorder.PlayBlock()
|
||||
conf.recordings.Delete(uintptr(point))
|
||||
closer.Close()
|
||||
}()
|
||||
w.Write([]byte(strconv.FormatUint(uint64(uintptr(point)), 10)))
|
||||
return
|
||||
}
|
||||
id := streamPath + "/" + t
|
||||
recorder.GetIO().ID = id
|
||||
conf.recordings.Store(id, recorder)
|
||||
w.Write([]byte(id))
|
||||
}
|
||||
|
||||
func (conf *RecordConfig) API_stop(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
id := query.Get("id")
|
||||
num, err := strconv.ParseInt(id, 10, 0)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if recorder, ok := conf.recordings.Load(uintptr(num)); ok {
|
||||
if recorder, ok := conf.recordings.Load(r.URL.Query().Get("id")); ok {
|
||||
recorder.(ISubscriber).Stop()
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
http.Error(w, "no such recorder", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func getDuration(file io.ReadSeeker) uint32 {
|
||||
|
@@ -1,11 +1,14 @@
|
||||
package mp4
|
||||
package record
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/edgeware/mp4ff/aac"
|
||||
"github.com/edgeware/mp4ff/mp4"
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/codec"
|
||||
"m7s.live/engine/v4/config"
|
||||
"m7s.live/engine/v4/track"
|
||||
"m7s.live/engine/v4/util"
|
||||
)
|
||||
@@ -17,12 +20,26 @@ var defaultFtyp = mp4.NewFtyp("isom", 0x200, []string{
|
||||
type mediaContext struct {
|
||||
trackId uint32
|
||||
fragment *mp4.Fragment
|
||||
ts uint32 // 起始时间戳
|
||||
ts uint32 // 每个小片段起始时间戳
|
||||
abs uint32 // 绝对起始时间戳
|
||||
absSet bool // 是否设置过abs
|
||||
}
|
||||
|
||||
func (m *mediaContext) push(recoder *Recorder, dt uint32, dur uint32, data []byte, flags uint32) {
|
||||
func (m *mediaContext) reset(recoder *MP4Recorder) {
|
||||
if m.fragment != nil {
|
||||
m.fragment.Encode(recoder)
|
||||
m.fragment = nil
|
||||
m.absSet = false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mediaContext) push(recoder *MP4Recorder, dt uint32, dur uint32, data []byte, flags uint32) {
|
||||
if !m.absSet {
|
||||
m.abs = dt
|
||||
m.absSet = true
|
||||
}
|
||||
if m.fragment != nil && dt-m.ts > 5000 {
|
||||
m.fragment.Encode(recoder.fp)
|
||||
m.fragment.Encode(recoder)
|
||||
m.fragment = nil
|
||||
}
|
||||
if m.fragment == nil {
|
||||
@@ -32,7 +49,7 @@ func (m *mediaContext) push(recoder *Recorder, dt uint32, dur uint32, data []byt
|
||||
}
|
||||
m.fragment.AddFullSample(mp4.FullSample{
|
||||
Data: data,
|
||||
DecodeTime: uint64(dt),
|
||||
DecodeTime: uint64(dt - m.abs),
|
||||
Sample: mp4.Sample{
|
||||
Flags: flags,
|
||||
Dur: dur,
|
||||
@@ -41,43 +58,43 @@ func (m *mediaContext) push(recoder *Recorder, dt uint32, dur uint32, data []byt
|
||||
})
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
Subscriber
|
||||
fp config.FileWr
|
||||
type MP4Recorder struct {
|
||||
Recorder
|
||||
*mp4.InitSegment `json:"-"`
|
||||
video mediaContext
|
||||
audio mediaContext
|
||||
seqNumber uint32
|
||||
}
|
||||
|
||||
func NewRecorder(fp config.FileWr) *Recorder {
|
||||
r := &Recorder{
|
||||
fp: fp,
|
||||
func NewMP4Recorder() *MP4Recorder {
|
||||
r := &MP4Recorder{
|
||||
InitSegment: mp4.CreateEmptyInit(),
|
||||
}
|
||||
r.InitSegment.Ftyp = defaultFtyp
|
||||
r.Moov.Mvhd.NextTrackID = 1
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Recorder) Close() error {
|
||||
if r.fp != nil {
|
||||
func (r *MP4Recorder) Close() error {
|
||||
if r.Writer != nil {
|
||||
if r.video.fragment != nil {
|
||||
r.video.fragment.Encode(r.fp)
|
||||
r.video.fragment.Encode(r.Writer)
|
||||
}
|
||||
if r.audio.fragment != nil {
|
||||
r.audio.fragment.Encode(r.fp)
|
||||
r.audio.fragment.Encode(r.Writer)
|
||||
}
|
||||
r.fp.Close()
|
||||
r.Closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) OnEvent(event any) {
|
||||
func (r *MP4Recorder) OnEvent(event any) {
|
||||
r.Recorder.OnEvent(event)
|
||||
switch v := event.(type) {
|
||||
case *track.Video:
|
||||
moov := r.Moov
|
||||
trackID := uint32(len(moov.Traks) + 1)
|
||||
moov.Mvhd.NextTrackID = trackID + 1
|
||||
trackID := moov.Mvhd.NextTrackID
|
||||
moov.Mvhd.NextTrackID++
|
||||
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "video", "chi")
|
||||
moov.AddChild(newTrak)
|
||||
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
||||
@@ -91,8 +108,8 @@ func (r *Recorder) OnEvent(event any) {
|
||||
r.AddTrack(v)
|
||||
case *track.Audio:
|
||||
moov := r.Moov
|
||||
trackID := uint32(len(moov.Traks) + 1)
|
||||
moov.Mvhd.NextTrackID = trackID + 1
|
||||
trackID := moov.Mvhd.NextTrackID
|
||||
moov.Mvhd.NextTrackID++
|
||||
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "audio", "chi")
|
||||
moov.AddChild(newTrak)
|
||||
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
||||
@@ -121,31 +138,38 @@ func (r *Recorder) OnEvent(event any) {
|
||||
stsd.AddChild(pcmu)
|
||||
}
|
||||
r.AddTrack(v)
|
||||
case ISubscriber:
|
||||
r.InitSegment.Ftyp.Encode(r)
|
||||
r.InitSegment.Moov.Encode(r)
|
||||
case *AudioFrame:
|
||||
if r.audio.trackId != 0 {
|
||||
if r.InitSegment != nil {
|
||||
r.InitSegment.Ftyp.Encode(r.fp)
|
||||
r.InitSegment.Moov.Encode(r.fp)
|
||||
r.InitSegment = nil
|
||||
}
|
||||
r.audio.push(r, v.AbsTime-r.Audio.First.AbsTime, v.DeltaTime, util.ConcatBuffers(v.Raw), mp4.SyncSampleFlags)
|
||||
r.audio.push(r, v.AbsTime, v.DeltaTime, util.ConcatBuffers(v.Raw), mp4.SyncSampleFlags)
|
||||
}
|
||||
case *VideoFrame:
|
||||
if r.Fragment != 0 {
|
||||
if r.newFile {
|
||||
r.newFile = false
|
||||
r.audio.reset(r)
|
||||
r.video.reset(r)
|
||||
if file, err := r.CreateFileFn(filepath.Join(r.Stream.Path, strconv.FormatInt(time.Now().Unix(), 10)+r.Ext), false); err == nil {
|
||||
r.SetIO(file)
|
||||
r.InitSegment = mp4.CreateEmptyInit()
|
||||
r.InitSegment.Ftyp = defaultFtyp
|
||||
r.Moov.Mvhd.NextTrackID = 1
|
||||
r.OnEvent(r.Video.Track)
|
||||
r.OnEvent(r.Audio.Track)
|
||||
r.InitSegment.Ftyp.Encode(r)
|
||||
r.InitSegment.Moov.Encode(r)
|
||||
r.seqNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.video.trackId != 0 {
|
||||
flag := mp4.NonSyncSampleFlags
|
||||
if v.IFrame {
|
||||
flag = mp4.SyncSampleFlags
|
||||
}
|
||||
if r.InitSegment != nil {
|
||||
r.InitSegment.Ftyp.Encode(r.fp)
|
||||
r.InitSegment.Moov.Encode(r.fp)
|
||||
r.InitSegment = nil
|
||||
}
|
||||
r.video.push(r, v.AbsTime-r.Video.First.AbsTime, v.DeltaTime, util.ConcatBuffers(v.AVCC)[5:], flag)
|
||||
r.video.push(r, v.AbsTime, v.DeltaTime, util.ConcatBuffers(v.AVCC)[5:], flag)
|
||||
}
|
||||
|
||||
default:
|
||||
r.Subscriber.OnEvent(event)
|
||||
}
|
||||
|
||||
}
|
43
subscriber.go
Normal file
43
subscriber.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
. "m7s.live/engine/v4"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
Subscriber
|
||||
*Record
|
||||
newFile bool // 创建了新的文件
|
||||
append bool // 是否追加模式
|
||||
}
|
||||
|
||||
func (r *Recorder) OnEvent(event any) {
|
||||
switch v := event.(type) {
|
||||
case ISubscriber:
|
||||
filename := strconv.FormatInt(time.Now().Unix(), 10) + r.Ext
|
||||
if r.Fragment == 0 {
|
||||
filename = r.Stream.Path + r.Ext
|
||||
} else {
|
||||
filename = filepath.Join(r.Stream.Path, filename)
|
||||
}
|
||||
if file, err := r.CreateFileFn(filename, r.append); err == nil {
|
||||
r.SetIO(file)
|
||||
go func() {
|
||||
r.PlayBlock()
|
||||
recordConfig.recordings.Delete(r.ID)
|
||||
r.Close()
|
||||
}()
|
||||
}
|
||||
case *VideoFrame:
|
||||
if ts := v.AbsTime; v.IFrame && int64(ts-r.Video.First.AbsTime) >= int64(r.Fragment*1000) {
|
||||
r.Video.First.AbsTime = ts
|
||||
r.newFile = true
|
||||
}
|
||||
default:
|
||||
r.Subscriber.OnEvent(event)
|
||||
}
|
||||
}
|
20
vod.go
20
vod.go
@@ -1,10 +1,7 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -18,18 +15,15 @@ func ext(path string) string {
|
||||
}
|
||||
|
||||
func (conf *RecordConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.RequestURI, "/record/")
|
||||
p := strings.TrimPrefix(r.RequestURI, "/")
|
||||
p = strings.TrimPrefix(p, "record/")
|
||||
r.URL.Path = p
|
||||
switch ext(p) {
|
||||
case ".flv":
|
||||
filePath := filepath.Join(conf.Flv.Path, p)
|
||||
if file, err := os.Open(filePath); err == nil {
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("Content-Type", "video/x-flv")
|
||||
io.Copy(w, file)
|
||||
} else {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
conf.Flv.ServeHTTP(w, r)
|
||||
case ".mp4":
|
||||
case ".m3u8":
|
||||
conf.Mp4.ServeHTTP(w, r)
|
||||
case ".m3u8", ".ts":
|
||||
conf.Hls.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user