feat: mp4 pull from record files

This commit is contained in:
langhuihui
2024-09-09 13:49:03 +08:00
parent 2f75168faf
commit 6ad3c8bddf
27 changed files with 647 additions and 245 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
// }
// }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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