- '**.go'
- '**.mod'
- '**.sum'
+ - '**.flv'
+ - '**.mp4'
jobs:
buildtest:
* closing connection #0
```
-添加快速索引文件生成,将在录制完成后,读取视频文件,并将关键帧的时间戳和对应的下标值记录在`.fastSeed`文件,用于加快后续切片请求响应。(>v0.14.28)
+æ·»å\8a å¿«é\80\9fç´¢å¼\95æ\96\87ä»¶ç\94\9fæ\88\90ï¼\8cå°\86å\9c¨å½\95å\88¶å®\8cæ\88\90å\90\8eï¼\8cè\87ªå®\9aä¹\89å\9b\9eè°\83å\91½ä»¤æ\89§è¡\8cå\89\8dï¼\8c读å\8f\96è§\86é¢\91æ\96\87ä»¶ï¼\8cå¹¶å°\86å\85³é\94®å¸§ç\9a\84æ\97¶é\97´æ\88³å\92\8c对åº\94ç\9a\84ä¸\8bæ \87å\80¼è®°å½\95å\9c¨`.fastSeed`æ\96\87ä»¶ï¼\8cç\94¨äº\8eå\8a å¿«å\90\8eç»å\88\87ç\89\87请æ±\82å\93\8dåº\94ã\80\82(>v0.14.28)
-相较于之前的请求时进行查找,效率提升如下(以mp4格式为例)
+可以设置`禁用快速索引生成`(默认为`false`)为`true`来禁用这个动作(>v0.14.28)
+
+相较于之前的请求时进行查找,效率提升如下
```
+mp4:
旧:
// 10s-30s 110.962896ms
// 10m-10m20s 1.955749395s
// 10s-30s 90.05983ms
// 10m-10m20s 88.769475ms
// 30m-30m20s 104.381225ms
+
+flv:
+旧:
+// 10s-30s 184.852917ms
+// 10m-10m20s 3.278605875s
+
+新:
+// 10s-30s 215.815423ms
+// 10m-10m20s 174.918508ms
```
`.fastSeed`文件格式:
if v, ok := c.C.K_v.LoadV(`flv音视频时间戳容差ms`).(float64); ok && v > 100 {
flvDecoder.Diff = v
}
- if e := flvDecoder.Cut(f, startT, duration, res); e != nil && !errors.Is(e, io.EOF) {
- flog.L(`E: `, e)
+ // fastSeed
+ if fastSeedF := file.New(v+".fastSeed", 0, true); fastSeedF.IsExist() {
+ if gf, e := replyFunc.VideoFastSeed.InitGet(v + ".fastSeed"); e != nil {
+ flog.L(`E: `, e)
+ } else if e := flvDecoder.CutSeed(f, startT, duration, res, f, gf); e != nil && !errors.Is(e, io.EOF) {
+ flog.L(`E: `, e)
+ }
+ } else {
+ if e := flvDecoder.Cut(f, startT, duration, res); e != nil && !errors.Is(e, io.EOF) {
+ flog.L(`E: `, e)
+ }
}
}
if strings.HasSuffix(v, "mp4") {
return
}
-func (t *FlvDecoder) oneF(buf []byte, ifWrite func(t int) bool, w ...io.Writer) (dropOffset int, err error) {
+type dealFFlv func(t int, index int, buf []byte) error
+
+func (t *FlvDecoder) oneF(buf []byte, w ...dealFFlv) (dropOffset int, err error) {
if !t.init {
err = ErrNoInit
if buf[bufOffset] == videoTag && buf[bufOffset+11]&0xf0 == 0x10 { //key frame
if keyframeOp >= 0 && len(w) > 0 {
dropOffset = bufOffset
- if ifWrite(timeStamp) {
- _, err = w[0].Write(buf[keyframeOp:bufOffset])
- }
+ err = w[0](timeStamp, keyframeOp, buf[keyframeOp:bufOffset])
return
}
keyframeOp = bufOffset
return
}
+// Deprecated: 效率低于GenFastSeed+CutSeed
func (t *FlvDecoder) Cut(reader io.Reader, startT, duration time.Duration, w io.Writer) (err error) {
+ return t.CutSeed(reader, startT, duration, w, nil, nil)
+}
+
+func (t *FlvDecoder) CutSeed(reader io.Reader, startT, duration time.Duration, w io.Writer, seeker io.Seeker, getIndex func(seedTo time.Duration) (int64, error)) (err error) {
bufSize := humanize.KByte * 1100
buf := make([]byte, humanize.KByte*500)
buff := slice.New[byte]()
over := false
+ seek := false
startTM := startT.Milliseconds()
durationM := duration.Milliseconds()
firstFT := -1
- ifWriteF := func(t int) bool {
+ wf := func(t int, index int, buf []byte) (e error) {
if firstFT == -1 {
firstFT = t
}
cu := int64(t - firstFT)
over = duration != 0 && cu > durationM+startTM
if startTM <= cu && !over {
- return true
+ _, e = w.Write(buf)
}
- return false
+ return
}
for c := 0; err == nil && !over; c++ {
}
}
} else {
- if dropOffset, e := t.oneF(buff.GetPureBuf(), ifWriteF, w); e != nil {
+ if !seek && seeker != nil && getIndex != nil {
+ if index, e := getIndex(startT); e != nil {
+ return perrors.New("s", e.Error())
+ } else {
+ if _, e := seeker.Seek(index, io.SeekStart); e != nil {
+ return perrors.New("s", e.Error())
+ }
+ }
+ seek = true
+ startTM = 0
+ buff.Clear()
+ }
+ if dropOffset, e := t.oneF(buff.GetPureBuf(), wf); e != nil {
+ return perrors.New("skip", e.Error())
+ } else {
+ if dropOffset != 0 {
+ _ = buff.RemoveFront(dropOffset)
+ } else {
+ bufSize *= 2
+ }
+ }
+ }
+ }
+ return
+}
+
+func (t *FlvDecoder) GenFastSeed(reader io.Reader, save func(seedTo time.Duration, cuIndex int64) error) (err error) {
+ bufSize := humanize.KByte * 1100
+ totalRead := 0
+ buf := make([]byte, humanize.KByte*500)
+ buff := slice.New[byte]()
+ over := false
+ firstFT := -1
+
+ for c := 0; err == nil && !over; c++ {
+ if buff.Size() < bufSize {
+ n, e := reader.Read(buf)
+ if n == 0 && errors.Is(e, io.EOF) {
+ return io.EOF
+ }
+ totalRead += n
+ err = buff.Append(buf[:n])
+ continue
+ }
+
+ if !t.init {
+ if frontBuf, dropOffset, e := t.InitFlv(buff.GetPureBuf()); e != nil {
+ return perrors.New("InitFlv", e.Error())
+ } else {
+ if dropOffset != 0 {
+ _ = buff.RemoveFront(dropOffset)
+ } else {
+ bufSize *= 2
+ }
+ if len(frontBuf) == 0 {
+ continue
+ }
+ }
+ } else {
+ if dropOffset, e := t.oneF(buff.GetPureBuf(), func(t, index int, buf []byte) error {
+ if firstFT == -1 {
+ firstFT = t
+ }
+ return save(time.Millisecond*time.Duration(t-firstFT), int64(totalRead-buff.Size()+index))
+ }); e != nil {
return perrors.New("skip", e.Error())
} else {
if dropOffset != 0 {
"time"
"github.com/dustin/go-humanize"
+ comp "github.com/qydysky/part/component2"
perrors "github.com/qydysky/part/errors"
file "github.com/qydysky/part/file"
slice "github.com/qydysky/part/slice"
t.Log("max", humanize.Bytes(uint64(max)))
}
+// 10s-30s 184.852917ms
+// 10m-10m20s 3.278605875s
func Test_FLVCut(t *testing.T) {
+ {
+ st := time.Now()
+ defer func() {
+ fmt.Println(time.Since(st))
+ }()
+ }
+ cutf := file.New("testdata/0.cut.flv", 0, false)
+ defer cutf.Close()
+ _ = cutf.Delete()
+
+ f := file.New("testdata/0.flv", 0, false)
+ defer f.Close()
+
+ if f.IsDir() || !f.IsExist() {
+ t.Log("test file not exist")
+ }
+
+ e := NewFlvDecoder().Cut(f, time.Minute*10, time.Second*20, cutf.File())
+ if perrors.Catch(e, "Read") {
+ t.Log("err Read", e)
+ }
+ if perrors.Catch(e, "InitFlv") {
+ t.Log("err InitFlv", e)
+ }
+ if perrors.Catch(e, "skip") {
+ t.Log("err skip", e)
+ }
+ if perrors.Catch(e, "cutW") {
+ t.Log("err cutW", e)
+ }
+ t.Log(e)
+}
+
+func Test_FLVGenFastSeed(t *testing.T) {
+ var VideoFastSeed = comp.Get[interface {
+ InitGet(fastSeedFilePath string) (getIndex func(seedTo time.Duration) (int64, error), e error)
+ InitSav(fastSeedFilePath string) (savIndex func(seedTo time.Duration, cuIndex int64) error, e error)
+ }](`videoFastSeed`)
+
+ f := file.New("testdata/0.flv", 0, false)
+ defer f.Close()
+ sf, e := VideoFastSeed.InitSav("testdata/0.flv.fastSeed")
+ if e != nil {
+ t.Fatal(e)
+ }
+
+ if f.IsDir() || !f.IsExist() {
+ t.Log("test file not exist")
+ }
- cutf := file.New("testdata/1.cut.flv", 0, false)
+ e = NewFlvDecoder().GenFastSeed(f, func(seedTo time.Duration, cuIndex int64) error {
+ return sf(seedTo, cuIndex)
+ })
+ if perrors.Catch(e, "Read") {
+ t.Log("err Read", e)
+ }
+ if perrors.Catch(e, "InitFlv") {
+ t.Log("err InitFlv", e)
+ }
+ if perrors.Catch(e, "skip") {
+ t.Log("err skip", e)
+ }
+ if perrors.Catch(e, "cutW") {
+ t.Log("err cutW", e)
+ }
+ t.Log(e)
+}
+
+// 10s-30s 215.815423ms
+// 10m-10m20s 174.918508ms
+func Test_FLVCutSeed(t *testing.T) {
+ {
+ st := time.Now()
+ defer func() {
+ fmt.Println(time.Since(st))
+ }()
+ }
+ cutf := file.New("testdata/0.cut.flv", 0, false)
defer cutf.Close()
_ = cutf.Delete()
- f := file.New("testdata/1.flv", 0, false)
+ f := file.New("testdata/0.flv", 0, false)
defer f.Close()
if f.IsDir() || !f.IsExist() {
t.Log("test file not exist")
}
- e := NewFlvDecoder().Cut(f, time.Second*10, time.Second*20, cutf.File())
+ var VideoFastSeed = comp.Get[interface {
+ InitGet(fastSeedFilePath string) (getIndex func(seedTo time.Duration) (int64, error), e error)
+ InitSav(fastSeedFilePath string) (savIndex func(seedTo time.Duration, cuIndex int64) error, e error)
+ }](`videoFastSeed`)
+
+ gf, e := VideoFastSeed.InitGet("testdata/0.flv.fastSeed")
+ if e != nil {
+ t.Fatal(e)
+ }
+
+ e = NewFlvDecoder().CutSeed(f, time.Minute*10, time.Second*20, cutf.File(), f, gf)
if perrors.Catch(e, "Read") {
t.Log("err Read", e)
}
return
}
-type dealF func(t float64, index int, buf *slice.Buf[byte]) error
+type dealFMp4 func(t float64, index int, buf *slice.Buf[byte]) error
-func (t *Fmp4Decoder) oneF(buf []byte, w ...dealF) (cu int, err error) {
+func (t *Fmp4Decoder) oneF(buf []byte, w ...dealFMp4) (cu int, err error) {
if len(buf) > humanize.MByte*100 {
return 0, ErrBufTooLarge
}
duration := time.Since(startT)
//PusherToFile fin genFastSeed
- {
+ if disableFastSeed, ok := ms.common.K_v.LoadV("禁用快速索引生成").(bool); !ok || !disableFastSeed {
+ type deal interface {
+ GenFastSeed(reader io.Reader, save func(seedTo time.Duration, cuIndex int64) error) (err error)
+ }
+ var dealer deal
+
switch ms.GetStreamType() {
case `mp4`:
fmp4Decoder := NewFmp4Decoder()
if v, ok := ms.common.K_v.LoadV(`fmp4音视频时间戳容差s`).(float64); ok && v > 0.1 {
fmp4Decoder.AVTDiff = v
}
+ dealer = fmp4Decoder
+ case `flv`:
+ flvDecoder := NewFlvDecoder()
+ if v, ok := ms.common.K_v.LoadV(`flv音视频时间戳容差ms`).(float64); ok && v > 100 {
+ flvDecoder.Diff = v
+ }
+ dealer = flvDecoder
+ default:
+ }
+
+ if dealer != nil {
f := file.New(path, 0, false)
if sf, e := replyFunc.VideoFastSeed.InitSav(path + ".fastSeed"); e != nil {
l.L(`E: `, e)
- } else if e := fmp4Decoder.GenFastSeed(f, sf); e != nil && !errors.Is(e, io.EOF) {
+ } else if e := dealer.GenFastSeed(f, sf); e != nil && !errors.Is(e, io.EOF) {
l.L(`E: `, e)
}
f.Close()
- default:
}
}
"标题修改检测s-help": "默认900秒,少于默认无效。直播间标题引入审核机制,触发审核时会接收到一个roomchange但标题不变,将持续检测指定时长,如通过审核将修改录播标题",
"标题修改检测s": 900,
"直播流保存到文件": true,
+ "禁用快速索引生成-help": "默认false,为false时,在录制结束后读取文件生成快速索引,用于快速响应切片请求",
+ "禁用快速索引生成": false,
"仅保存当前直播间流-help": "启用此项,才会保存Ass",
"仅保存当前直播间流": true,
"修改标题时重新录制": true,