增加分片录制能力以及hls录制能力

This commit is contained in:
dexter
2022-05-26 11:29:24 +08:00
parent 48d7f1a65b
commit da81af3bf9
9 changed files with 417 additions and 149 deletions

View File

@@ -1,6 +1,6 @@
# RECORD插件 # RECORD插件
对流进行录制的功能插件提供Flvfmp4格式的录制功能。 对流进行录制的功能插件提供Flvfmp4、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
View 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
View 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()
}
}
}

View File

@@ -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
View 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
View File

@@ -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 {

View File

@@ -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
View 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
View File

@@ -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)
} }
} }