mirror of
https://github.com/Monibuca/plugin-record.git
synced 2025-10-18 14:40:42 +08:00
增加分片录制能力以及hls录制能力
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# RECORD插件
|
# RECORD插件
|
||||||
|
|
||||||
对流进行录制的功能插件,提供Flv和fmp4格式的录制功能。
|
对流进行录制的功能插件,提供Flv、fmp4、hls格式的录制功能。
|
||||||
|
|
||||||
## 插件地址
|
## 插件地址
|
||||||
|
|
||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
|
|
||||||
- 配置中的path 表示要保存的文件的根路径,可以使用相对路径或者绝对路径
|
- 配置中的path 表示要保存的文件的根路径,可以使用相对路径或者绝对路径
|
||||||
- filter 代表要过滤的StreamPath正则表达式,如果不匹配,则表示不录制。为空代表不进行过滤
|
- filter 代表要过滤的StreamPath正则表达式,如果不匹配,则表示不录制。为空代表不进行过滤
|
||||||
|
- fragment表示分片大小(秒),0代表不分片
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
record:
|
record:
|
||||||
subscribe:
|
subscribe:
|
||||||
@@ -28,16 +30,19 @@ record:
|
|||||||
path: ./flv
|
path: ./flv
|
||||||
autorecord: false
|
autorecord: false
|
||||||
filter: ""
|
filter: ""
|
||||||
|
fragment: 0
|
||||||
mp4:
|
mp4:
|
||||||
ext: .mp4
|
ext: .mp4
|
||||||
path: ./mp4
|
path: ./mp4
|
||||||
autorecord: false
|
autorecord: false
|
||||||
filter: ""
|
filter: ""
|
||||||
|
fragment: 0
|
||||||
hls:
|
hls:
|
||||||
ext: .m3u8
|
ext: .m3u8
|
||||||
path: ./hls
|
path: ./hls
|
||||||
autorecord: false
|
autorecord: false
|
||||||
filter: ""
|
filter: ""
|
||||||
|
fragment: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## 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"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
. "m7s.live/engine/v4"
|
. "m7s.live/engine/v4"
|
||||||
"m7s.live/engine/v4/codec"
|
"m7s.live/engine/v4/codec"
|
||||||
"m7s.live/engine/v4/config"
|
"m7s.live/engine/v4/config"
|
||||||
"m7s.live/engine/v4/util"
|
"m7s.live/engine/v4/util"
|
||||||
"m7s.live/plugin/record/v4/flv"
|
|
||||||
"m7s.live/plugin/record/v4/mp4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordConfig struct {
|
type RecordConfig struct {
|
||||||
config.Subscribe
|
config.Subscribe
|
||||||
Flv config.Record
|
Flv Record
|
||||||
Mp4 config.Record
|
Mp4 Record
|
||||||
Hls config.Record
|
Hls Record
|
||||||
recordings sync.Map
|
recordings sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
var recordConfig = &RecordConfig{
|
var recordConfig = &RecordConfig{
|
||||||
Flv: config.Record{
|
Flv: Record{
|
||||||
Path: "./flv",
|
Path: "./flv",
|
||||||
Ext: ".flv",
|
Ext: ".flv",
|
||||||
GetDurationFn: getDuration,
|
GetDurationFn: getDuration,
|
||||||
},
|
},
|
||||||
Mp4: config.Record{
|
Mp4: Record{
|
||||||
Path: "./mp4",
|
Path: "./mp4",
|
||||||
Ext: ".mp4",
|
Ext: ".mp4",
|
||||||
},
|
},
|
||||||
Hls: config.Record{
|
Hls: Record{
|
||||||
Path: "./hls",
|
Path: "./hls",
|
||||||
Ext: ".m3u8",
|
Ext: ".m3u8",
|
||||||
},
|
},
|
||||||
@@ -51,22 +46,19 @@ func (conf *RecordConfig) OnEvent(event any) {
|
|||||||
conf.Hls.Init()
|
conf.Hls.Init()
|
||||||
case SEpublish:
|
case SEpublish:
|
||||||
if conf.Flv.NeedRecord(v.Stream.Path) {
|
if conf.Flv.NeedRecord(v.Stream.Path) {
|
||||||
var recorder flv.Recorder
|
var flv FLVRecorder
|
||||||
if file, err := conf.Flv.CreateFileFn(v.Stream.Path+".flv", recorder.Append); err == nil {
|
flv.Record = &conf.Flv
|
||||||
go func() {
|
plugin.Subscribe(v.Stream.Path, &flv)
|
||||||
plugin.SubscribeBlock(v.Stream.Path, &recorder)
|
|
||||||
file.Close()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if conf.Mp4.NeedRecord(v.Stream.Path) {
|
if conf.Mp4.NeedRecord(v.Stream.Path) {
|
||||||
if file, err := conf.Mp4.CreateFileFn(v.Stream.Path+".mp4", false); err == nil {
|
mp4 := NewMP4Recorder()
|
||||||
recorder := mp4.NewRecorder(file)
|
mp4.Record = &conf.Mp4
|
||||||
go func() {
|
plugin.Subscribe(v.Stream.Path, mp4)
|
||||||
plugin.SubscribeBlock(v.Stream.Path, recorder)
|
}
|
||||||
recorder.Close()
|
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) {
|
func (conf *RecordConfig) API_list(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
t := query.Get("type")
|
t := query.Get("type")
|
||||||
var recorder *config.Record
|
var recorder *Record
|
||||||
switch t {
|
switch t {
|
||||||
case "", "flv":
|
case "", "flv":
|
||||||
recorder = &conf.Flv
|
recorder = &conf.Flv
|
||||||
@@ -108,70 +100,42 @@ func (conf *RecordConfig) API_start(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
t := query.Get("type")
|
t := query.Get("type")
|
||||||
var recorderConf *config.Record
|
|
||||||
var recorder ISubscriber
|
var recorder ISubscriber
|
||||||
var filePath string
|
var filePath string
|
||||||
var point unsafe.Pointer
|
|
||||||
var closer io.Closer
|
|
||||||
switch t {
|
switch t {
|
||||||
case "":
|
case "":
|
||||||
t = "flv"
|
t = "flv"
|
||||||
fallthrough
|
fallthrough
|
||||||
case "flv":
|
case "flv":
|
||||||
recorderConf = &conf.Flv
|
var flvRecoder FLVRecorder
|
||||||
var flvRecoder flv.Recorder
|
flvRecoder.Record = &conf.Flv
|
||||||
recorder = &flvRecoder
|
recorder = &flvRecoder
|
||||||
point = unsafe.Pointer(&flvRecoder)
|
flvRecoder.append = query.Get("append") != "" && util.Exist(filePath)
|
||||||
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
|
|
||||||
case "mp4":
|
case "mp4":
|
||||||
recorderConf = &conf.Mp4
|
recorder := NewMP4Recorder()
|
||||||
filePath = filepath.Join(recorderConf.Path, streamPath+".mp4")
|
recorder.Record = &conf.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
|
|
||||||
case "hls":
|
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 {
|
if err := plugin.Subscribe(streamPath, recorder); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
closer.Close()
|
return
|
||||||
} 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)))
|
|
||||||
}
|
}
|
||||||
|
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) {
|
func (conf *RecordConfig) API_stop(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
if recorder, ok := conf.recordings.Load(r.URL.Query().Get("id")); ok {
|
||||||
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 {
|
|
||||||
recorder.(ISubscriber).Stop()
|
recorder.(ISubscriber).Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, "no such recorder", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDuration(file io.ReadSeeker) uint32 {
|
func getDuration(file io.ReadSeeker) uint32 {
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
package mp4
|
package record
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/edgeware/mp4ff/aac"
|
"github.com/edgeware/mp4ff/aac"
|
||||||
"github.com/edgeware/mp4ff/mp4"
|
"github.com/edgeware/mp4ff/mp4"
|
||||||
. "m7s.live/engine/v4"
|
. "m7s.live/engine/v4"
|
||||||
"m7s.live/engine/v4/codec"
|
"m7s.live/engine/v4/codec"
|
||||||
"m7s.live/engine/v4/config"
|
|
||||||
"m7s.live/engine/v4/track"
|
"m7s.live/engine/v4/track"
|
||||||
"m7s.live/engine/v4/util"
|
"m7s.live/engine/v4/util"
|
||||||
)
|
)
|
||||||
@@ -17,12 +20,26 @@ var defaultFtyp = mp4.NewFtyp("isom", 0x200, []string{
|
|||||||
type mediaContext struct {
|
type mediaContext struct {
|
||||||
trackId uint32
|
trackId uint32
|
||||||
fragment *mp4.Fragment
|
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 {
|
if m.fragment != nil && dt-m.ts > 5000 {
|
||||||
m.fragment.Encode(recoder.fp)
|
m.fragment.Encode(recoder)
|
||||||
m.fragment = nil
|
m.fragment = nil
|
||||||
}
|
}
|
||||||
if 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{
|
m.fragment.AddFullSample(mp4.FullSample{
|
||||||
Data: data,
|
Data: data,
|
||||||
DecodeTime: uint64(dt),
|
DecodeTime: uint64(dt - m.abs),
|
||||||
Sample: mp4.Sample{
|
Sample: mp4.Sample{
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Dur: dur,
|
Dur: dur,
|
||||||
@@ -41,43 +58,43 @@ func (m *mediaContext) push(recoder *Recorder, dt uint32, dur uint32, data []byt
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Recorder struct {
|
type MP4Recorder struct {
|
||||||
Subscriber
|
Recorder
|
||||||
fp config.FileWr
|
|
||||||
*mp4.InitSegment `json:"-"`
|
*mp4.InitSegment `json:"-"`
|
||||||
video mediaContext
|
video mediaContext
|
||||||
audio mediaContext
|
audio mediaContext
|
||||||
seqNumber uint32
|
seqNumber uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecorder(fp config.FileWr) *Recorder {
|
func NewMP4Recorder() *MP4Recorder {
|
||||||
r := &Recorder{
|
r := &MP4Recorder{
|
||||||
fp: fp,
|
|
||||||
InitSegment: mp4.CreateEmptyInit(),
|
InitSegment: mp4.CreateEmptyInit(),
|
||||||
}
|
}
|
||||||
r.InitSegment.Ftyp = defaultFtyp
|
r.InitSegment.Ftyp = defaultFtyp
|
||||||
|
r.Moov.Mvhd.NextTrackID = 1
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recorder) Close() error {
|
func (r *MP4Recorder) Close() error {
|
||||||
if r.fp != nil {
|
if r.Writer != nil {
|
||||||
if r.video.fragment != nil {
|
if r.video.fragment != nil {
|
||||||
r.video.fragment.Encode(r.fp)
|
r.video.fragment.Encode(r.Writer)
|
||||||
}
|
}
|
||||||
if r.audio.fragment != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recorder) OnEvent(event any) {
|
func (r *MP4Recorder) OnEvent(event any) {
|
||||||
|
r.Recorder.OnEvent(event)
|
||||||
switch v := event.(type) {
|
switch v := event.(type) {
|
||||||
case *track.Video:
|
case *track.Video:
|
||||||
moov := r.Moov
|
moov := r.Moov
|
||||||
trackID := uint32(len(moov.Traks) + 1)
|
trackID := moov.Mvhd.NextTrackID
|
||||||
moov.Mvhd.NextTrackID = trackID + 1
|
moov.Mvhd.NextTrackID++
|
||||||
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "video", "chi")
|
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "video", "chi")
|
||||||
moov.AddChild(newTrak)
|
moov.AddChild(newTrak)
|
||||||
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
||||||
@@ -91,8 +108,8 @@ func (r *Recorder) OnEvent(event any) {
|
|||||||
r.AddTrack(v)
|
r.AddTrack(v)
|
||||||
case *track.Audio:
|
case *track.Audio:
|
||||||
moov := r.Moov
|
moov := r.Moov
|
||||||
trackID := uint32(len(moov.Traks) + 1)
|
trackID := moov.Mvhd.NextTrackID
|
||||||
moov.Mvhd.NextTrackID = trackID + 1
|
moov.Mvhd.NextTrackID++
|
||||||
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "audio", "chi")
|
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "audio", "chi")
|
||||||
moov.AddChild(newTrak)
|
moov.AddChild(newTrak)
|
||||||
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
||||||
@@ -121,31 +138,38 @@ func (r *Recorder) OnEvent(event any) {
|
|||||||
stsd.AddChild(pcmu)
|
stsd.AddChild(pcmu)
|
||||||
}
|
}
|
||||||
r.AddTrack(v)
|
r.AddTrack(v)
|
||||||
|
case ISubscriber:
|
||||||
|
r.InitSegment.Ftyp.Encode(r)
|
||||||
|
r.InitSegment.Moov.Encode(r)
|
||||||
case *AudioFrame:
|
case *AudioFrame:
|
||||||
if r.audio.trackId != 0 {
|
if r.audio.trackId != 0 {
|
||||||
if r.InitSegment != nil {
|
r.audio.push(r, v.AbsTime, v.DeltaTime, util.ConcatBuffers(v.Raw), mp4.SyncSampleFlags)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
case *VideoFrame:
|
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 {
|
if r.video.trackId != 0 {
|
||||||
flag := mp4.NonSyncSampleFlags
|
flag := mp4.NonSyncSampleFlags
|
||||||
if v.IFrame {
|
if v.IFrame {
|
||||||
flag = mp4.SyncSampleFlags
|
flag = mp4.SyncSampleFlags
|
||||||
}
|
}
|
||||||
if r.InitSegment != nil {
|
r.video.push(r, v.AbsTime, v.DeltaTime, util.ConcatBuffers(v.AVCC)[5:], flag)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package record
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,18 +15,15 @@ func ext(path string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (conf *RecordConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
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) {
|
switch ext(p) {
|
||||||
case ".flv":
|
case ".flv":
|
||||||
filePath := filepath.Join(conf.Flv.Path, p)
|
conf.Flv.ServeHTTP(w, r)
|
||||||
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)
|
|
||||||
}
|
|
||||||
case ".mp4":
|
case ".mp4":
|
||||||
case ".m3u8":
|
conf.Mp4.ServeHTTP(w, r)
|
||||||
|
case ".m3u8", ".ts":
|
||||||
|
conf.Hls.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user