mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
feat: mp4 pull from record files
This commit is contained in:
@@ -12,7 +12,6 @@ import (
|
||||
_ "m7s.live/m7s/v5/plugin/monitor"
|
||||
_ "m7s.live/m7s/v5/plugin/mp4"
|
||||
_ "m7s.live/m7s/v5/plugin/preview"
|
||||
_ "m7s.live/m7s/v5/plugin/record"
|
||||
_ "m7s.live/m7s/v5/plugin/rtmp"
|
||||
_ "m7s.live/m7s/v5/plugin/rtsp"
|
||||
_ "m7s.live/m7s/v5/plugin/stress"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
global:
|
||||
loglevel: debug
|
||||
mp4:
|
||||
# pull:
|
||||
# record/test: record/live/test?start=2024-09-09T13:11:50
|
||||
publish:
|
||||
delayclosetimeout: 1s
|
||||
onsub:
|
||||
pull:
|
||||
^vod/(.+)$: record/$1
|
||||
onpub:
|
||||
record:
|
||||
.+:
|
||||
filepath: record/$0.mp4
|
||||
^live/.+:
|
||||
fragment: 1m
|
||||
filepath: record/$0
|
||||
@@ -156,23 +156,21 @@ func (r *AVRingReader) ReadFrame(conf *config.Subscribe) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.AbsTime = uint32((r.Value.Timestamp - r.SkipTs).Milliseconds())
|
||||
if r.AbsTime == 0 {
|
||||
r.AbsTime = 1
|
||||
|
||||
if r.Value.Timestamp < r.SkipTs {
|
||||
r.Error("timestamp < skipTs", "ts", r.Value.Timestamp, "skipTs", r.SkipTs)
|
||||
r.AbsTime++
|
||||
} else {
|
||||
r.AbsTime = uint32((r.Value.Timestamp - r.SkipTs).Milliseconds())
|
||||
if r.AbsTime == 0 {
|
||||
r.AbsTime = 1
|
||||
}
|
||||
}
|
||||
r.Delay = uint32(r.Track.LastValue.Sequence - r.Value.Sequence)
|
||||
r.Delay = r.Track.LastValue.Sequence - r.Value.Sequence
|
||||
r.Log(context.TODO(), task.TraceLevel, r.Track.FourCC().String(), "delay", r.Delay)
|
||||
return
|
||||
}
|
||||
|
||||
// func (r *AVRingReader) GetPTS32() uint32 {
|
||||
// return uint32((r.Value.Raw.Timestamp - r.SkipTs*90/time.Millisecond))
|
||||
// }
|
||||
|
||||
// func (r *AVRingReader) GetDTS32() uint32 {
|
||||
// return uint32((r.Value.CTS - r.SkipTs*90/time.Millisecond))
|
||||
// }
|
||||
|
||||
func (r *AVRingReader) ResetAbsTime() {
|
||||
r.SkipTs = r.Value.Timestamp
|
||||
r.AbsTime = 1
|
||||
|
||||
@@ -132,7 +132,11 @@ func (config *Config) Parse(s any, prefix ...string) {
|
||||
prop := config.Get(name)
|
||||
|
||||
prop.tag = ft.Tag
|
||||
prop.Parse(fv, append(prefix, strings.ToUpper(ft.Name))...)
|
||||
if len(prefix) > 0 {
|
||||
prop.Parse(fv, append(prefix, strings.ToUpper(ft.Name))...)
|
||||
} else {
|
||||
prop.Parse(fv)
|
||||
}
|
||||
for _, kv := range strings.Split(ft.Tag.Get("enum"), ",") {
|
||||
kvs := strings.Split(kv, ":")
|
||||
if len(kvs) != 2 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
"io"
|
||||
@@ -18,9 +19,13 @@ type RawAudio struct {
|
||||
util.RecyclableMemory
|
||||
}
|
||||
|
||||
func (r *RawAudio) Parse(track *AVTrack) error {
|
||||
func (r *RawAudio) Parse(track *AVTrack) (err error) {
|
||||
if track.ICodecCtx == nil {
|
||||
switch r.FourCC {
|
||||
case codec.FourCC_MP4A:
|
||||
ctx := &codec.AACCtx{}
|
||||
ctx.CodecData, err = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(r.ToBytes())
|
||||
track.ICodecCtx = ctx
|
||||
case codec.FourCC_ALAW:
|
||||
track.ICodecCtx = &codec.PCMACtx{
|
||||
AudioCtx: codec.AudioCtx{
|
||||
@@ -39,7 +44,7 @@ func (r *RawAudio) Parse(track *AVTrack) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RawAudio) ConvertCtx(ctx codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error) {
|
||||
|
||||
@@ -129,6 +129,8 @@ func (r *MemoryReader) ReadBytesTo(buf []byte) (actual int) {
|
||||
}
|
||||
l := n
|
||||
for n > 0 {
|
||||
curBuf = r.GetCurrent()
|
||||
curBufLen = len(curBuf)
|
||||
if n < curBufLen {
|
||||
actual += n
|
||||
copy(buf[l-n:], curBuf[:n])
|
||||
|
||||
@@ -486,7 +486,7 @@ func (p *Plugin) Subscribe(ctx context.Context, streamPath string) (subscriber *
|
||||
}
|
||||
|
||||
func (p *Plugin) Pull(streamPath string, conf config.Pull) {
|
||||
puller := p.Meta.Puller()
|
||||
puller := p.Meta.Puller(conf)
|
||||
puller.GetPullJob().Init(puller, p, streamPath, conf)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,9 @@ package plugin_flv
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"m7s.live/m7s/v5/pkg/util"
|
||||
|
||||
"m7s.live/m7s/v5"
|
||||
|
||||
. "m7s.live/m7s/v5/plugin/flv/pkg"
|
||||
@@ -27,30 +23,31 @@ var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPuller, NewRecorder)
|
||||
|
||||
func (plugin *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".flv")
|
||||
query := r.URL.Query()
|
||||
speedStr := query.Get("speed")
|
||||
speed, err := strconv.ParseFloat(speedStr, 64)
|
||||
//query := r.URL.Query()
|
||||
//speedStr := query.Get("speed")
|
||||
//speed, err := strconv.ParseFloat(speedStr, 64)
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
speed = 1
|
||||
}
|
||||
if startTime, err := util.TimeQueryParse(query.Get("start")); err == nil {
|
||||
var vod Vod
|
||||
if err = vod.Init(startTime, filepath.Join(plugin.Path, streamPath)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
vod.Writer = w
|
||||
vod.SetSpeed(speed)
|
||||
plugin.Info("vod start", "streamPath", streamPath, "startTime", startTime, "speed", speed)
|
||||
err = vod.Run(r.Context())
|
||||
plugin.Info("vod done", "streamPath", streamPath, "err", err)
|
||||
return
|
||||
}
|
||||
//if err != nil {
|
||||
// speed = 1
|
||||
//}
|
||||
//if startTime, err := util.TimeQueryParse(query.Get("start")); err == nil {
|
||||
// var vod Vod
|
||||
// if err = vod.Init(startTime, filepath.Join(plugin.Path, streamPath)); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// vod.Writer = w
|
||||
// vod.SetSpeed(speed)
|
||||
// plugin.Info("vod start", "streamPath", streamPath, "startTime", startTime, "speed", speed)
|
||||
// err = vod.Run(r.Context())
|
||||
// plugin.Info("vod done", "streamPath", streamPath, "err", err)
|
||||
// return
|
||||
//}
|
||||
var live Live
|
||||
if r.URL.RawQuery != "" {
|
||||
streamPath += "?" + r.URL.RawQuery
|
||||
|
||||
@@ -2,6 +2,7 @@ package flv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"m7s.live/m7s/v5/pkg/config"
|
||||
|
||||
"m7s.live/m7s/v5"
|
||||
"m7s.live/m7s/v5/pkg/util"
|
||||
@@ -12,7 +13,7 @@ type Puller struct {
|
||||
m7s.HTTPFilePuller
|
||||
}
|
||||
|
||||
func NewPuller() m7s.IPuller {
|
||||
func NewPuller(_ config.Pull) m7s.IPuller {
|
||||
return &Puller{}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,80 @@ const (
|
||||
FullBoxLen = 12
|
||||
)
|
||||
|
||||
var (
|
||||
TypeFTYP = [4]byte{'f', 't', 'y', 'p'}
|
||||
TypeSTYP = [4]byte{'s', 't', 'y', 'p'}
|
||||
TypeMOOV = [4]byte{'m', 'o', 'o', 'v'}
|
||||
TypeMVHD = [4]byte{'m', 'v', 'h', 'd'}
|
||||
TypeTRAK = [4]byte{'t', 'r', 'a', 'k'}
|
||||
TypeTKHD = [4]byte{'t', 'k', 'h', 'd'}
|
||||
TypeMDIA = [4]byte{'m', 'd', 'i', 'a'}
|
||||
TypeMDHD = [4]byte{'m', 'd', 'h', 'd'}
|
||||
TypeHDLR = [4]byte{'h', 'd', 'l', 'r'}
|
||||
TypeMINF = [4]byte{'m', 'i', 'n', 'f'}
|
||||
TypeSTBL = [4]byte{'s', 't', 'b', 'l'}
|
||||
TypeSTSD = [4]byte{'s', 't', 's', 'd'}
|
||||
TypeSTTS = [4]byte{'s', 't', 't', 's'}
|
||||
TypeSTSC = [4]byte{'s', 't', 's', 'c'}
|
||||
TypeSTSZ = [4]byte{'s', 't', 's', 'z'}
|
||||
TypeSTCO = [4]byte{'s', 't', 'c', 'o'}
|
||||
TypeMDAT = [4]byte{'m', 'd', 'a', 't'}
|
||||
TypeFREE = [4]byte{'f', 'r', 'e', 'e'}
|
||||
TypeUUID = [4]byte{'u', 'u', 'i', 'd'}
|
||||
|
||||
TypeVMHD = [4]byte{'v', 'm', 'h', 'd'}
|
||||
TypeSMHD = [4]byte{'s', 'm', 'h', 'd'}
|
||||
TypeHMHD = [4]byte{'h', 'm', 'h', 'd'}
|
||||
TypeNMHD = [4]byte{'n', 'm', 'h', 'd'}
|
||||
TypeCTTS = [4]byte{'c', 't', 't', 's'}
|
||||
TypeCO64 = [4]byte{'c', 'o', '6', '4'}
|
||||
TypePSSH = [4]byte{'p', 's', 's', 'h'}
|
||||
|
||||
TypeSTSS = [4]byte{'s', 't', 's', 's'}
|
||||
TypeENCv = [4]byte{'e', 'n', 'c', 'v'}
|
||||
TypeSINF = [4]byte{'s', 'i', 'n', 'f'}
|
||||
TypeFRMA = [4]byte{'f', 'r', 'm', 'a'}
|
||||
TypeSCHI = [4]byte{'s', 'c', 'h', 'i'}
|
||||
TypeTENC = [4]byte{'t', 'e', 'n', 'c'}
|
||||
TypeAVC1 = [4]byte{'a', 'v', 'c', '1'}
|
||||
TypeHVC1 = [4]byte{'h', 'v', 'c', '1'}
|
||||
TypeHEV1 = [4]byte{'h', 'e', 'v', '1'}
|
||||
TypeENCA = [4]byte{'e', 'n', 'c', 'a'}
|
||||
TypeMP4A = [4]byte{'m', 'p', '4', 'a'}
|
||||
TypeULAW = [4]byte{'u', 'l', 'a', 'w'}
|
||||
TypeALAW = [4]byte{'a', 'l', 'a', 'w'}
|
||||
TypeOPUS = [4]byte{'o', 'p', 'u', 's'}
|
||||
TypeAVCC = [4]byte{'a', 'v', 'c', 'C'}
|
||||
TypeHVCC = [4]byte{'h', 'v', 'c', 'C'}
|
||||
TypeESDS = [4]byte{'e', 's', 'd', 's'}
|
||||
TypeEDTS = [4]byte{'e', 'd', 't', 's'}
|
||||
TypeELST = [4]byte{'e', 'l', 's', 't'}
|
||||
TypeMVEX = [4]byte{'m', 'v', 'e', 'x'}
|
||||
TypeMOOF = [4]byte{'m', 'o', 'o', 'f'}
|
||||
TypeMFHD = [4]byte{'m', 'f', 'h', 'd'}
|
||||
TypeTRAF = [4]byte{'t', 'r', 'a', 'f'}
|
||||
TypeTFHD = [4]byte{'t', 'f', 'h', 'd'}
|
||||
TypeTFDT = [4]byte{'t', 'f', 'd', 't'}
|
||||
TypeTRUN = [4]byte{'t', 'r', 'u', 'n'}
|
||||
TypeSENC = [4]byte{'s', 'e', 'n', 'c'}
|
||||
TypeSAIZ = [4]byte{'s', 'a', 'i', 'z'}
|
||||
TypeSAIO = [4]byte{'s', 'a', 'i', 'o'}
|
||||
TypeSGPD = [4]byte{'s', 'g', 'p', 'd'}
|
||||
TypeWAVE = [4]byte{'w', 'a', 'v', 'e'}
|
||||
TypeMSDH = [4]byte{'m', 's', 'd', 'h'}
|
||||
TypeMSIX = [4]byte{'m', 's', 'i', 'x'}
|
||||
TypeISOM = [4]byte{'i', 's', 'o', 'm'}
|
||||
TypeISO2 = [4]byte{'i', 's', 'o', '2'}
|
||||
TypeISO3 = [4]byte{'i', 's', 'o', '3'}
|
||||
TypeISO4 = [4]byte{'i', 's', 'o', '4'}
|
||||
TypeISO5 = [4]byte{'i', 's', 'o', '5'}
|
||||
TypeISO6 = [4]byte{'i', 's', 'o', '6'}
|
||||
TypeMP41 = [4]byte{'m', 'p', '4', '1'}
|
||||
TypeMP42 = [4]byte{'m', 'p', '4', '2'}
|
||||
TypeDASH = [4]byte{'d', 'a', 's', 'h'}
|
||||
TypeMFRA = [4]byte{'m', 'f', 'r', 'a'}
|
||||
)
|
||||
|
||||
type BoxEncoder interface {
|
||||
Encode(buf []byte) (int, []byte)
|
||||
}
|
||||
@@ -31,7 +105,7 @@ type BoxSize interface {
|
||||
// } else if (size==0) {
|
||||
// // box extends to end of file
|
||||
// }
|
||||
// if (boxtype==‘uuid’) {
|
||||
// if (boxtype=='uuid') {
|
||||
// unsigned int(8)[16] usertype = extended_type;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -2,7 +2,6 @@ package box
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"io"
|
||||
"m7s.live/m7s/v5/pkg/util"
|
||||
)
|
||||
@@ -106,19 +105,19 @@ func (demuxer *MovDemuxer) ReadHead() ([]TrackInfo, error) {
|
||||
err = errors.New("mp4 Parser error")
|
||||
break
|
||||
}
|
||||
switch mov_tag(basebox.Type) {
|
||||
case mov_tag([4]byte{'f', 't', 'y', 'p'}):
|
||||
switch basebox.Type {
|
||||
case TypeFTYP:
|
||||
err = decodeFtypBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'f', 'r', 'e', 'e'}):
|
||||
case TypeFREE:
|
||||
err = decodeFreeBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'m', 'd', 'a', 't'}):
|
||||
case TypeMDAT:
|
||||
var currentOffset int64
|
||||
if currentOffset, err = demuxer.reader.Seek(0, io.SeekCurrent); err != nil {
|
||||
break
|
||||
}
|
||||
demuxer.mdatOffset = append(demuxer.mdatOffset, uint64(currentOffset))
|
||||
_, err = demuxer.reader.Seek(int64(basebox.Size)-BasicBoxLen, io.SeekCurrent)
|
||||
case mov_tag([4]byte{'m', 'o', 'o', 'v'}):
|
||||
case TypeMOOV:
|
||||
var currentOffset int64
|
||||
if currentOffset, err = demuxer.reader.Seek(0, io.SeekCurrent); err != nil {
|
||||
break
|
||||
@@ -132,114 +131,113 @@ func (demuxer *MovDemuxer) ReadHead() ([]TrackInfo, error) {
|
||||
break
|
||||
}
|
||||
_, err = demuxer.reader.Seek(currentOffset, io.SeekStart)
|
||||
case mov_tag([4]byte{'m', 'v', 'h', 'd'}):
|
||||
case TypeMVHD:
|
||||
err = decodeMvhd(demuxer)
|
||||
case mov_tag([4]byte{'p', 's', 's', 'h'}):
|
||||
case TypePSSH:
|
||||
err = decodePsshBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'t', 'r', 'a', 'k'}):
|
||||
track := &mp4track{}
|
||||
demuxer.tracks = append(demuxer.tracks, track)
|
||||
case mov_tag([4]byte{'t', 'k', 'h', 'd'}):
|
||||
case TypeTRAK:
|
||||
demuxer.tracks = append(demuxer.tracks, &mp4track{})
|
||||
case TypeTKHD:
|
||||
err = decodeTkhdBox(demuxer)
|
||||
case mov_tag([4]byte{'m', 'd', 'h', 'd'}):
|
||||
case TypeMDHD:
|
||||
err = decodeMdhdBox(demuxer)
|
||||
case mov_tag([4]byte{'h', 'd', 'l', 'r'}):
|
||||
case TypeHDLR:
|
||||
err = decodeHdlrBox(demuxer, basebox.Size)
|
||||
case mov_tag([4]byte{'m', 'd', 'i', 'a'}):
|
||||
case mov_tag([4]byte{'m', 'i', 'n', 'f'}):
|
||||
case mov_tag([4]byte{'v', 'm', 'h', 'd'}):
|
||||
case TypeMDIA:
|
||||
case TypeMINF:
|
||||
case TypeVMHD:
|
||||
err = decodeVmhdBox(demuxer)
|
||||
case mov_tag([4]byte{'s', 'm', 'h', 'd'}):
|
||||
case TypeSMHD:
|
||||
err = decodeSmhdBox(demuxer)
|
||||
case mov_tag([4]byte{'h', 'm', 'h', 'd'}):
|
||||
case TypeHMHD:
|
||||
_, err = fullbox.Decode(demuxer.reader)
|
||||
case mov_tag([4]byte{'n', 'm', 'h', 'd'}):
|
||||
case TypeNMHD:
|
||||
_, err = fullbox.Decode(demuxer.reader)
|
||||
case mov_tag([4]byte{'s', 't', 'b', 'l'}):
|
||||
case TypeSTBL:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].stbltable = new(movstbl)
|
||||
case mov_tag([4]byte{'s', 't', 's', 'd'}):
|
||||
case TypeSTSD:
|
||||
err = decodeStsdBox(demuxer)
|
||||
case mov_tag([4]byte{'s', 't', 't', 's'}):
|
||||
case TypeSTTS:
|
||||
err = decodeSttsBox(demuxer)
|
||||
case mov_tag([4]byte{'c', 't', 't', 's'}):
|
||||
case TypeCTTS:
|
||||
err = decodeCttsBox(demuxer)
|
||||
case mov_tag([4]byte{'s', 't', 's', 'c'}):
|
||||
case TypeSTSC:
|
||||
err = decodeStscBox(demuxer)
|
||||
case mov_tag([4]byte{'s', 't', 's', 'z'}):
|
||||
case TypeSTSZ:
|
||||
err = decodeStszBox(demuxer)
|
||||
case mov_tag([4]byte{'s', 't', 'c', 'o'}):
|
||||
case TypeSTCO:
|
||||
err = decodeStcoBox(demuxer)
|
||||
case mov_tag([4]byte{'c', 'o', '6', '4'}):
|
||||
case TypeCO64:
|
||||
err = decodeCo64Box(demuxer)
|
||||
case mov_tag([4]byte{'s', 't', 's', 's'}):
|
||||
case TypeSTSS:
|
||||
err = decodeStssBox(demuxer)
|
||||
case mov_tag([4]byte{'e', 'n', 'c', 'v'}):
|
||||
case TypeENCv:
|
||||
err = decodeVisualSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'s', 'i', 'n', 'f'}):
|
||||
case mov_tag([4]byte{'f', 'r', 'm', 'a'}):
|
||||
case TypeSINF:
|
||||
case TypeFRMA:
|
||||
err = decodeFrmaBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'s', 'c', 'h', 'i'}):
|
||||
case mov_tag([4]byte{'t', 'e', 'n', 'c'}):
|
||||
case TypeSCHI:
|
||||
case TypeTENC:
|
||||
err = decodeTencBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'a', 'v', 'c', '1'}):
|
||||
case TypeAVC1:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_H264
|
||||
demuxer.tracks[len(demuxer.tracks)-1].extra = new(h264ExtraData)
|
||||
err = decodeVisualSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'h', 'v', 'c', '1'}), mov_tag([4]byte{'h', 'e', 'v', '1'}):
|
||||
case TypeHVC1, TypeHEV1:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_H265
|
||||
demuxer.tracks[len(demuxer.tracks)-1].extra = newh265ExtraData()
|
||||
err = decodeVisualSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'e', 'n', 'c', 'a'}):
|
||||
case TypeENCA:
|
||||
err = decodeAudioSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'m', 'p', '4', 'a'}):
|
||||
case TypeMP4A:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_AAC
|
||||
demuxer.tracks[len(demuxer.tracks)-1].extra = new(aacExtraData)
|
||||
err = decodeAudioSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'u', 'l', 'a', 'w'}):
|
||||
case TypeULAW:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_G711U
|
||||
err = decodeAudioSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'a', 'l', 'a', 'w'}):
|
||||
case TypeALAW:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_G711A
|
||||
err = decodeAudioSampleEntry(demuxer)
|
||||
case mov_tag([4]byte{'o', 'p', 'u', 's'}):
|
||||
case TypeOPUS:
|
||||
demuxer.tracks[len(demuxer.tracks)-1].cid = MP4_CODEC_OPUS
|
||||
case mov_tag([4]byte{'a', 'v', 'c', 'C'}):
|
||||
case TypeAVCC:
|
||||
err = decodeAvccBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'h', 'v', 'c', 'C'}):
|
||||
case TypeHVCC:
|
||||
err = decodeHvccBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'e', 's', 'd', 's'}):
|
||||
case TypeESDS:
|
||||
err = decodeEsdsBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'e', 'd', 't', 's'}):
|
||||
case mov_tag([4]byte{'e', 'l', 's', 't'}):
|
||||
case TypeEDTS:
|
||||
case TypeELST:
|
||||
err = decodeElstBox(demuxer)
|
||||
case mov_tag([4]byte{'m', 'v', 'e', 'x'}):
|
||||
case TypeMVEX:
|
||||
demuxer.isFragement = true
|
||||
case mov_tag([4]byte{'m', 'o', 'o', 'f'}):
|
||||
case TypeMOOF:
|
||||
if demuxer.moofOffset, err = demuxer.reader.Seek(0, io.SeekCurrent); err != nil {
|
||||
break
|
||||
}
|
||||
demuxer.moofOffset -= 8
|
||||
demuxer.dataOffset = uint32(basebox.Size) + 8
|
||||
case mov_tag([4]byte{'m', 'f', 'h', 'd'}):
|
||||
case TypeMFHD:
|
||||
err = decodeMfhdBox(demuxer)
|
||||
case mov_tag([4]byte{'t', 'r', 'a', 'f'}):
|
||||
case mov_tag([4]byte{'t', 'f', 'h', 'd'}):
|
||||
case TypeTRAF:
|
||||
case TypeTFHD:
|
||||
err = decodeTfhdBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'t', 'f', 'd', 't'}):
|
||||
case TypeTFDT:
|
||||
err = decodeTfdtBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'t', 'r', 'u', 'n'}):
|
||||
case TypeTRUN:
|
||||
err = decodeTrunBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'s', 'e', 'n', 'c'}):
|
||||
case TypeSENC:
|
||||
err = decodeSencBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'s', 'a', 'i', 'z'}):
|
||||
case TypeSAIZ:
|
||||
err = decodeSaizBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'s', 'a', 'i', 'o'}):
|
||||
case TypeSAIO:
|
||||
err = decodeSaioBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'u', 'u', 'i', 'd'}):
|
||||
case TypeUUID:
|
||||
_, err = demuxer.reader.Seek(int64(basebox.Size)-BasicBoxLen-16, io.SeekCurrent)
|
||||
case mov_tag([4]byte{'s', 'g', 'p', 'd'}):
|
||||
case TypeSGPD:
|
||||
err = decodeSgpdBox(demuxer, uint32(basebox.Size))
|
||||
case mov_tag([4]byte{'w', 'a', 'v', 'e'}):
|
||||
case TypeWAVE:
|
||||
err = decodeWaveBox(demuxer)
|
||||
default:
|
||||
_, err = demuxer.reader.Seek(int64(basebox.Size)-BasicBoxLen, io.SeekCurrent)
|
||||
@@ -269,11 +267,12 @@ func (demuxer *MovDemuxer) ReadHead() ([]TrackInfo, error) {
|
||||
info.Timescale = track.timescale
|
||||
switch e := track.extra.(type) {
|
||||
case *h264ExtraData:
|
||||
data, err := h264parser.NewCodecDataFromSPSAndPPS(e.spss[0], e.ppss[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info.ExtraData = data.Record
|
||||
//data, err := h264parser.NewCodecDataFromSPSAndPPS(e.spss[0], e.ppss[0])
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//info.ExtraData = data.Record
|
||||
info.ExtraData = e.export()
|
||||
case *h265ExtraData:
|
||||
info.ExtraData = e.export()
|
||||
case *aacExtraData:
|
||||
|
||||
@@ -9,18 +9,15 @@ func decodeFrmaBox(demuxer *MovDemuxer, size uint32) (err error) {
|
||||
if _, err = io.ReadFull(demuxer.reader, buf); err != nil {
|
||||
return
|
||||
}
|
||||
var format [4]byte
|
||||
copy(format[:], buf)
|
||||
|
||||
track := demuxer.tracks[len(demuxer.tracks)-1]
|
||||
switch mov_tag(format) {
|
||||
case mov_tag([4]byte{'a', 'v', 'c', '1'}):
|
||||
switch *(*[4]byte)(buf) {
|
||||
case TypeAVC1:
|
||||
track.cid = MP4_CODEC_H264
|
||||
if track.extra == nil {
|
||||
track.extra = new(h264ExtraData)
|
||||
}
|
||||
return
|
||||
case mov_tag([4]byte{'m', 'p', '4', 'a'}):
|
||||
case TypeMP4A:
|
||||
track.cid = MP4_CODEC_AAC
|
||||
if track.extra == nil {
|
||||
track.extra = new(aacExtraData)
|
||||
|
||||
@@ -5,19 +5,6 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
var isom [4]byte = [4]byte{'i', 's', 'o', 'm'}
|
||||
var iso2 [4]byte = [4]byte{'i', 's', 'o', '2'}
|
||||
var iso3 [4]byte = [4]byte{'i', 's', 'o', '3'}
|
||||
var iso4 [4]byte = [4]byte{'i', 's', 'o', '4'}
|
||||
var iso5 [4]byte = [4]byte{'i', 's', 'o', '5'}
|
||||
var iso6 [4]byte = [4]byte{'i', 's', 'o', '6'}
|
||||
var avc1 [4]byte = [4]byte{'a', 'v', 'c', '1'}
|
||||
var mp41 [4]byte = [4]byte{'m', 'p', '4', '1'}
|
||||
var mp42 [4]byte = [4]byte{'m', 'p', '4', '2'}
|
||||
var dash [4]byte = [4]byte{'d', 'a', 's', 'h'}
|
||||
var msdh [4]byte = [4]byte{'m', 's', 'd', 'h'}
|
||||
var msix [4]byte = [4]byte{'m', 's', 'i', 'x'}
|
||||
|
||||
func mov_tag(tag [4]byte) uint32 {
|
||||
return binary.LittleEndian.Uint32(tag[:])
|
||||
}
|
||||
@@ -31,13 +18,13 @@ type FileTypeBox struct {
|
||||
|
||||
func NewFileTypeBox() *FileTypeBox {
|
||||
return &FileTypeBox{
|
||||
Box: NewBasicBox([4]byte{'f', 't', 'y', 'p'}),
|
||||
Box: NewBasicBox(TypeFTYP),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSegmentTypeBox() *FileTypeBox {
|
||||
return &FileTypeBox{
|
||||
Box: NewBasicBox([4]byte{'s', 't', 'y', 'p'}),
|
||||
Box: NewBasicBox(TypeSTYP),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ type MediaHeaderBox struct {
|
||||
func NewMediaHeaderBox() *MediaHeaderBox {
|
||||
_, offset := time.Now().Zone()
|
||||
return &MediaHeaderBox{
|
||||
Box: NewFullBox([4]byte{'m', 'd', 'h', 'd'}, 0),
|
||||
Box: NewFullBox(TypeMDHD, 0),
|
||||
Creation_time: uint64(time.Now().Unix() + int64(offset) + 0x7C25B080),
|
||||
Modification_time: uint64(time.Now().Unix() + int64(offset) + 0x7C25B080),
|
||||
Timescale: 1000,
|
||||
|
||||
@@ -34,18 +34,19 @@ type extraData interface {
|
||||
}
|
||||
|
||||
type h264ExtraData struct {
|
||||
spss [][]byte
|
||||
ppss [][]byte
|
||||
aacExtraData
|
||||
//spss [][]byte
|
||||
//ppss [][]byte
|
||||
}
|
||||
|
||||
func (extra *h264ExtraData) export() []byte {
|
||||
data, _ := codec.CreateH264AVCCExtradata(extra.spss, extra.ppss)
|
||||
return data
|
||||
}
|
||||
|
||||
func (extra *h264ExtraData) load(data []byte) {
|
||||
extra.spss, extra.ppss = codec.CovertExtradata(data)
|
||||
}
|
||||
//func (extra *h264ExtraData) export() []byte {
|
||||
// data, _ := codec.CreateH264AVCCExtradata(extra.spss, extra.ppss)
|
||||
// return data
|
||||
//}
|
||||
//
|
||||
//func (extra *h264ExtraData) load(data []byte) {
|
||||
// extra.spss, extra.ppss = codec.CovertExtradata(data)
|
||||
//}
|
||||
|
||||
type h265ExtraData struct {
|
||||
hvccExtra *codec.HEVCRecordConfiguration
|
||||
|
||||
@@ -64,13 +64,13 @@ func CreateMp4Muxer(w io.WriteSeeker, options ...MuxerOption) (*Movmuxer, error)
|
||||
|
||||
if !muxer.movFlag.isFragment() && !muxer.movFlag.isDash() {
|
||||
ftyp := NewFileTypeBox()
|
||||
ftyp.Major_brand = mov_tag(isom)
|
||||
ftyp.Major_brand = mov_tag(TypeISOM)
|
||||
ftyp.Minor_version = 0x200
|
||||
ftyp.Compatible_brands = make([]uint32, 4)
|
||||
ftyp.Compatible_brands[0] = mov_tag(isom)
|
||||
ftyp.Compatible_brands[1] = mov_tag(iso2)
|
||||
ftyp.Compatible_brands[2] = mov_tag(avc1)
|
||||
ftyp.Compatible_brands[3] = mov_tag(mp41)
|
||||
ftyp.Compatible_brands[0] = mov_tag(TypeISOM)
|
||||
ftyp.Compatible_brands[1] = mov_tag(TypeISO2)
|
||||
ftyp.Compatible_brands[2] = mov_tag(TypeAVC1)
|
||||
ftyp.Compatible_brands[3] = mov_tag(TypeMP41)
|
||||
length, boxdata := ftyp.Encode()
|
||||
_, err := muxer.writer.Write(boxdata[0:length])
|
||||
if err != nil {
|
||||
@@ -87,7 +87,7 @@ func CreateMp4Muxer(w io.WriteSeeker, options ...MuxerOption) (*Movmuxer, error)
|
||||
return nil, err
|
||||
}
|
||||
muxer.mdatOffset = uint32(currentOffset)
|
||||
muxer.mdat = &BasicBox{Type: [4]byte{'m', 'd', 'a', 't'}}
|
||||
muxer.mdat = &BasicBox{Type: TypeMDAT}
|
||||
muxer.mdat.Size = 8
|
||||
mdatlen, mdatBox := muxer.mdat.Encode()
|
||||
_, err = muxer.writer.Write(mdatBox[0:mdatlen])
|
||||
@@ -223,7 +223,7 @@ func (muxer *Movmuxer) ReWriteWithMoov(w io.Writer) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.CopyN(w, reader, int64(muxer.mdat.Size))
|
||||
_, err = io.CopyN(w, reader, int64(muxer.mdat.Size)-BasicBoxLen)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ func (muxer *Movmuxer) OnNewFragment(onFragment OnFragment) {
|
||||
}
|
||||
|
||||
func (muxer *Movmuxer) WriteInitSegment(w io.Writer) error {
|
||||
ftypBox := makeFtypBox(mov_tag(iso5), 0x200, []uint32{mov_tag(iso5), mov_tag(iso6), mov_tag(mp41)})
|
||||
ftypBox := makeFtypBox(mov_tag(TypeISO5), 0x200, []uint32{mov_tag(TypeISO5), mov_tag(TypeISO6), mov_tag(TypeMP41)})
|
||||
_, err := w.Write(ftypBox)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -282,7 +282,7 @@ func (muxer *Movmuxer) reWriteMdatSize() (err error) {
|
||||
}
|
||||
datalen := currentOffset - int64(muxer.mdatOffset)
|
||||
if datalen > 0xFFFFFFFF {
|
||||
muxer.mdat = &BasicBox{Type: [4]byte{'m', 'd', 'a', 't'}}
|
||||
muxer.mdat = &BasicBox{Type: TypeMDAT}
|
||||
muxer.mdat.Size = uint64(datalen + 8)
|
||||
mdatBoxLen, mdatBox := muxer.mdat.Encode()
|
||||
if _, err = muxer.writer.Seek(int64(muxer.mdatOffset)-8, io.SeekStart); err != nil {
|
||||
@@ -333,7 +333,7 @@ func (muxer *Movmuxer) writeMoov(w io.Writer) (err error) {
|
||||
moovsize += len(traks[i-1])
|
||||
}
|
||||
|
||||
moov := BasicBox{Type: [4]byte{'m', 'o', 'o', 'v'}}
|
||||
moov := BasicBox{Type: TypeMOOV}
|
||||
moov.Size = 8 + uint64(moovsize)
|
||||
offset, moovBox := moov.Encode()
|
||||
copy(moovBox[offset:], mvhd)
|
||||
@@ -358,7 +358,7 @@ func (muxer *Movmuxer) writeMfra() (err error) {
|
||||
|
||||
mfro := makeMfroBox(uint32(mfraSize) + 16)
|
||||
mfraSize += len(mfro)
|
||||
mfra := BasicBox{Type: [4]byte{'m', 'f', 'r', 'a'}}
|
||||
mfra := BasicBox{Type: TypeMFRA}
|
||||
mfra.Size = 8 + uint64(mfraSize)
|
||||
offset, mfraBox := mfra.Encode()
|
||||
for _, tfra := range tfras {
|
||||
@@ -381,7 +381,7 @@ func (muxer *Movmuxer) flushFragment() (err error) {
|
||||
|
||||
if muxer.movFlag.isFragment() {
|
||||
if muxer.nextFragmentId == 1 { //first fragment ,write moov
|
||||
ftypBox := makeFtypBox(mov_tag(iso5), 0x200, []uint32{mov_tag(iso5), mov_tag(iso6), mov_tag(mp41)})
|
||||
ftypBox := makeFtypBox(mov_tag(TypeISO5), 0x200, []uint32{mov_tag(TypeISO5), mov_tag(TypeISO6), mov_tag(TypeMP41)})
|
||||
_, err := muxer.writer.Write(ftypBox)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -427,7 +427,7 @@ func (muxer *Movmuxer) flushFragment() (err error) {
|
||||
}
|
||||
muxer.nextFragmentId++
|
||||
|
||||
moof := BasicBox{Type: [4]byte{'m', 'o', 'o', 'f'}}
|
||||
moof := BasicBox{Type: TypeMOOF}
|
||||
moof.Size = uint64(moofSize)
|
||||
offset, moofBox := moof.Encode()
|
||||
copy(moofBox[offset:], mfhd)
|
||||
@@ -437,12 +437,12 @@ func (muxer *Movmuxer) flushFragment() (err error) {
|
||||
offset += len(trafs[i])
|
||||
}
|
||||
|
||||
mdat := BasicBox{Type: [4]byte{'m', 'd', 'a', 't'}}
|
||||
mdat := BasicBox{Type: TypeMDAT}
|
||||
mdat.Size = 8
|
||||
_, mdatBox := mdat.Encode()
|
||||
|
||||
if muxer.movFlag.isDash() {
|
||||
stypBox := makeStypBox(mov_tag(msdh), 0, []uint32{mov_tag(msdh), mov_tag(msix)})
|
||||
stypBox := makeStypBox(mov_tag(TypeMSDH), 0, []uint32{mov_tag(TypeMSDH), mov_tag(TypeMSIX)})
|
||||
_, err := muxer.writer.Write(stypBox)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
"io"
|
||||
"m7s.live/m7s/v5"
|
||||
"m7s.live/m7s/v5/pkg/codec"
|
||||
"m7s.live/m7s/v5/pkg/config"
|
||||
"m7s.live/m7s/v5/pkg/util"
|
||||
"m7s.live/m7s/v5/plugin/mp4/pkg/box"
|
||||
rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Puller struct {
|
||||
m7s.HTTPFilePuller
|
||||
type (
|
||||
RecordReader struct {
|
||||
m7s.RecordFilePuller
|
||||
demuxer *box.MovDemuxer
|
||||
}
|
||||
HTTPReader struct {
|
||||
m7s.HTTPFilePuller
|
||||
}
|
||||
)
|
||||
|
||||
func NewPuller(conf config.Pull) m7s.IPuller {
|
||||
if strings.HasPrefix(conf.URL, "http") || strings.HasSuffix(conf.URL, ".mp4") {
|
||||
return &HTTPReader{}
|
||||
}
|
||||
return &RecordReader{}
|
||||
}
|
||||
|
||||
func NewPuller() m7s.IPuller {
|
||||
return &Puller{}
|
||||
}
|
||||
|
||||
func (p *Puller) Run() (err error) {
|
||||
ctx := &p.PullJob
|
||||
func (p *HTTPReader) Run() (err error) {
|
||||
pullJob := &p.PullJob
|
||||
var demuxer *box.MovDemuxer
|
||||
allocator := util.NewScalableMemoryAllocator(1 << 10)
|
||||
defer allocator.Recycle()
|
||||
switch v := p.ReadCloser.(type) {
|
||||
case io.ReadSeeker:
|
||||
demuxer = box.CreateMp4Demuxer(v)
|
||||
@@ -35,28 +50,30 @@ func (p *Puller) Run() (err error) {
|
||||
if tracks, err = demuxer.ReadHead(); err != nil {
|
||||
return
|
||||
}
|
||||
publisher := ctx.Publisher
|
||||
publisher := pullJob.Publisher
|
||||
for _, track := range tracks {
|
||||
switch track.Cid {
|
||||
case box.MP4_CODEC_H264:
|
||||
var sequece rtmp.RTMPVideo
|
||||
sequece.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequece)
|
||||
var sequence rtmp.RTMPVideo
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequence)
|
||||
case box.MP4_CODEC_H265:
|
||||
var sequece rtmp.RTMPVideo
|
||||
sequece.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequece)
|
||||
var sequence rtmp.RTMPVideo
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequence)
|
||||
case box.MP4_CODEC_AAC:
|
||||
var sequence rtmp.RTMPAudio
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0xaf, 0x00}, track.ExtraData)
|
||||
err = publisher.WriteAudio(&sequence)
|
||||
}
|
||||
}
|
||||
allocator := util.NewScalableMemoryAllocator(1 << 10)
|
||||
for {
|
||||
for !p.IsStopped() {
|
||||
pkg, err := demuxer.ReadPacket(allocator)
|
||||
if err != nil {
|
||||
ctx.Error("Error reading MP4 packet", "err", err)
|
||||
pullJob.Error("Error reading MP4 packet", "err", err)
|
||||
return err
|
||||
}
|
||||
switch track := tracks[pkg.TrackId-1]; track.Cid {
|
||||
@@ -105,4 +122,143 @@ func (p *Puller) Run() (err error) {
|
||||
err = publisher.WriteAudio(&audioFrame)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *RecordReader) Run() (err error) {
|
||||
pullJob := &p.PullJob
|
||||
publisher := pullJob.Publisher
|
||||
allocator := util.NewScalableMemoryAllocator(1 << 10)
|
||||
var ts int64
|
||||
var tsOffset int64
|
||||
defer allocator.Recycle()
|
||||
var firstKeyFrameSent bool
|
||||
for i, stream := range p.Streams {
|
||||
tsOffset = ts
|
||||
p.File, err = os.Open(filepath.Join(p.PullJob.RemoteURL, fmt.Sprintf("%d.mp4", stream.ID)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.demuxer = box.CreateMp4Demuxer(p.File)
|
||||
var tracks []box.TrackInfo
|
||||
if tracks, err = p.demuxer.ReadHead(); err != nil {
|
||||
return
|
||||
}
|
||||
if i == 0 {
|
||||
for _, track := range tracks {
|
||||
switch track.Cid {
|
||||
case box.MP4_CODEC_H264:
|
||||
var sequence rtmp.RTMPVideo
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0x17, 0x00, 0x00, 0x00, 0x00}, track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequence)
|
||||
case box.MP4_CODEC_H265:
|
||||
var sequence rtmp.RTMPVideo
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0b1001_0000 | rtmp.PacketTypeSequenceStart}, codec.FourCC_H265[:], track.ExtraData)
|
||||
err = publisher.WriteVideo(&sequence)
|
||||
case box.MP4_CODEC_AAC:
|
||||
var sequence rtmp.RTMPAudio
|
||||
sequence.SetAllocator(allocator)
|
||||
sequence.Append([]byte{0xaf, 0x00}, track.ExtraData)
|
||||
err = publisher.WriteAudio(&sequence)
|
||||
}
|
||||
}
|
||||
startTimestamp := p.PullStartTime.Sub(stream.StartTime).Milliseconds()
|
||||
if err = p.demuxer.SeekTime(uint64(startTimestamp)); err != nil {
|
||||
return
|
||||
}
|
||||
tsOffset = -startTimestamp
|
||||
}
|
||||
|
||||
for !p.IsStopped() {
|
||||
pkg, err := p.demuxer.ReadPacket(allocator)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
pullJob.Error("Error reading MP4 packet", "err", err)
|
||||
return err
|
||||
}
|
||||
ts = int64(pkg.Dts + uint64(tsOffset))
|
||||
switch track := tracks[pkg.TrackId-1]; track.Cid {
|
||||
case box.MP4_CODEC_H264:
|
||||
keyFrame := codec.ParseH264NALUType(pkg.Data[5]) == codec.NALU_IDR_Picture
|
||||
if !firstKeyFrameSent {
|
||||
if keyFrame {
|
||||
firstKeyFrameSent = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var videoFrame rtmp.RTMPVideo
|
||||
videoFrame.SetAllocator(allocator)
|
||||
videoFrame.CTS = uint32(pkg.Pts - pkg.Dts)
|
||||
videoFrame.Timestamp = uint32(ts)
|
||||
videoFrame.AppendOne([]byte{util.Conditoinal[byte](keyFrame, 0x17, 0x27), 0x01, byte(videoFrame.CTS >> 24), byte(videoFrame.CTS >> 8), byte(videoFrame.CTS)})
|
||||
videoFrame.AddRecycleBytes(pkg.Data)
|
||||
err = publisher.WriteVideo(&videoFrame)
|
||||
case box.MP4_CODEC_H265:
|
||||
var keyFrame bool
|
||||
switch codec.ParseH265NALUType(pkg.Data[5]) {
|
||||
case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
|
||||
h265parser.NAL_UNIT_CODED_SLICE_CRA:
|
||||
keyFrame = true
|
||||
}
|
||||
if !firstKeyFrameSent {
|
||||
if keyFrame {
|
||||
firstKeyFrameSent = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var videoFrame rtmp.RTMPVideo
|
||||
videoFrame.SetAllocator(allocator)
|
||||
videoFrame.CTS = uint32(pkg.Pts - pkg.Dts)
|
||||
videoFrame.Timestamp = uint32(ts)
|
||||
var head []byte
|
||||
var b0 byte = 0b1010_0000
|
||||
if keyFrame {
|
||||
b0 = 0b1001_0000
|
||||
}
|
||||
if videoFrame.CTS == 0 {
|
||||
head = videoFrame.NextN(5)
|
||||
head[0] = b0 | rtmp.PacketTypeCodedFramesX
|
||||
} else {
|
||||
head = videoFrame.NextN(8)
|
||||
head[0] = b0 | rtmp.PacketTypeCodedFrames
|
||||
util.PutBE(head[5:8], videoFrame.CTS) // cts
|
||||
}
|
||||
copy(head[1:], codec.FourCC_H265[:])
|
||||
videoFrame.AddRecycleBytes(pkg.Data)
|
||||
err = publisher.WriteVideo(&videoFrame)
|
||||
case box.MP4_CODEC_AAC:
|
||||
var audioFrame rtmp.RTMPAudio
|
||||
audioFrame.SetAllocator(allocator)
|
||||
audioFrame.Timestamp = uint32(ts)
|
||||
audioFrame.AppendOne([]byte{0xaf, 0x01})
|
||||
audioFrame.AddRecycleBytes(pkg.Data)
|
||||
err = publisher.WriteAudio(&audioFrame)
|
||||
case box.MP4_CODEC_G711A:
|
||||
var audioFrame rtmp.RTMPAudio
|
||||
audioFrame.SetAllocator(allocator)
|
||||
audioFrame.Timestamp = uint32(ts)
|
||||
audioFrame.AppendOne([]byte{0x72})
|
||||
audioFrame.AddRecycleBytes(pkg.Data)
|
||||
err = publisher.WriteAudio(&audioFrame)
|
||||
case box.MP4_CODEC_G711U:
|
||||
var audioFrame rtmp.RTMPAudio
|
||||
audioFrame.SetAllocator(allocator)
|
||||
audioFrame.Timestamp = uint32(ts)
|
||||
audioFrame.AppendOne([]byte{0x82})
|
||||
audioFrame.AddRecycleBytes(pkg.Data)
|
||||
err = publisher.WriteAudio(&audioFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"m7s.live/m7s/v5"
|
||||
"m7s.live/m7s/v5/pkg"
|
||||
"m7s.live/m7s/v5/pkg/codec"
|
||||
"m7s.live/m7s/v5/pkg/task"
|
||||
"m7s.live/m7s/v5/plugin/mp4/pkg/box"
|
||||
record "m7s.live/m7s/v5/plugin/record/pkg"
|
||||
rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,18 +21,6 @@ type WriteTrailerQueueTask struct {
|
||||
|
||||
var writeTrailerQueueTask WriteTrailerQueueTask
|
||||
|
||||
func init() {
|
||||
m7s.Servers.AddTaskLazy(&writeTrailerQueueTask)
|
||||
}
|
||||
|
||||
func NewRecorder() m7s.IRecorder {
|
||||
return &Recorder{}
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
}
|
||||
|
||||
type writeTrailerTask struct {
|
||||
task.Task
|
||||
muxer *box.Movmuxer
|
||||
@@ -61,53 +52,143 @@ func (task *writeTrailerTask) Start() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) Run() (err error) {
|
||||
func init() {
|
||||
m7s.Servers.AddTaskLazy(&writeTrailerQueueTask)
|
||||
}
|
||||
|
||||
func NewRecorder() m7s.IRecorder {
|
||||
return &Recorder{}
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
file *os.File
|
||||
muxer *box.Movmuxer
|
||||
stream record.RecordStream
|
||||
}
|
||||
|
||||
func (r *Recorder) writeTailer() {
|
||||
r.stream.EndTime = time.Now()
|
||||
r.RecordJob.Plugin.DB.Save(&r.stream)
|
||||
writeTrailerQueueTask.AddTask(&writeTrailerTask{
|
||||
file: r.file,
|
||||
muxer: r.muxer,
|
||||
}, r.Logger)
|
||||
}
|
||||
|
||||
func (r *Recorder) createStream() (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
var file *os.File
|
||||
var muxer *box.Movmuxer
|
||||
var audioId, videoId uint32
|
||||
// TODO: fragment
|
||||
if file, err = os.OpenFile(recordJob.FilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666); err != nil {
|
||||
return
|
||||
r.stream = record.RecordStream{
|
||||
StartTime: time.Now(),
|
||||
FilePath: recordJob.FilePath,
|
||||
}
|
||||
muxer, err = box.CreateMp4Muxer(file)
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.AudioConfig = sub.Publisher.AudioTrack.ICodecCtx.GetRecord()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
|
||||
r.stream.VideoConfig = sub.Publisher.VideoTrack.ICodecCtx.GetRecord()
|
||||
}
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
if r.RecordJob.Fragment == 0 {
|
||||
if r.file, err = os.Create(fmt.Sprintf("%d.mp4", r.RecordJob.FilePath)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if r.file, err = os.Create(filepath.Join(r.RecordJob.FilePath, fmt.Sprintf("%d.mp4", r.stream.ID))); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
r.muxer, err = box.CreateMp4Muxer(r.file)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Recorder) Start() (err error) {
|
||||
err = r.RecordJob.Plugin.DB.AutoMigrate(&r.stream)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.DefaultRecorder.Start()
|
||||
}
|
||||
|
||||
func (r *Recorder) Dispose() {
|
||||
if r.file != nil && r.muxer != nil {
|
||||
r.writeTailer()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) Run() (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
var audioId, videoId uint32
|
||||
err = r.createStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer writeTrailerQueueTask.AddTask(&writeTrailerTask{
|
||||
file: file,
|
||||
muxer: muxer,
|
||||
}, r.Logger)
|
||||
var at, vt *pkg.AVTrack
|
||||
//err = muxer.WriteInitSegment(file)
|
||||
|
||||
checkFragment := func(absTime uint32) (err error) {
|
||||
if duration := int64(absTime); time.Duration(duration)*time.Millisecond >= recordJob.Fragment {
|
||||
r.writeTailer()
|
||||
err = r.createStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
at, vt = nil, nil
|
||||
if vr := sub.VideoReader; vr != nil {
|
||||
vr.ResetAbsTime()
|
||||
//seq := vt.SequenceFrame.(*rtmp.RTMPVideo)
|
||||
//offset = int64(seq.Size + 15)
|
||||
}
|
||||
if ar := sub.AudioReader; ar != nil {
|
||||
ar.ResetAbsTime()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return m7s.PlayBlock(sub, func(audio *pkg.RawAudio) error {
|
||||
if sub.VideoReader == nil && recordJob.Fragment != 0 {
|
||||
err := checkFragment(sub.AudioReader.AbsTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if at == nil {
|
||||
at = sub.AudioReader.Track
|
||||
switch ctx := at.ICodecCtx.GetBase().(type) {
|
||||
case *codec.AACCtx:
|
||||
audioId = muxer.AddAudioTrack(box.MP4_CODEC_AAC, box.WithExtraData(ctx.ConfigBytes))
|
||||
audioId = r.muxer.AddAudioTrack(box.MP4_CODEC_AAC, box.WithExtraData(ctx.ConfigBytes))
|
||||
case *codec.PCMACtx:
|
||||
audioId = muxer.AddAudioTrack(box.MP4_CODEC_G711A, box.WithAudioSampleRate(uint32(ctx.SampleRate)), box.WithAudioChannelCount(uint8(ctx.Channels)), box.WithAudioSampleBits(uint8(ctx.SampleSize)))
|
||||
audioId = r.muxer.AddAudioTrack(box.MP4_CODEC_G711A, box.WithAudioSampleRate(uint32(ctx.SampleRate)), box.WithAudioChannelCount(uint8(ctx.Channels)), box.WithAudioSampleBits(uint8(ctx.SampleSize)))
|
||||
case *codec.PCMUCtx:
|
||||
audioId = muxer.AddAudioTrack(box.MP4_CODEC_G711U, box.WithAudioSampleRate(uint32(ctx.SampleRate)), box.WithAudioChannelCount(uint8(ctx.Channels)), box.WithAudioSampleBits(uint8(ctx.SampleSize)))
|
||||
audioId = r.muxer.AddAudioTrack(box.MP4_CODEC_G711U, box.WithAudioSampleRate(uint32(ctx.SampleRate)), box.WithAudioChannelCount(uint8(ctx.Channels)), box.WithAudioSampleBits(uint8(ctx.SampleSize)))
|
||||
}
|
||||
}
|
||||
return muxer.WriteSample(audioId, box.Sample{
|
||||
dts := sub.AudioReader.AbsTime
|
||||
return r.muxer.WriteSample(audioId, box.Sample{
|
||||
Data: audio.ToBytes(),
|
||||
PTS: uint32(audio.Timestamp / time.Millisecond),
|
||||
DTS: uint32(audio.Timestamp / time.Millisecond),
|
||||
PTS: dts,
|
||||
DTS: dts,
|
||||
})
|
||||
}, func(video *rtmp.RTMPVideo) error {
|
||||
if sub.VideoReader.Value.IDR && recordJob.Fragment != 0 {
|
||||
err := checkFragment(sub.VideoReader.AbsTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
offset := 5
|
||||
bytes := video.ToBytes()
|
||||
if vt == nil {
|
||||
vt = sub.VideoReader.Track
|
||||
switch ctx := vt.ICodecCtx.GetBase().(type) {
|
||||
case *codec.H264Ctx:
|
||||
videoId = muxer.AddVideoTrack(box.MP4_CODEC_H264, box.WithExtraData(ctx.Record), box.WithVideoWidth(uint32(ctx.Width())), box.WithVideoHeight(uint32(ctx.Height())))
|
||||
videoId = r.muxer.AddVideoTrack(box.MP4_CODEC_H264, box.WithExtraData(ctx.Record), box.WithVideoWidth(uint32(ctx.Width())), box.WithVideoHeight(uint32(ctx.Height())))
|
||||
case *codec.H265Ctx:
|
||||
videoId = muxer.AddVideoTrack(box.MP4_CODEC_H265, box.WithExtraData(ctx.Record), box.WithVideoWidth(uint32(ctx.Width())), box.WithVideoHeight(uint32(ctx.Height())))
|
||||
videoId = r.muxer.AddVideoTrack(box.MP4_CODEC_H265, box.WithExtraData(ctx.Record), box.WithVideoWidth(uint32(ctx.Width())), box.WithVideoHeight(uint32(ctx.Height())))
|
||||
}
|
||||
}
|
||||
switch ctx := vt.ICodecCtx.(type) {
|
||||
@@ -128,11 +209,11 @@ func (r *Recorder) Run() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
return muxer.WriteSample(videoId, box.Sample{
|
||||
return r.muxer.WriteSample(videoId, box.Sample{
|
||||
KeyFrame: sub.VideoReader.Value.IDR,
|
||||
Data: bytes[offset:],
|
||||
PTS: video.Timestamp + video.CTS,
|
||||
DTS: video.Timestamp,
|
||||
PTS: sub.VideoReader.AbsTime + video.CTS,
|
||||
DTS: sub.VideoReader.AbsTime,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ func (p *Puller) Run() (err error) {
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
if len(streams) == 0 {
|
||||
return fmt.Errorf("stream not found")
|
||||
}
|
||||
var startTimestamp int64
|
||||
beginTime := time.Now()
|
||||
speedControl := func(ts int64) {
|
||||
@@ -80,51 +83,62 @@ func (p *Puller) Run() (err error) {
|
||||
startTimestamp = p.PullStartTime.Sub(stream.StartTime).Milliseconds()
|
||||
hasAudio, hasVideo := stream.AudioCodec != "", stream.VideoCodec != ""
|
||||
audioFourCC, videoFourCC := codec.ParseFourCC(stream.AudioCodec), codec.ParseFourCC(stream.VideoCodec)
|
||||
var startId uint
|
||||
if hasAudio && audioFourCC == codec.FourCC_MP4A {
|
||||
var rawAudio pkg.RawAudio
|
||||
rawAudio.SetAllocator(p.allocator)
|
||||
rawAudio.FourCC = audioFourCC
|
||||
rawAudio.Memory.AppendOne(stream.AudioConfig)
|
||||
err = p.PullJob.Publisher.WriteAudio(&rawAudio)
|
||||
}
|
||||
if hasVideo {
|
||||
var rawVideo pkg.H26xFrame
|
||||
rawVideo.SetAllocator(p.allocator)
|
||||
rawVideo.FourCC = videoFourCC
|
||||
switch videoFourCC {
|
||||
case codec.FourCC_H264:
|
||||
conf, _ := h264parser.NewCodecDataFromAVCDecoderConfRecord(stream.VideoConfig)
|
||||
conf, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(stream.VideoConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawVideo.Nalus.Append(conf.SPS())
|
||||
rawVideo.Nalus.Append(conf.PPS())
|
||||
case codec.FourCC_H265:
|
||||
conf, _ := h265parser.NewCodecDataFromAVCDecoderConfRecord(stream.VideoConfig)
|
||||
conf, err := h265parser.NewCodecDataFromAVCDecoderConfRecord(stream.VideoConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawVideo.Nalus.Append(conf.VPS())
|
||||
rawVideo.Nalus.Append(conf.SPS())
|
||||
rawVideo.Nalus.Append(conf.PPS())
|
||||
}
|
||||
err = p.PullJob.Publisher.WriteVideo(&rawVideo)
|
||||
var keyFrame Sample
|
||||
tx = streamDB.Last(&keyFrame, "type=? AND timestamp<=?", FRAME_TYPE_VIDEO_KEY_FRAME, startTimestamp)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
startId = keyFrame.ID
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
rows, err := streamDB.Model(&Sample{}).Where("id>=? ", startId).Rows()
|
||||
var keyFrame Sample
|
||||
tx = streamDB.Last(&keyFrame, "type=? AND timestamp<=?", util.Conditoinal(hasVideo, FRAME_TYPE_VIDEO_KEY_FRAME, FRAME_TYPE_AUDIO), startTimestamp)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
rows, err := streamDB.Model(&Sample{}).Where("id>=? ", keyFrame.ID).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var seek bool
|
||||
for rows.Next() {
|
||||
if p.IsStopped() {
|
||||
return p.StopReason()
|
||||
}
|
||||
var frame Sample
|
||||
streamDB.ScanRows(rows, &frame)
|
||||
if !seek {
|
||||
seek = true
|
||||
file.Seek(frame.Offset, io.SeekStart)
|
||||
}
|
||||
switch frame.Type {
|
||||
case FRAME_TYPE_AUDIO:
|
||||
var rawAudio pkg.RawAudio
|
||||
rawAudio.FourCC = audioFourCC
|
||||
rawAudio.SetAllocator(p.allocator)
|
||||
rawAudio.Timestamp = time.Duration(frame.Timestamp) * time.Millisecond
|
||||
rawAudio.FourCC = audioFourCC
|
||||
file.Seek(frame.Offset, io.SeekStart)
|
||||
file.Read(rawAudio.NextN(int(frame.Length)))
|
||||
err = p.PullJob.Publisher.WriteAudio(&rawAudio)
|
||||
case FRAME_TYPE_VIDEO, FRAME_TYPE_VIDEO_KEY_FRAME:
|
||||
@@ -132,8 +146,13 @@ func (p *Puller) Run() (err error) {
|
||||
rawVideo.FourCC = videoFourCC
|
||||
rawVideo.SetAllocator(p.allocator)
|
||||
rawVideo.Timestamp = time.Duration(frame.Timestamp) * time.Millisecond
|
||||
file.Seek(frame.Offset, io.SeekStart)
|
||||
file.Read(rawVideo.NextN(int(frame.Length)))
|
||||
n, err := file.Read(rawVideo.NextN(int(frame.Length)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != int(frame.Length) {
|
||||
return fmt.Errorf("read frame error")
|
||||
}
|
||||
r := rawVideo.NewReader()
|
||||
for {
|
||||
nalulen, err := r.ReadBE(4)
|
||||
|
||||
@@ -2,7 +2,6 @@ package record
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -78,13 +77,15 @@ func (r *Recorder) Start() (err error) {
|
||||
func (r *Recorder) Run() (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
var offset int64
|
||||
return m7s.PlayBlock(sub, func(audio *pkg.RawAudio) (err error) {
|
||||
var sample Sample
|
||||
sample.Type = FRAME_TYPE_AUDIO
|
||||
sample.Timestamp = audio.Timestamp.Milliseconds()
|
||||
sample.Length = uint(audio.Size)
|
||||
data := slices.Clone(audio.Buffers)
|
||||
sample.Offset, err = r.file.Seek(0, io.SeekCurrent)
|
||||
sample.Offset = offset
|
||||
offset += int64(sample.Length)
|
||||
_, err = data.WriteTo(r.file)
|
||||
r.DB.Save(&sample)
|
||||
return
|
||||
@@ -96,12 +97,13 @@ func (r *Recorder) Run() (err error) {
|
||||
}
|
||||
sample.Timestamp = video.Timestamp.Milliseconds()
|
||||
sample.CTS = video.CTS.Milliseconds()
|
||||
sample.Offset, err = r.file.Seek(0, io.SeekCurrent)
|
||||
sample.Offset = offset
|
||||
for _, nalu := range video.Nalus {
|
||||
sample.Length += uint(nalu.Size) + 4
|
||||
avcc := append(net.Buffers{[]byte{byte(nalu.Size >> 24), byte(nalu.Size >> 16), byte(nalu.Size >> 8), byte(nalu.Size)}}, nalu.Buffers...)
|
||||
_, err = avcc.WriteTo(r.file)
|
||||
}
|
||||
offset += int64(sample.Length)
|
||||
r.DB.Save(&sample)
|
||||
return
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package rtmp
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"m7s.live/m7s/v5/pkg/config"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -78,7 +79,7 @@ func (c *Client) GetPushJob() *m7s.PushJob {
|
||||
return &c.pushCtx
|
||||
}
|
||||
|
||||
func NewPuller() m7s.IPuller {
|
||||
func NewPuller(_ config.Pull) m7s.IPuller {
|
||||
ret := &Client{
|
||||
direction: DIRECTION_PULL,
|
||||
chunkSize: 4096,
|
||||
|
||||
@@ -2,6 +2,7 @@ package rtsp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"m7s.live/m7s/v5/pkg/config"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -71,7 +72,7 @@ func (c *Client) GetPushJob() *m7s.PushJob {
|
||||
return &c.pushCtx
|
||||
}
|
||||
|
||||
func NewPuller() m7s.IPuller {
|
||||
func NewPuller(_ config.Pull) m7s.IPuller {
|
||||
client := &Client{
|
||||
direction: DIRECTION_PULL,
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
func (r *StressPlugin) pull(count int, format, url string, puller m7s.Puller) (err error) {
|
||||
if i := r.pullers.Length; count > i {
|
||||
for j := i; j < count; j++ {
|
||||
p := puller()
|
||||
ctx := p.GetPullJob().Init(p, &r.Plugin, fmt.Sprintf("stress/%d", j), config.Pull{URL: fmt.Sprintf(format, url, j)})
|
||||
conf := config.Pull{URL: fmt.Sprintf(format, url, j)}
|
||||
p := puller(conf)
|
||||
ctx := p.GetPullJob().Init(p, &r.Plugin, fmt.Sprintf("stress/%d", j), conf)
|
||||
if err = ctx.WaitStarted(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func (s *SpeedControl) speedControl(speed float64, ts time.Duration) {
|
||||
}
|
||||
should := time.Duration(float64(ts) / speed)
|
||||
s.Delta = should - elapsed
|
||||
//fmt.Println(speed, elapsed, should, s.Delta)
|
||||
if s.Delta > threshold {
|
||||
time.Sleep(s.Delta)
|
||||
}
|
||||
|
||||
69
puller.go
69
puller.go
@@ -1,14 +1,17 @@
|
||||
package m7s
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"m7s.live/m7s/v5/pkg"
|
||||
"m7s.live/m7s/v5/pkg/config"
|
||||
"m7s.live/m7s/v5/pkg/task"
|
||||
"m7s.live/m7s/v5/pkg/util"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -27,7 +30,7 @@ type (
|
||||
GetPullJob() *PullJob
|
||||
}
|
||||
|
||||
Puller = func() IPuller
|
||||
Puller = func(config.Pull) IPuller
|
||||
|
||||
PullJob struct {
|
||||
Connection
|
||||
@@ -41,6 +44,15 @@ type (
|
||||
PullJob PullJob
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
RecordFilePuller struct {
|
||||
task.Task
|
||||
PullJob PullJob
|
||||
PullStartTime time.Time
|
||||
Streams []RecordStream
|
||||
File *os.File
|
||||
offsetTime time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
func (conn *Connection) Init(plugin *Plugin, streamPath string, href string, proxyConf string, header http.Header) {
|
||||
@@ -68,7 +80,24 @@ func (p *PullJob) Init(puller IPuller, plugin *Plugin, streamPath string, conf c
|
||||
publishConfig.PublishTimeout = 0
|
||||
p.publishConfig = &publishConfig
|
||||
p.Args = conf.Args
|
||||
p.Connection.Init(plugin, streamPath, conf.URL, conf.Proxy, conf.Header)
|
||||
remoteURL := conf.URL
|
||||
u, err := url.Parse(remoteURL)
|
||||
if err == nil {
|
||||
if u.Host == "" {
|
||||
// file
|
||||
remoteURL = u.Path
|
||||
}
|
||||
if p.Args == nil {
|
||||
p.Args = u.Query()
|
||||
} else {
|
||||
for k, v := range u.Query() {
|
||||
for _, vv := range v {
|
||||
p.Args.Add(k, vv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Connection.Init(plugin, streamPath, remoteURL, conf.Proxy, conf.Header)
|
||||
p.puller = puller
|
||||
p.Description = map[string]any{
|
||||
"plugin": plugin.Meta.Name,
|
||||
@@ -134,3 +163,39 @@ func (p *HTTPFilePuller) GetPullJob() *PullJob {
|
||||
func (p *HTTPFilePuller) Dispose() {
|
||||
p.ReadCloser.Close()
|
||||
}
|
||||
|
||||
func (p *RecordFilePuller) GetPullJob() *PullJob {
|
||||
return &p.PullJob
|
||||
}
|
||||
|
||||
func (p *RecordFilePuller) SpeedControl(ts int64) {
|
||||
targetTime := time.Duration(float64(time.Since(p.PullJob.StartTime)) * p.PullJob.Publisher.Speed)
|
||||
timestamp := time.Duration(ts) * time.Millisecond
|
||||
if ts > 0 && p.offsetTime == 0 {
|
||||
p.offsetTime = timestamp
|
||||
}
|
||||
sleepTime := timestamp - targetTime - p.offsetTime
|
||||
p.Trace("SpeedControl", "timestamp", timestamp, "targetTime", targetTime, "offsetTime", p.offsetTime, "sleepTime", sleepTime)
|
||||
if sleepTime > 0 {
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RecordFilePuller) Start() (err error) {
|
||||
if err = p.PullJob.Publish(); err != nil {
|
||||
return
|
||||
}
|
||||
if p.PullStartTime, err = util.TimeQueryParse(p.PullJob.Args.Get("start")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tx := p.PullJob.Plugin.DB.Find(&p.Streams, "end_time>? AND file_path=?", p.PullStartTime, p.PullJob.RemoteURL)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
if len(p.Streams) == 0 {
|
||||
return fmt.Errorf("stream not found")
|
||||
}
|
||||
p.Info("vod", "streams", p.Streams)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ type (
|
||||
task.Task
|
||||
RecordJob RecordJob
|
||||
}
|
||||
RecordStream struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
StartTime, EndTime time.Time
|
||||
FilePath string
|
||||
AudioCodec, VideoCodec string
|
||||
}
|
||||
)
|
||||
|
||||
func (r *DefaultRecorder) GetRecordJob() *RecordJob {
|
||||
|
||||
@@ -37,8 +37,6 @@ func (ps *PubSubBase) Init(streamPath string, conf any) {
|
||||
}
|
||||
// args to config
|
||||
if len(ps.Args) != 0 {
|
||||
var c config.Config
|
||||
c.Parse(conf)
|
||||
ignores, cc := make(map[string]struct{}), make(map[string]any)
|
||||
for key, value := range ps.Args {
|
||||
if strings.HasSuffix(key, "ArgName") {
|
||||
@@ -50,7 +48,7 @@ func (ps *PubSubBase) Init(streamPath string, conf any) {
|
||||
cc[strings.ToLower(key)] = value[0]
|
||||
}
|
||||
}
|
||||
c.ParseModifyFile(cc)
|
||||
config.Parse(conf, cc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,11 +174,10 @@ func (s *Subscriber) createVideoReader(dataType reflect.Type, startVideoTs time.
|
||||
|
||||
type SubscribeHandler[A any, V any] struct {
|
||||
task.Task
|
||||
s *Subscriber
|
||||
OnAudio func(A) error
|
||||
OnVideo func(V) error
|
||||
ProcessAudio chan func(*AVFrame)
|
||||
ProcessVideo chan func(*AVFrame)
|
||||
s *Subscriber
|
||||
OnAudio func(A) error
|
||||
OnVideo func(V) error
|
||||
ProcessAudio, ProcessVideo chan func(*AVFrame)
|
||||
}
|
||||
|
||||
func CreatePlayTask[A any, V any](s *Subscriber, onAudio func(A) error, onVideo func(V) error) task.ITask {
|
||||
|
||||
Reference in New Issue
Block a user